Handle Internationalization (i18n)
A localized app adapts to a specific culture by displaying text in the correct language and formatting data to expectations. With western languages, it often boils down to providing the app with the translated text and a mechanism to choose the correct translation. Things like currency, measurement units, dates, etc., are formatted on the fly.
The Rhapsody platform, build pipeline, dev toolchain, and CI are being updated to provide a solid foundation for localization. Once the main work here is complete, we will provide guidelines on how to approach internationalization in other places.
Glossaryโ
ICU - International Components for Unicode
Internationalization (i18n) - making software capable of supporting multiple locales
Localization (l10n) - making an internationalized software adapt to a specific locale
Message - a string that supports a special templating syntax for localization
Locale - a string consisting of a set of parameters that represent language, region, and preferences. en-US or en-UK.UTF-8
๐ก Although internationalization (I18n) and localization (l10n) mean different things, they are used interchangeably here.
How does this affect us?โ
The most important thing โ developers are responsible for making user-facing text go through an i18n library.
The library, or rather a set of packages, of our choosing is FormatJS.
We use three packages to cover our needs:
@formatjs/intl- The core FormatJS library that provides functions to take ICU strings and translate them.react-intl- React bindings that make using@formatjs/intlin components easier@rhapsody/localization- Rhapsody platform package to interface with@formatjs/intl
Basic Usageโ
JavaScript / TypeScriptโ
The intl object provides methods such as formatMessage to localize data. Usually, this object is provided by @formatjs/intl, but the @rhapsody/localization platform package provides a pre-configured instance of intl. Always use the intl export from @rhapsody/localization.
import { intl } from '@rhapsody/localization'
const eventsMessage = intl.formatMessage({
defaultMessage: 'Future events to this domain will be recorded',
description:
'Message displayed when the system tells the user that future events to this domain will be recorded',
})
๐ก
intlhas methods to format dates, numbers, lists, and more. Format.JSintldocs.
Reactโ
In addition to the preconfigured intl from @rhapsody/localization, which can be used directly, there are also React bindings.
They can be imported from react-intl package.
Importing intl directly from @rhapsody/localizationโ
import { intl } from '@rhapsody/localization';
const PostDate = ({ date }) => {
return (
<span title={intl.formatDate(date)}>
{intl.formatMessage({ defaultMessage: 'Yesterday', description: 'Date text' })}
</span>
)
});
Using useIntl hook from react-intlโ
intl can be obtained from a hook, given that we have I18nProvider somewhere above the component tree.
import { render } from 'react-dom'
import { I18nProvider } from '@rhapsody/localization'
import { FormattedRelative, useIntl } from 'react-intl';
const PostDate = ({ date }) => {
const intl = useIntl()
return (
<span title={intl.formatDate(date)}>
<FormattedRelativeTime value={date} unit="day" />
</span>
)
});
render(
<I18nProvider>
<Component />
</I18nProvider>,
document.getElementById('#mount')
)
Using FormattedMessage component from react-intlโ
FormattedMessage is another way to handle translations in React. Why components?
import { render } from 'react-dom'
import { FormattedMessage } from 'react-intl'
import { I18nProvider } from '@rhapsody/localization'
function Component() {
return (
<FormattedMessage
defaultMessage="Getting Started with Salesloft"
description="Onboarding title"
/>
)
}
render(
<I18nProvider>
<Component />
</I18nProvider>,
document.getElementById('#mount')
)
NOTE: the above example assumes we have added the I18nProvider component provided by @rhapsody/localization higher up in the React component tree.
๐ก there are more components to format dates, numbers, lists, etc. Format.JS React bindings docs.
Angularโ
Import intl instance normally and use it as in regular JS.
import { intl } from '@rhapsody/localization'
angular.module('companies', ['shared']).config(function ($stateProvider) {
$stateProvider.state('companies', {
abstract: true,
url: '/company',
templateUrl: 'abstract.html',
resolve: {
$title: function () {
return intl.formatMessage({
defaultMessage: 'Accounts',
description: 'Page title of /company',
})
},
},
})
})
โ๏ธ Do not use
intlin Angular templates.
example of code that is not supported
<li ng-click="ctrl.handleBulkEnable(true)">
<a><span>{{ intl.formatMessage({ defaultMessage: 'Enable' }) }}</span></a>
</li>
Advanced Examplesโ
Dynamic Stringsโ
To make localized text dynamic, we must use templating capabilities of ICU messages:
import { intl } from '@rhapsody/localization'
const greeting = intl.formatMessage(
{
defaultMessage: 'Hello, {name}!',
description: 'Greeting to welcome the user to the app',
},
{
name: 'Lisa',
}
)
Rich Text Formattingโ
Pseudo tags can be used in defaultMessage in combination with formatting functions.
import { intl } from '@rhapsody/localization'
const greeting = intl.formatMessage(
{
defaultMessage: 'Hello, <bold>{name}</bold>!',
description: 'Greeting to welcome the user to the app',
},
{
name: 'Eric',
bold: (str) => <b>{str}</b>,
}
)
Conditionally render parts of textโ
In this example, we make use of the select and plural ICU argments.
import React, { useState } from 'react'
import { FormattedMessage } from 'react-intl'
export const HiddenFields = ({ count, isExpanded }) => {
return (
<FormattedMessage
defaultMessage="{isExpanded, select, true {Hide} other {View}} {count} Blank {count, plural, one {Field} other {Fields}}"
values={{ isExpanded: String(isExpanded), count }}
description="A button that opens a list of extra fields"
/>
)
}
select is equivalent to switch in JS. We can make it work with a boolean value by converting the value to a string first. other in select is the same as the default clause in switch, but other must alway be present.
plural is similar to select but lets us match numbers to select a proper plural category. For example, there's one/two in English but one/few/many/other in Ukrainian.
More guides and API reference here https://formatjs.io/
Externalizing Stringsโ
When externalizing strings, the following standards should be adhered to:
- Translations MAY be colocated in the same file in which they are used if there are only a few ("a few" is left to the discretion of the individual developer)
- Translations MAY be separated into a different file from where they are used. In doing so, one must adhere the following conventions:
- Translation files MUST be named
<adjacentfile>.messages.(js|ts)if scoped to a single file - Translation files MUST be named
messages.(js|ts)if scoped to the current directory - Translation files scoped to the current directory (
messages.(js|ts)) MAY include messages for code in child directories - Translation files MUST NEVER be placed in child directories, e.g.
constants,utils,messages - Translation files MUST export a single
messagesnamed export that is an object of translated messages
- Translation files MUST be named
Pseudolocalizationโ
Pseudolocalization helps us see how a product's UI will look after translation, without creating a real translation.
We currently support two types of it:
en-XA: replacing text with accented characters (Cadence -> แธรขแธรจลฤรจ)xx-LS: appending a bunch of "S" characters (Cadence -> CadenceSSSSSSSSSSSSSSSSSSSSSSSSS)
Generating dataโ
Pseudolocalization data is static and must be updated semi-manually after localized code changes. It's not different from the regular translation files, so we store them together in the lang/ directory.
- run
pnpm intland give it a few moments so it can:- scan the codebase
- extract necessary strings
- compile our default (
en-US.json) and fake messages (en-XA.json,xx-LS.json)
- verify changes in
lang/ - check in and commit it with the rest of your code
Switching browser's localeโ
- Open Chrome devtools
- Click "โฎ" menu within devtools โ More tools โ Sensors
- In "Location" select "Other..."
- Type the pseudolocale code (
en-XA,xx-LS)
โ ๏ธ To avoid typing locale every time you can save it as a custom location: "Manage" โ "Add Location..."
โ ๏ธ "Sensors" tab can be moved to the main part of devtools for easier access
Requirementsโ
defaultMessagemust be a static string literal. To make it dynamic we have to use templating capabilities of ICU format.
// โ wrong
const text = intl.formatMessage(`Hello ${user.name}`)
const rendered = <FormmattedMessage defaultMessage={`Hello ${user.name}!`} />
// โ
correct
const text = intl.formatMessage('Hello {name}!', { name: user.name })
const rendered = (
<FormmattedMessage
defaultMessage="Hello {name}!"
values={{ name: user.name }}
/>
)
defaultMessageanddescriptionmust not come from a variable.
// โ wrong - will cause errors
const message = 'My test message'
const invalid = intl.formatMessage({
defaultMessage: message,
description: 'A message for testing localization.',
})
// โ
correct
const valid = intl.formatMessage({
defaultMessage: 'My test message',
description: 'A message for testing localization.',
})
What should we know?โ
Localeโ
A locale is a set of parameters that defines the user's language, region, and any special variant preferences.
In its simplest form it's just a string that includes a language tag (e.g. en, uk, es), often in combination with a region after "-" (e.g. en-US, uk-UA, es-MX).
In browsers the locale is stored in window.navigator.language, which inherits its value from the OS.
https://en.wikipedia.org/wiki/Locale_(computer_software)
https://developer.mozilla.org/en-US/docs/Web/API/Navigator/language
https://formatjs.io/docs/core-concepts/basic-internationalization-principles
ICUโ
The ICU is an open-source project for Unicode support and software internationalization. Initially written for C/C++ and Java in 1999 it has since been widely spread across many languages and platforms.
While we don't need to understand the full scope of the ICU libraries, we should be comfortable when working with ICU messages and know their formatting capabilities.
ICU Messages are user-visible strings, often with variable elements like names, numbers, and dates. Message strings are typically translated into the different languages of a UI, and translators move around the variable elements according to the grammar of the target language. (ICU documentation)
This is the reason why text is often referred to as "message" in internationalization libraries (e.g. defaultMessage).
Further reading: