Skip to main content

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 a preferredLanguage (default)
  • 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 a preferredLocale (default)
  • 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 like IntlPhoneNumberField and Address to set the default country
  • 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 like CurrencyInput

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 like FormattedDate and FormattedNumber, as well as APIs to format dates, numbers, and strings and also handle translations. Some of our components like CurrencyInput, wrap components supplied by react-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 through react-intl's IntlProvider 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 select locale and language independently of each other
    • LanguageSelector - Similar to GlobalizationChooser, 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 from src/**. Do not manually edit them, or check them into source control. .gitignore is already configured to ignore these files
  • src/i18n/lang.json - The generated translation file (can be changed in package.json by modifying jutro 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 application
  • defaultMessage: The actual UI text. It is also the fallback string if no translations are present
  • description (optional): Helps the translator understand the context in which the string appears
  • args (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 input
  • messages: 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:

src/config/config.json
{
"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:

src/config/config.json
{
"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.

note

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 to preferredLanguage
  • The application's locale will also default to the user's browser language preference if that preference is also in availableLocales, otherwise it defaults to preferredLocale

If the createG11nLocalStorageStore function is used, the user's preferences are also saved in localStorage and is remembered across sessions.

important

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:

src/startApp.js
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:

src/startApp.js
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.

src/startApp.js
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.

  1. Create a store using createG11nLocalStorageStore. This store saves changes in the browser's local store. In the example below, we call this store g11nStore.
  2. Pass g11nStore to your app's start function.
src/startApp.js
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)
  • Your premium is due on {someDate, date, long}
    • Will format someDate in the long style, like for en-US: February 17, 2019
  • 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
  • Your premium will increase by {somePercent, number, percent}
    • Will format somePercent as a percentage string

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
  • 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 to src/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 is src/i18n/lang.json.
  • --jsxFileNamePattern <jsxFileNamePattern>. This defines the file name pattern of your JSX files. The default value is src/**/*.{ts,tsx,js,jsx}.
  • --metadataFileNamePattern <metadataFileNamePattern>. This defines the file name pattern for your metadata JSON files. The default value is src/**/*.metadata.json5.
  • --translationOutputDir <translationOutputDir>. This defines where the output messages containing both default translations and extracted translations are stored. The default value is src/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 is i18n/src/js-jsx-strings.json.
  • --mergeBizCompOutputDir <mergeBizCompOutputDir>. This defines the output filename of merged translations. The default value is i18n/biz-comp-translations.
  • --messagesFileNamePattern <messagesFileNamePattern>. This defines where the extracted messages have been stored. The default value is i18n/src/**/*.json.
  • --metadataMessageOutputDir <metadataMessageOutputDir>. This defines the output directory for the extracted metadata messages. The default value is i18n.
  • --pseudoOutputFileName <pseudoOutputFileName>. This defines the output file name template. The default value is yy.json.
  • --translationFileName <translationFileName>. This defines the output filename of merged translations. The default value is en.

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:

  1. Somewhere under ./src, create a framework.messages.js file, similar to what is described here
  2. Copy the list of source framework strings from node_modules/@jutro/translations/lang-data/en.json
  3. Into the framework.messages.js created above, add all (or just some) of those framework strings - using the id and defaultMessage pair shape. If you want to change the defaultMessage for a particular string, do it here
  4. Upon rebuilding, you'll see that those strings have now been added to the ./src/i18n/lang.json file
  5. 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:

Localeshortlongabbreviatedfull
en-USAug 30, 2018August 30, 2018Thu, Aug 30, 2018Thursday, August 30, 2018
fr-FR30 août 201830 août 2018jeu. 30 août 2018jeudi 30 août 2018
pl-PL30 sie 201830 sierpnia 2018czw., 30 sierpnia 2018czwartek, 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.

LocaleFormatted Output
en-US23,234.34
fr-FR23 234,34
de-DE23.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.

LocaleFormatted Output (code)Formatted Output (symbol)
en-USUSD 23,234.34$23,234.34
fr-FR23 234,34 USD23 234,34 $US
de-DE23.234,34 USD23.234,34 $

APIs

Available from @jutro/components

formatCurrency: Imperative API for formatting currency values using current locale

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