TL;DR; Instead of using the default FormattedMessage
component and useIntl
hook from react-intl, you should provide your own version of these components with custom variables injected by default. Creating your own wrapper will simplify your code and give more power to your translation keys.
When it comes to internationalization in React the react-intl package is a robust choice and will give you plenty of flexibility in terms of making your translations dynamic, handling pluralization etc.
But like with your code, there are plenty of constants you do not want to hardcode into your translations. This also applies to links and small components you wish to embed inside your translations.
Fortunately it is possible to wrap react-intl inside custom hooks and components to extend it with custom context and markup.
In this article, I will start by showing the basics of using react-intl then show you how to provide your own customized version for better reusability.
Setting up react-intl
Installing and configuring react-intl inside your application is fairly straightforward.
First add the package to your application
# With npm
npm i -S react-intl
# With yarn
yarn add react-intl
Then create a lang folder with an english translation file:
// src/lang/locales/en_US.ts
const messages = {
'default.welcome': 'Welcome to my app!'
}
export default messages;
Add a registry with all your available languages:
// src/lang/index.ts
import enMessages from './locales/en_US';
interface LocaleConfig {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
const appLocales: LocaleConfig = {
en: {
messages: enMessages,
locale: 'en-US'
}
};
export default appLocales;
Finally, configure your top App Component to use react-intl with the chosen language:
// src/index.ts
import React, { FunctionComponent } from 'react';
import { IntlProvider, FormattedMessage } from 'react-intl';
import appLocales from 'lang/index';
const App: FunctionComponent = () => {
// Get the locale to use. You could use Redux, useContext, URL params or local storage
// to manage this value.
const locale = 'en';
// Load the language configuration
const localeConfig = appLocales[locale];
// Application top component (entrypoint)
return (
<IntlProvider locale={localeConfig.locale} messages={localeConfig.messages}>
{/* Add your first translated text */}
<FormattedMessage id="default.welcome" />
</IntlProvider>
);
}
export default App;
Alright, we've got react-intl setup. Now let's cover the basics of how to use it.
Using react-intl
In this section we'll see how to use react-intl to translate messages and how to dynamically format these messages.
Component & Hook
There are two ways to use react-intl: components and hooks. They're essentially the same. Let's see both approaches.
Here is how to use react-intl using a component. This is the easiest and most common use of react-intl.
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from 'react-intl';
const MyComponent: FunctionComponent = () => {
return (
<div>
{/* This component will be replaced by the message set under "default.welcome" in your i18n files */}
<FormattedMessage id="default.welcome" />
</div>
);
}
export default MyComponent;
If you need to access messages, you can use the hook version. This is particularly useful when you need to inject translated messages into component props.
import React, { FunctionComponent } from 'react';
import { useIntl, FormattedMessage } from 'react-intl';
const MyComponent: FunctionComponent = () => {
// Get react-intl service
const intl = useIntl();
// Get the formatted button title
const translatedTitle = intl.formatMessage({ id: 'default.button-title' })
// Inject your translations
return (
<div>
<button title={translatedTitle}>
<FormattedMessage id="default.welcome" />
</button>
</div>
);
}
export default MyComponent;
The props of the hook and component versions are the same in the end. Going forward I will use the component version because it is simpler to format. But remember you can always use the hook version if you need.
Message Formatting
Now let's see how to make your messages dynamic. The react-intl library is ICU compliant and has therefore a wide range of dynamic formatting directives.
Formatting always has the following structure in translation keys:
"My cat has {value, type, format} legs"
E.g.
"Almost {pctBlack, number, ::percent} of them are black."
"Coupon expires at {expires, time, short}"
"{gender, select, male {He} female {She} other {They}} will respond shortly."
"You have {itemCount, plural, =0 {no items} one {1 item} other {{itemCount} items}}.
Now this is what these examples look like in React:
import React, { FunctionComponent } from 'react';
const MyComponent: FunctionComponent = () => {
return (
<div>
{/* Injecting variables */}
{/* mymessage: "I have {catCount} cats and {dogCount} dogs" */}
<FormattedMessage id="mymessage" values={{ catCount: 3, dogCount: 2 }} />
{/* Percent formatting */}
{/* mymessage: "Almost {pctBlack, number, ::percent} of them are black." */}
<FormattedMessage id="mymessage" values={{ pctBlack: 0.2 }} />
{/* Date formatting */}
{/* mymessage: "Coupon expires at {expires, time, short}" */}
<FormattedMessage id="mymessage" values={{ expires: new Date() }} />
{/* Select from enum */}
{/* mymessage: "{gender, select, male {He} female {She} other {They}} will respond shortly." */}
<FormattedMessage id="mymessage" values={{ gender: 'male' }} />
{/* Pluralization */}
{/* mymessage: "You have {itemCount, plural, =0 {no items} one {1 item} other {{itemCount} items}}. */}
<FormattedMessage id="mymessage" values={{ itemCount: 3 }} />
</div>
);
}
export default MyComponent;
You can read more about message syntax on in the FormatJS documentation.
Component injection
You can extend the react-intl markup with custom components. Custom components can be injected in the form of variables or tags.
Here is a concrete example of injecting a break variable and a link tag.
import React, { FunctionComponent } from 'react';
const MyComponent: FunctionComponent = () => {
return (
<div>
{/* Inject a double break and a link to Google */}
{/* mymessage: "Want to search something?{break2}Go to <link-to-google>Google</link-to-google>" */}
<FormattedMessage
id="mymessage"
values={{
break2: (
<Fragment>
<br />
<br />
</Fragment>
),
'link-to-google': (...chunks: ReactNodeArray) => (
<a href="https://www.google.com">{chunks}</a>
)
}}
/>
</div>
);
}
export default MyComponent;
From there you can inject any custom component into your translation files!
Convenient right? We can do more :)
RichMessage: your custom version of react-intl
As you can see above, it's fairly straightforward to inject custom context into react-intl translation keys.
So what about creating a wrapper around react-intl to make common configuration parameters available in your translation keys without having to explicitly pass values every time. Nothing easier!
Let's start by creating a custom component. This component will inject a list of constant variables into react-intl plus any user-defined variable.
// src/components/RichMessage/RichMessage.tsx
import React, { FunctionComponent } from 'react';
import { FormattedMessage } from 'react-intl';
import { RICH_TAGS } from './RichMessageConfig';
interface Props {
id: string;
values?: object;
}
// Wrapper component used to inject common HTML tags in translations
// This allows us to define a kind of "meta language" for translation keys
// with commonly used HTML tags (bold, italic, pre etc.)
export const RichMessage: FunctionComponent<Props> = ({ id, values }: Props) => {
return <FormattedMessage id={id} values={{ ...RICH_TAGS, ...values }} />;
};
Let's also create a hook version of this component. Note that we only extend useIntl with a formatRichMessage function, we do not override anything. This means that the native react-intl functions are still available in our hook.
// src/components/RichMessage/useRichIntl.tsx
import { useIntl, IntlShape, MessageDescriptor } from 'react-intl';
import { PrimitiveType, FormatXMLElementFn } from 'intl-messageformat';
import { RICH_TAGS } from './RichMessageConfig';
interface RichIntlShape extends IntlShape {
formatRichMessage(
descriptor: MessageDescriptor,
values?: Record<string, PrimitiveType | React.ReactElement | FormatXMLElementFn>
): string | React.ReactNodeArray;
}
// Wrapper hook that adds a formatRichMessage. See RichMessage for an explanation.
export const useRichIntl = (): RichIntlShape => {
const intl = useIntl();
// Format message with custom HTML tags
const formatRichMessage = (
descriptor: MessageDescriptor,
values?: Record<string, PrimitiveType | React.ReactElement | FormatXMLElementFn>
): string | React.ReactNodeArray => {
return intl.formatMessage(descriptor, { ...RICH_TAGS, ...values });
};
return { ...intl, formatRichMessage };
};
Now let's define that missing RICH_TAGS
constant. This constant defines all the variables and tags available by default in our translation keys. You will notice that we even created a reusable component for external links.
// src/components/RichMessage/RichMessageConfig.tsx
import React, { ReactNodeArray, Fragment, FunctionComponent } from 'react';
interface ExternalLinkProps {
href: string;
children: ReactNodeArray;
}
const ExternalLink: FunctionComponent<ExternalLinkProps> = ({ href, children }: ExternalLinkProps) => {
return (
<a href={href} className="hover-underline text-primary" target="_blank" rel="noopener noreferrer">
{children}
</a>
);
};
// Helper method used to generate the link tag function
const externalLinkTag = (href: string): (() => JSX.Element) => {
return (...chunks: ReactNodeArray): JSX.Element => {
return <ExternalLink href={href}>{chunks}</ExternalLink>;
};
};
export const RICH_TAGS = {
freeTrialDurationDays: 14,
teamPlanCostUsd: 4.49,
break: <br />,
break2: (
<Fragment>
<br />
<br />
</Fragment>
),
b: (...chunks: ReactNodeArray) => chunks.map((e, i) => <b key={i}>{e}</b>),
em: (...chunks: ReactNodeArray) => chunks.map((e, i) => <em key={i}>{e}</em>),
pre: (...chunks: ReactNodeArray) =>
chunks.map((e, i) => (
<pre className="d-inline text-secondary" key={i}>
{e}
</pre>
)),
'text-muted': (...chunks: ReactNodeArray) =>
chunks.map((e, i) => (
<span className="text-muted" key={i}>
{e}
</span>
)),
'text-danger': (...chunks: ReactNodeArray) =>
chunks.map((e, i) => (
<span className="text-danger" key={i}>
{e}
</span>
)),
'link-to-helpcenter-get-started': externalLinkTag(
'https://help.mysite.com/articles/get-started'
),
'link-to-helpcenter-cancel-account': externalLinkTag(
'https://help.mysite.com/articles/cancel-account'
),
'link-to-blog': externalLinkTag(
'https://blog.mysite.com'
)
};
Finally, let's create a module index to expose our newly created components:
// src/components/RichMessage/index.ts
export * from './RichMessage';
export * from './useRichIntl';
That's it! The constants and tags defined in RICH_TAGS
will now always be available in our translation context. We just need to use our new component and hook to benefit from them.
This is the component approach:
import React, { FunctionComponent } from 'react';
import { RichMessage } from 'components/RichMessage';
const MyComponent: FunctionComponent = () => {
return (
<div>
{/* mymessage: "If you need help getting started, read this <link-to-helpcenter-get-started>article</link-to-helpcenter-get-started>." */}
<RichMessage id="mymessage" />
</div>
);
}
export default MyComponent;
This is the hook approach:
import React, { FunctionComponent } from 'react';
import { useRichIntl, RichMessage } from 'components/RichMessage';
const MyComponent: FunctionComponent = () => {
// Get our custom react-intl service
const intl = useRichIntl();
// Get the formatted button title
// Note that we use the formatRichMessage function this time
// mymessage: "Remember you benefit from a {freeTrialDurationDays} day free trial"
const translatedTitle = intl.formatRichMessage({ id: 'mymessage' })
// Inject your translations
return (
<div>
<button title={translatedTitle}>
<RichMessage id="default.welcome" />
</button>
</div>
);
}
export default MyComponent;
Easy!
Wrapping up
The react-intl library provides a lot of out-of-the-box functionalities. We strongly recommend reading the FormatJS documentation to get a good grasp on the formatting options it provides.
Once you are comfortable with it, we highly recommend you create a RichMessage/useRichIntl wrapper to expose your constants and extend the FormatJS markup with your own.
Having a custom wrapper will make your translation keys easier to read and your React code simpler by not having to pass important constants as values every time.
Top comments (0)