# I18n best practices * [Introduction](#introduction) * [TL;DR](#tldr) * [Details](#details) * [Dates and times](#dates-and-times) * [FormattedMessage component (renderProps usage)](#formattedmessage-component-renderprops-usage) * [IntlConsumer component](#intlconsumer-component) * [intl object](#intl-object) * [Don't split sentences](#dont-split-sentences) * [Use placeholders for dynamic content](#use-placeholders-for-dynamic-content) * [Avoid substituting nouns and verbs for placeholders](#avoid-substituting-nouns-and-verbs-for-placeholders) ## Introduction FOLIO's i18n implementation is based on components provided by [react-intl](https://formatjs.io/docs/react-intl). The [Basic Internationalization Principles](https://formatjs.io/docs/basic-internationalization-principles) and [React Internationalization – How To](https://www.smashingmagazine.com/2017/01/internationalizing-react-apps/) guides mentioned there are worthwhile reads to get the lay of the land. Note that in addition to strings, dates and times also have locale-specific formats, e.g. dates in the US may be expressed as MM/DD/YYYY while in Europe they will be expressed as DD/MM/YYYY. We store locale data in each app's `translations/` directory, e.g. `translations/ui-users/en.json` and use the components `` and ``, and the method `intl.formatMessage`, to replace placeholder strings in the code with values loaded from those files. We use the components `` and `` and the methods `intl.formatDate` and `intl.formatTime` to render dates and times. FOLIO front-end modules should stick to exchanging date and time information with the back end in UTC. Our approach to providing rich-text markup, e.g. `The value {value} was removed.`, is to use HTML directly in the translation files and to use the `` component to display it. (HTML markup in values passed through `` will be escaped, e.g. `` will be converted to `&%lt;strong>&` whereas `` allows HTML markup to stand but sanitizes it to remove dangerous code without escaping known-good values.) The FOLIO Project uses [lokalise.com](https://lokalise.com/) to manage the process of providing language-specific strings in FOLIO modules. Developers add keys and English translation strings to the `translations//en.json` file. New strings are automatically added to the Lokalise database of strings via a GitHub repository hook. A batch job creates pull requests for new and updated localization files in GitHub; these pull requests are automatically merged if the Jenkins tests pass. (This batch job is not automated at this time; contact Peter Murray ([FOLIO Slack](slack://user?team=T1EPYQDAQ&id=U1T4P0GPQ), [email](mailto:peter@indexdata.com?subject=Lokalise+request)) to request a Lokalise-to-GitHub merge for a particular git repository.) Developers *_MUST NOT_* edit locale files directly (other than `en.json`); such changes will be automatically and silently overwritten by the Lokalise batch job. The strings stored in `en.json` are not directly used in the front end; instead, for English locales, the file with the appropriate subtag is used (`en_US` for _"English -- United States"_ and `en_GB` for _"English -- United Kingdom"_). The Lokalise tool automatically copies locale string values from `en.json` to `en_US.json` and `en_GB.json`. Translators can adjust the string values in `en_US.json` and `en_GB.json` as needed (for instance, turning _"color"_ into _"colour"_). ## TL;DR `stripes-core` sets up a `react-intl` `` available by consuming its `` context. FOLIO front-end modules should stick to exchanging date and time information with the back end in UTC. To format a string as a `React.Node` element, use ``` import { FormattedMessage } from 'react-intl'; ... const message = ; ``` If you need an actual string rather than a React.Node, as for an `aria-label` attribute, use the [useIntl hook](https://formatjs.io/docs/react-intl/api/#useintl-hook), the [injectIntl HOC](https://formatjs.io/docs/react-intl/api#injection-api) or `stripes-core`'s ``: ``` import { IntlConsumer } from 'stripes-core'; ... {intl => ( )} ``` In a functional component, you may retrieve the `intl` object via the [`useIntl` Hook](https://formatjs.io/docs/react-intl/api#injection-api). For HTML template strings, e.g. `The value {value} was removed.`, use ``` import SafeHTMLMessage from '@folio/react-intl-safe-html'; ... const message = ; ``` For date and time values formatted as `React.Node` elements, use ``` import { FormattedDate } from 'react-intl'; ... const message = ``` or, if you need an actual string, consume `` as above, then: `intl.formatDate(loan.dueDate)` and/or `intl.formatTime(loan.dueDate)`. ## Details Keys in libraries have the name of the library automatically prefixed, e.g. a key named `search` in `stripes-components/translations/stripes-components/en.json` would be accessible as `stripes-components.search`. Keys in apps have the name of the app automatically prefixed, e.g. a key named `search` in `ui-users/translations/ui-users/en.json` would be accessible as `ui-users.search`. `react-intl` uses [ICU Message Syntax](https://formatjs.io/docs/icu-syntax) to handle variable substitution in the values. In its simplest form, the argument is assumed to be a string and the placeholder in the value is replaced with the argument, e.g. given `{ name: "Natasha" }`, the value ``` "Please, {name}. This is kiddie show." ``` would be returned as ``` Please, Natasha. This is kiddie show. ``` Formatted values are given as `{key, type [, format]}`, e.g. ``` "{count, number} {count, plural, one {Record found} other {Records found}}", ``` Here, the same argument `count` is formatted in two different ways; once as the type "number" and once as the type "plural". A ["number"](https://formatjs.io/docs/icu-syntax#number-type) without a formatter is handled the same way as a string; the value is simply replaced by the argument. A ["plural"](https://formatjs.io/docs/icu-syntax#plural-format) works similar to a switch statement operating on the argument, which is interpreted as a number whose values are matched against the keys in the third argument. For example, `formatMessage({ id: 'ui-users.resultCount' }, { count: 1 })` would return "1 Record found" whereas `formatMessage({ id: 'ui-users.resultCount' }, { count: 99 })` would return "99 Records found". ### Dates and times For date and time values, use `import { FormattedDate } from 'react-intl'; ... const message = ` or `react-intl`'s methods': `this.props.intl.formatDate(loan.dueDate)` and/or `this.props.intl.formatTime(loan.dueDate)`. When comparing or manipulating dates, it is safest to operate in UTC mode and leave display formatting to internationalization helpers. If using moment, this can be done via [`moment.utc()`](http://momentjs.com/docs/#/parsing/utc/). This is because `moment()` assumes any value without timezone information to be in the local timezone, and converting it to UTC for displaying, it will be affected by the time offset. For example, `12/01` when given to moment and formatted to UTC for display will appear as `11/30` in timezones east of UTC. ### FormattedMessage component (renderProps usage) `` defaults to wrapping the translated messages in a `` - if this is undesired, it's possible to use the component in a way that only provides the string without any elemental wrapper. E.g. ``` { label => ( )} ``` ### IntlConsumer component For cases where multiple translated strings are necessary, `stripes-core` provides an `` component for accessing methods found on the `intl` object from `react-intl`. This gives module developers an additional declarative way to add translations to their JSX. It allows access to the `intl.formatMessage` method without having to wrap their component in an HOC. ``` import { IntlConsumer } from '@folio/stripes/core'; //... later in JSX { intl => ( )} ``` ### intl object The `` is at the root level of the `stripes-core` UI, so all child components can use `react-intl`'s components, `injectIntl`, or `useIntl`. ``` import { injectIntl } from 'react-intl'; class MyComponent extends React.Component { render() { const msg = this.props.intl.formatMessage({ id: 'hello.world' }); return (
{msg}
); } } MyComponent.propTypes = { intl: PropTypes.object.isRequired }; export default injectIntl(MyComponent); ``` ## Don't split sentences **DO NOT** split sentences into multiple translation chunks that you concatenate together: ``` The URL of this feed is invalid. cannot be reached. cannot be parsed. ``` Instead of taking the first string and appending one of the last three strings, simply write out all the sentences as below: ``` The URL of this feed is invalid. The URL of this feed cannot be reached. The URL of this feed cannot be parsed. ``` Note how in Gaelic, the structure of the sentences isn't even close to similar. Writing out all sentences works, but trying to concatenate chunks would not have. ``` Tha URL an inbhir seo mì-dhligheach. Cha ruig sinn URL an inbhir seo. Cha ghabh URL an inbhir seo a pharsadh. ``` Plenty of other languages run into similar issues. So use complete sentences as shown in the correct example above. However, you may generate the translation keys to save space in the .js file: ``` ``` Note that the above will make it difficult to do project-wide searches for translation keys, so be aware of that for your maintenance costs. Additionally, try not to use this pattern if you are consuming translation keys from Stripes (e.g., `stripes-core.button.new`) because it will make Stripes development difficult when trying to discover what translations are and aren't being used. ## Use placeholders for dynamic content Don't concatenate dynamic content and a translated string. Wrong: ``` ${id} ``` ``` "idWasFound": "was found" ``` Instead embed placeholders into the string using the ICU syntax explained above. For some languages translators need to reorder words and placeholders and can only do this with placeholders. Correct: ``` ``` ``` "idWasFound": "{value} was found" ``` A possible German translation is ``` "idWasFound": "Es wurde {value} gefunden" ``` ## Avoid substituting nouns and verbs for placeholders In the above section, placeholders were used for identifiers. They can also safely be used for things like names, or titles, etc. However, avoid placeholders if the placeholder is known at compile time and especially if the placeholder can be different nouns or different verbs. **DO NOT** use placeholders like these: ``` "typeWasFound": "{type} was found", "deleteType": "Delete {type}", "instance": "Instance", "holding": "Holding", "item": "Item", ``` This is because other languages will completely change the sentences based on different nouns such as instance/holding/item. A very common example of these changes would be gendered nouns, where some languages would have `instance` be masculine and `holding` be feminine. [Grammatical cases](https://en.wikipedia.org/wiki/Grammatical_case) are different across languages as well, which also hinders the ability to use placeholders. A better option would be: ``` "wasFound.instance": "Instance was found", "wasFound.holding": "Holding was found", "wasFound.item": "Item was found", "delete.instance": "Delete instance", "delete.holding": "Delete holding", "delete.item": "Delete item", ``` and this lookup in your code: ``` ``` ``` ``` If there are both pre-defined and custom types handle them separately: ``` "wasFound.instance": "Instance was found", "wasFound.holding": "Holding was found", "wasFound.item": "Item was found", "wasFound.customType": "{type} was found", ``` A possible German translation, note the changing article Der and Den before Bestand: ``` "wasFound.instance": "Die Instance wurde gefunden", "wasFound.holding": "Der Bestand wurde gefunden", "wasFound.item": "Das Exemplar wurde gefunden", "wasFound.customType": "{type} wurde gefunden", "delete.instance": "Die Instance löschen", "delete.holding": "Den Bestand löschen", "delete.item": "Das Exemplar löschen", "delete.customType": "{type} löschen", ```