Internationalization
Overview
All applications must be internationalized, while many will also be localized (which for our purposes, is essentially translation). If internationalization is not done correctly, then full localization is difficult, if not impossible.
Jutro gives you good internationalization and localization support but you manage the implementation.
We treat language, locale, country, and currency as entirely independent variables:
- Language: This covers regular UI strings
- Applications can configure an array of
availableLanguages
, and apreferredLanguage
(default)
- Applications can configure an array of
- Locale: Also know as regional format, governs how dates, times, numbers, and calendars are formatted. Note that any regular UI string (governed by language), that includes a date, time, or number variable - that variable should be formatted according to locale
- Applications can configure an array of
availableLocales
and apreferredLocale
(default)
- Applications can configure an array of
- Country: This governs how phone number or address components behave
- Applications can set a
defaultCountryCode
, that unless overwritten at the component level, will be used by components likeIntlPhoneNumberField
andAddress
to set the default country
- Applications can set a
- Currency: Currency should not be inferred from a user's language, country, or locale
- Currency is simply a property of a transaction
- Applications can configure a
defaultCurrency
, that unless overwritten at the component level, will be used to configure components likeCurrencyInput
Before you start
The Jutro sample app gives you the following:
- All UI strings are marked up such that they're extractable to the
lang.json
file and because of this are translatable - The build process automatically creates our pseudo language, known as Sherlock
- All locale sensitive components like date pickers and currency input automatically behave correctly
Internationalization (i18n) support in Jutro
react-intl
- a library that provides React components likeFormattedDate
andFormattedNumber
, as well as APIs to format dates, numbers, and strings and also handle translations. Some of our components likeCurrencyInput
, wrap components supplied byreact-intl
.jutro
- This command line utility provides one subcommand that simplifies the handling of translatable text. For more details, see the relevant sections on Jutro Platform CLI commands.jutro-locale
- The Jutro package that contains, among other things:GlobalizationProvider
- Sets up an app's internationalization context throughreact-intl
'sIntlProvider
component- Globalization stores based on zustand which you can use to interact with the user's locale preferences and changes to language, locale, currency, and country
jutro-components
- Contains a variety of locale aware components, such as the following mix of input and value display fields:CurrencyInput
,CurrencyValue
SimpleDate
,DateRange
,DateTime
,DateTimeZone
InputNumber
,NumberValue
IntlPhoneNumberField
,PhoneNumberValue
GlobalizationChooser
- Widget allowing you to selectlocale
andlanguage
independently of each otherLanguageSelector
- Similar toGlobalizationChooser
, but only presents a language picker
Your folder structure
i18n/src/*
- Auto-generated temporary files that simply hold the UI strings that have been extracted fromsrc/**
. Do not manually edit them, or check them into source control..gitignore
is already configured to ignore these filessrc/i18n/lang.json
- The generated translation file (can be changed inpackage.json
by modifyingjutro
command options). Similarly, do not manually edit this file as it is added to.gitignore
. Translators will use this file as a basis for further translations. They will add other language files in the same folder, for example,ru.json
,fr.json
String shape
Jutro provides an intlMessageShape
type, which has the following properties:
id
: Must be unique in the applicationdefaultMessage
: The actual UI text. It is also the fallback string if no translations are presentdescription
(optional): Helps the translator understand the context in which the string appearsargs
(optional): Allows the passing of argument values. See Variable Interpolation
Strings must use this shape - otherwise, it is not possible to extract them for translation.
See also: intlMessageShape
Internationalizing your app
react-intl
provides most of the internationalization support to Jutro apps.
Note: If you use the sample app shipped with Jutro - then you'll get most/all of the below for free.
Add IntlProvider
(via GlobalizationProvider
)
If you are not using the Jutro start
function, you can add Jutro globalization features to your app using GlobalizationProvider
.
At the root of your app, you need to add Jutro's GlobalizationProvider
component from jutro-locale
. This provides an internationalization context for everything it encloses. It uses react-intl
's IntlProvider
component.
IntlProvider
sets two key properties:
locale
: Which impacts the behavior of all of locale sensitive components such as those used for date, calendar, display, and time and number inputmessages
: The array of translations to be used by the enclosed components
Note that locale
and messages
are independent of each other. This means that you could set locale
to say fr-FR
, but the messages
array could contain say German translations.
Configure localeSettings
You can set localeSettings
in src/config/config.json
. For example:
{
"localeSettings": {
"availableLocales": ["en-US", "es-ES", "es-MX", "de-DE", "pl"],
"availableLanguages": ["en", "es", "de", "pl", "yy"],
"preferredLocale": "en-US",
"preferredLanguage": "en",
"defaultCountryCode": "US",
"defaultCurrency": "USD"
}
}
If localeSettings
, or any part thereof, is omitted from your config, then the the app reverts to these defaults:
{
"localeSettings": {
"availableLocales": ["en-US"],
"availableLanguages": ["en"],
"preferredLocale": "en-US",
"preferredLanguage": "en",
"defaultCountryCode": "US",
"defaultCurrency": "USD"
}
}
Provide users a way to select language and locale
You have a few options around letting users select language and locale, as well as how you store those selections.
GlobalizationChooser
component
GlobalizationChooser
allows the user to select language and locale separately. This component accepts the following props:
className
- Additional class names for the component (PropTypes.string)containerStyle
- Additional class names for component container (PropTypes.string)localeId
- ID of the locale select element (PropTypes.string)languageId
- ID of the language select element (PropTypes.string)localeValue
- Selected locale (PropTypes.string)languageValue
- Selected language (PropTypes.string)languageLabelText
- Message key for the language label (intlMessageShape)localeLabelText
- Message key for the locale label (intlMessageShape)availableLanguageValues
- Languages available for selection (PropTypes.arrayOf(PropTypes.string))availableLocaleValues
- Locales available for selection (PropTypes.arrayOf(PropTypes.string))onLocaleValueChange
- Callback invoked on locale change (PropTypes.func)onLanguageValueChange
- Callback invoked on language change (PropTypes.func)renderLocaleLabel
- Render prop to display locale in options (PropTypes.func)renderLanguageLabel
- Render prop to display language in options (PropTypes.func)showLocaleLabel
- Flag for showing or hiding the locale label (PropTypes.bool)showLanguageLabel
- Flag for showing or hiding the language label (PropTypes.bool)showLocaleSelect
- Flag for showing/hiding the locale select (PropTypes.bool)showLanguageSelect
- Flag for showing/hiding the language select (PropTypes.bool)readOnly
- If set to true, the drop-down lists are readonly, ignored in storybook mode (PropTypes.bool)skipPropagation
- If set to true, the config is not updated on value change and GlobalizationChooser becomes a controlled component (PropTypes.bool)
LanguageSelector
component
This component presents a language selection menu only. The selected language value will also be used to set the locale value. The selection is not persisted in localStorage
.
The @jutro/router
dependency and its peer dependencies like
history
and react-router-dom
are now optional for
component packages. However, the LanguageSelector
component requires
the dependency to be added to your app dependencies in order to be used.
How Jutro determines which language and locale to use
When a user loads a Jutro app, the following will happen:
- The application reads the user's browser language preference (
navigator.language
) - The application's language defaults to the user's browser preference, if that preference is also in
availableLanguages
, otherwise the app defaults topreferredLanguage
- The application's locale will also default to the user's browser language preference if that preference is also in
availableLocales
, otherwise it defaults topreferredLocale
If the createG11nLocalStorageStore
function is used, the user's preferences are also saved in localStorage
and is remembered across sessions.
accept-language
header. Instead, the JS on the client side reads the navigator.language
property.While accept-language
can contain an ordered list of preferences, navigator.language
only has a single value. The first value in accept-language
will be the same as the single value in navigator.language
.
Interacting with the g11n store
The globalization (g11n) store is enabled in the src/startApp.js
file. By default, it is initialized using locale settings from src/config/config.json
. See the section about configuration for more details.
You can use one of the following functions to initialize the store:
createG11nMemoryStore
- Manages locale settings in memory. This store is the default setting. It means changes made to the store are reset when the page is refreshed and go back to your configuration.createG11nLocalStorageStore
- Manages locale settings in local storage, so that the user's preferences are remembered across sessions. See the section about persisting user choice for more details.
When use one of these functions, you can override the default settings by passing in an object with the following properties:
const g11nStore = createG11nMemoryStore({
country: 'PL',
currency: 'PLN',
language: 'pl',
locale: 'pl-PL',
locales: ['en-GB', 'pl-PL'],
timeZone: 'Europe/Warsaw',
languages: ['en', 'pl'],
});
Then you need to pass the store to the startApp
function:
startApp({
g11nStore,
// ... other settings
});
Reacting to locale changes
If you want to react to locale and language changes, anywhere in your React app, you can use LocaleContext
and the useLanguage
hook. Here is an example:
import { useContext } from 'react';
import { useLanguage, LocaleContext } from '@jutro/locale';
const MyPanel = () => {
const {
availableLocales,
dateLocale,
locale,
defaultTimeZone,
localeOnChangeCallback,
} = useContext(LocaleContext);
const { availableLanguages, language, languageOnChangeCallback } =
useLanguage();
return <div>Functionality coming soon, I promise!</div>;
};
You can use localeOnChangeCallback
and languageOnChangeCallback
to force a change in the locale or language. For example, you can use them in button click handlers:
return (
<>
<button onClick={() => languageOnChangeCallback('lang')}>
Change language to "lang"
</button>
<button onClick={() => localeOnChangeCallback('loc')}>
Change locale to "loc"
</button>
</>
);
Interacting with the store outside the React tree
If you want to access the store outside the React tree, you can use one of the createG11n*Store
functions in the src/startApp.js
file.
Whenever the user changes any of the locale settings, the store will be updated. You can subscribe to these changes by calling subscribe
and passing a callback function. This callback will be called whenever the locale settings change.
import { createG11nMemoryStore } from '@jutro/locale';
import { loadConfiguration } from '@jutro/config';
import appConfig from './config/config.json';
// load the configuration from the config.json file
// because globalization store gets defaults from this config
loadConfiguration(appConfig);
// create a store that will save changes in memory
const g11nStore = createG11nMemoryStore({});
// subscribe to all changes
g11nStore.subscribe((state) => {
console.log('G11n store changed', state);
});
// subscribe to language changes specifically
g11nStore.subscribe(
(state) => state.language,
(language) => {
console.log('Language changed to: ', language);
window.location.reload();
}
);
export const startApp = () => {
start(Jutro, {
// pass your new local store here
g11nStore,
// ... other options
});
};
Persisting user choice
To persist the user's choice, you can use the createG11nLocalStorageStore
function from jutro-locale
. It automatically updates the locale settings in localStorage
.
- Create a store using
createG11nLocalStorageStore
. This store saves changes in the browser's local store. In the example below, we call this storeg11nStore
. - Pass
g11nStore
to your app's start function.
import { createG11nLocalStorageStore } from '@jutro/locale';
import { loadConfiguration } from '@jutro/config';
import appConfig from './config/config.json';
// load the configuration from the config.json file
// because globalization store gets defaults from this config
loadConfiguration(appConfig);
// create a store that will save changes in the browser's local store
const g11nStore = createG11nLocalStorageStore({
name: 'unique-store-name',
});
export const startApp = () => {
start(Jutro, {
// pass your new local store here
g11nStore,
// ... other options
});
};
Mark-up messages that require translation
This is something developers should be doing all along.
See also:
For components defined in JSX
Import the defineMessages
function from @jutro/locale
to define strings for translation.
Place them in a file that corresponds to your component (for example, MyPage.messages.js
) and export them:
import { defineMessages } from '@jutro/locale';
export default defineMessages({
clickMe: {
id: 'jutro-app.pages.myPage.clickMe',
defaultMessage: 'Click me!',
description: 'Action message',
},
thanks: {
id: 'pages.myPage.thanks',
defaultMessage: 'Thanks for clicking me!',
description: 'Result message that appears when user clicks a button',
},
});
Import the messages into your corresponding .js
file, and reference them:
import { TranslatorContext } from '@jutro/locale';
import messages from './MyPage.messages';
...
const translator = useContext(TranslatorContext);
...
const handleClick = () => {
// eslint-disable-next-line no-alert
alert(translator(messages.thanks));
};
...
<Button onClick={handleClick}>
{translator(messages.clickMe)}
</Button>
While useContext(TranslatorContext)
can still be used, it is preferred to switch to useTranslator()
for your translator. useTranslator
has additional functionality that checks if the context value is valid and informs you if it is not. The corresponding line in your .js
file will look like this:
...
const translator = useTranslator();
...
Why use a separate file for JSX messages?
It is not required, but it is cleaner. (Ideally one would do something similar for JSON5 based strings too. At this time though that is not possible. In metadata / JSON5 files, strings must be declared directly therein.)
When strings are more isolated from the code, it's easier to modify them while lowering the risk of introducing breakage.
Using MessageFormat
syntax for formatted arguments
Note: for more information, see Variable Interpolation.
react-intl
(specifically intl-messageformat
) supports ICU4J's MessageFormat
syntax. Instead of simply embedding a variable / argument in a string, by using the MessageFormat
syntax, you can also declare an argument's type, and formatting style.
Examples:
Your premium is due on {someDate, date}
- Will format
someDate
in a short style (generally not a good idea - the format is ambiguous)
- Will format
Your premium is due on {someDate, date, long}
- Will format
someDate
in thelong
style, like foren-US
:February 17, 2019
- Will format
You've had {numClaims, number} on this policy
- Will format
numClaims
as a number. That is, it will use locale appropriate decimal and grouping separators
- Will format
Your premium will increase by {somePercent, number, percent}
- Will format
somePercent
as a percentage string
- Will format
See https://formatjs.io/docs/core-concepts/icu-syntax/#formatted-argument for full details.
MessageSyntax
and currency arguments
To format currencies using MessageFormat
you use this syntax:
Your next payment is for {someAmount, number, :: currency/EUR}.
The issue here is that your string declares the currency - and so you have to know the currency in advance. This will only really work for those apps that positively only use a single currency
Handling Translatable UI Text
Once UI text has been properly marked up in code, the text must be extracted and merged to a single file. This file can then be translated.