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.
Jutro does not actually read the HTTP 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.
The i18n
script
In your app's package.json
file, there's the i18n
script. This script executes jutro generate:i18n
. By default, this does the following:
- Extracts properly marked-up text from files matching these patterns:
src/**/*.metadata.json5, src/**/*.{ts,tsx,js,jsx}
- The text is extracted to the
./i18n/src/
temporary directory
- The text is extracted to the
- Merges all the UI text from the various files in
./i18n/src
to./src/i18n/lang.json
- The
src/i18n/lang.json
file is pseudo-translated tosrc/i18n/yy.json
. This is discussed in more detail below
Options on the generate:i18n
command
--pseudoInputFile <pseudoInputFile>
. This defines a custom input file to pseudolize. The default value issrc/i18n/lang.json
.--jsxFileNamePattern <jsxFileNamePattern>
. This defines the file name pattern of your JSX files. The default value issrc/**/*.{ts,tsx,js,jsx}
.--metadataFileNamePattern <metadataFileNamePattern>
. This defines the file name pattern for your metadata JSON files. The default value issrc/**/*.metadata.json5
.--translationOutputDir <translationOutputDir>
. This defines where the output messages containing both default translations and extracted translations are stored. The default value issrc/i18n
.--pseudoType <pseudoType>
. This defines the pseudotype to use. Can be one of ['expansion', 'sherlock', 'both']. The default value is 'sherlock'.--fileNameIgnorePattern <fileNameIgnorePattern>
. This is the comma-separated list of filename patterns to ignore. The default value is**/__mocks__/**,**/__local_mocks__/**,**/basic.metadata.schema.json,**/dist/**,**/node_modules/**,**/generated/**
.--jsxMessageOutputPath <jsxMessageOutputPath>
. This defines the output directory for the extracted jsx messages. The default value isi18n/src/js-jsx-strings.json
.--mergeBizCompOutputDir <mergeBizCompOutputDir>
. This defines the output filename of merged translations. The default value isi18n/biz-comp-translations
.--messagesFileNamePattern <messagesFileNamePattern>
. This defines where the extracted messages have been stored. The default value isi18n/src/**/*.json
.--metadataMessageOutputDir <metadataMessageOutputDir>
. This defines the output directory for the extracted metadata messages. The default value isi18n
.--pseudoOutputFileName <pseudoOutputFileName>
. This defines the output file name template. The default value isyy.json
.--translationFileName <translationFileName>
. This defines the output filename of merged translations. The default value isen
.
Make sure you extract all strings
Developers often leave strings hardcoded/embedded in applications - rendering translation impossible. With that in mind, we have the Sherlock language.
"Sherlock": the / mock / fake / pseudo language
What is it
It is an auto-generated language that converts strings from say, Codeless Form
to [2zqq25_Codeless Form]
. This is included with the default sample app configuration.
Our default configuration has yy
in availableLanguages
. This causes "Sherlock" to show up in the language menu of the GlobalizationChooser
component. Strictly speaking, in the real world, yy
is not a valid language code. However, our software maps this language code to our own "Sherlock" mock language. It should be removed from production configurations.
What value does it provide
- Any string that appears without the enclosing square brackets, and the pre-pended hash - then it has not been properly extracted. It means there's a bug in your code. Obviously, customer data strings will appear normally
- The hash itself: while it may seem random, it is unique to each key/value pair.
UI strings & live preview
When you start your app using npm start
, it enters into "live preview" mode. That means changes you make to your code are immediately reflected in your browser.
Unfortunately, changes to existing source strings do not update in the live preview. One must re-run the extract-merge-pseudo cycle with npm run i18n
. Rebuilding the app will also call this script.
Translate your application
The default source language is English, with the language code en
.
By default, as per the sample app, these strings will be in the ./src/i18n/lang.json
file. This file is added to .gitignore
, translators should instead copy the file and translate it in other languages, e.g. en.json
, de.json
...
Also remember, that only npm run i18n
(or build
) can guarantee that all strings will be extracted from code, and merged to this file.
Your translation files should be added to the same directory as the lang.json
file.
UI text from the Jutro framework
Like any UI framework, Jutro has some UI text - that is separate from your app. Your app will likely expose at least some of this text. This text covers strings like "Ok", "Next", "Cancel", and "Submit". Components that expose such text often allow you to overwrite the default text. However, some do not - such as the list of countries to choose from in the IntlPhoneNumberField
component.
As well as the base English (US), the framework text is translated into the following languages: Danish, German, Spanish (Spain), Spanish (US), French, Italian, Japanese, Dutch, Norwegian, Portuguese, Russian, and Simplified Chinese.
This coverage may not be sufficient for you.
Overriding framework strings or providing your own translations
Suppose you encounter one of the following scenarios:
- You want to change the default text used by our components
- You want to change some of the translations we supply
- You want to translate the framework beyond the languages we support
Here's what you do:
- Somewhere under
./src
, create aframework.messages.js
file, similar to what is described here - Copy the list of source framework strings from
node_modules/@jutro/translations/lang-data/en.json
- Into the
framework.messages.js
created above, add all (or just some) of those framework strings - using theid
anddefaultMessage
pair shape. If you want to change thedefaultMessage
for a particular string, do it here - Upon rebuilding, you'll see that those strings have now been added to the
./src/i18n/lang.json
file - You can now translate the framework strings as you would your app strings
Locale sensitive components
Elements like the display, formatting of dates, times, numbers, calendars, monetary amounts, and percentages vary by locale. Jutro offers several components that honor the user's locale.
Dates, times, calendars
Jutro offers a variety of date, time, and calendaring options.
Components
All are available from @jutro/components
.
DateField
: Renders a date-picker input element. Has a read-only option also offers full date and date-time picking/input support, as well as a read-only mode.
FormattedDate
: Renders a formatted date inline (no <div>
). Supports a number of predefined formats:
Locale | short | long | abbreviated | full |
---|---|---|---|---|
en-US | Aug 30, 2018 | August 30, 2018 | Thu, Aug 30, 2018 | Thursday, August 30, 2018 |
fr-FR | 30 août 2018 | 30 août 2018 | jeu. 30 août 2018 | jeudi 30 août 2018 |
pl-PL | 30 sie 2018 | 30 sierpnia 2018 | czw., 30 sierpnia 2018 | czwartek, 30 sierpnia 2018 |
DateValue
: Renders a formatted date using the tag
property to wrap the value.
FormattedDateRange
: similar to FormattedDate
, just for a date range, rather than a single date.
All are presented in a locale sensitive way.
APIs
Available from @jutro/components
formatDate
and formatDateRange
: Imperative APIs for formatting dates & date-times, using current locale.
Numbers (not monetary amounts)
Components
Available from @jutro/components
InputNumberField
: Renders an input element for number fields. Has a read-only option also.
FormattedNumber
: Offers read-only, inline support.
NumberValue
: Renders a formatted currency using the tag
property to wrap the value.
Locale | Formatted Output |
---|---|
en-US | 23,234.34 |
fr-FR | 23 234,34 |
de-DE | 23.234,34 |
APIs
Available from @jutro/components
formatNumber
: Imperative API for formatting numbers using current locale.
Monetary amounts
Components
Available from @jutro/components
CurrencyInput
: Use it to enter or display a monetary value. The formatting is based on the locale. Supports input, and read-only modes.
FormattedCurrency
: Displays a formatted currency value inline. (No <div>
).
CurrencyValue
: Renders a formatted currency using the tag
property to wrap the value.
The above components allow you to set the currencyDisplay
property. For an amount in US dollars, in the en-US
locale, setting currencyDisplay
to code
will give you USD
; while setting it to symbol
will give you $
.
The currency itself (for example USD or EUR) is provided as a property on the component - and is totally unrelated to the user's locale and country.
The locale determines how the currency amount is formatted, but does not determine the currency itself.
Locale | Formatted Output (code ) | Formatted Output (symbol ) |
---|---|---|
en-US | USD 23,234.34 | $23,234.34 |
fr-FR | 23 234,34 USD | 23 234,34 $US |
de-DE | 23.234,34 USD | 23.234,34 $ |
APIs
Available from @jutro/components
formatCurrency
: Imperative API for formatting currency values using current locale
defaultCurrency
for currency related components
There is no global defaultCurrency
setting for currency related components.
This means that you need to pass the currency or the default currency to be used to the currency related components CurrencyInput
, FormattedCurrency
and CurrencyValue
:
<CurrencyInput
availableCurrencies={['USD']}
label="Currency input component"
/>
In the case of CurrencyValue
, if defaultCurrency
is not set, the component defaults to US dollars or USD. You can also read this value by calling the getDefaultCurrency
function from Jutro's locale package in order to retrieve and then pass it to the corresponding component.
import { getDefaultCurrency } from '@jutro/locale';
Sorting / collation
Text collation (sorting) is language specific. Every language has its own collation rules.
Timezone
By default, all dates with time are stored in UTC and converted to the user's local timezone when displaying. You can override this by setting defaultTimeZone
in localeSettings
in src/config/config.json
.
"localeSettings": {
...
"defaultTimeZone": "Atlantic/Faroe"
}
When defaultTimeZone
is set, all dates with time are converted to the defaultTimeZone
timezone when displaying (but still stored as UTC date). The only exception is the DateTimeZoneField
component where the user can select a timezone from the list. In this case defaultTimeZone
is used as the default timezone.
The value of defaultTimeZone
must be a valid IANA timezone.
Plural nouns in UI text
Plural nouns are a tricky linguistic problem that Jutro solves via its use of the react-intl
library.
Take this pseudo code:
if (numPolicies === 1) print('You have 1 policy with us');
else print('You have {numPolicies} policies with us');
The above code works for English - but the logic fails for many other languages.
Many languages have much more complex rules around plural nouns. Even French has different rules to English. (French uses the singular form when the quantity is 1 or 0).
Some languages have three (Polish), four (Russian), or even six (Arabic) plural forms - all dependent on the value of the integer, which can only be known at runtime. This page captures the math logic around plurals for many languages.
Pluralization support in Jutro
Please refer to the FormatJS documentation, and the ICU MessageFormat syntax guide.
Note:
- The plural syntax is not entirely intuitive - for either developers or translators
- If translating, ensure that your translation tooling and translation vendors understand and support this syntax
Was this page helpful?