Skip to content

Latest commit

 

History

History
288 lines (219 loc) · 13.2 KB

i18n.md

File metadata and controls

288 lines (219 loc) · 13.2 KB

I18n best practices

Introduction

FOLIO's i18n implementation is based on components provided by react-intl. The Basic Internationalization Principles and React Internationalization – How To 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/<module-name> directory, e.g. translations/ui-users/en.json and use the components <FormattedMessage> and <SafeHTMLMessage>, and the method intl.formatMessage, to replace placeholder strings in the code with values loaded from those files.

We use the components <FormattedDate> and <FormattedTime> 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 <strong>{value}</strong> was removed., is to use HTML directly in the translation files and to use the <SafeHTMLMessage> component to display it. (HTML markup in values passed through <FormattedMessage> will be escaped, e.g. <strong> will be converted to &amp;%lt;strong&gt;&amp; whereas <SafeHTMLMessage> allows HTML markup to stand but sanitizes it to remove dangerous code without escaping known-good values.)

The FOLIO Project uses lokalise.com to manage the process of providing language-specific strings in FOLIO modules. Developers add keys and English translation strings to the translations/<module-name>/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, email) 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 <IntlProvider> available by consuming its <IntlConsumer> 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 = <FormattedMessage id="the.message.id" values={{ value: "Flying Squirrel" }} />;

If you need an actual string rather than a React.Node, as for an aria-label attribute, use the useIntl hook, the injectIntl HOC or stripes-core's <IntlConsumer>:

import { IntlConsumer } from 'stripes-core';
...
<IntlConsumer>
  {intl => (
    <SomeComponent ariaLabel={intl.formatMessage({ id: 'the.message.id' })} />
  )}
</IntlConsumer>

In a functional component, you may retrieve the intl object via the useIntl Hook.

For HTML template strings, e.g. The value <strong>{value}</strong> was removed., use

import SafeHTMLMessage from '@folio/react-intl-safe-html';
...
const message = <SafeHTMLMessage id="the.message.id" values={{ value: "Frostbite Falls" }} />;

For date and time values formatted as React.Node elements, use

import { FormattedDate } from 'react-intl';
...
const message = <FormattedDate value={item.metadata.updatedDate} />

or, if you need an actual string, consume <IntlConsumer> 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 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" without a formatter is handled the same way as a string; the value is simply replaced by the argument. A "plural" 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 = <FormattedDate value={item.metadata.updatedDate} /> 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(). 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)

<FormatMessage> defaults to wrapping the translated messages in a <span> - 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.

<FormattedMessage id="module.message.id">
  { label => (
   <TextField
     label={label}
     ...
   />
  )}
</FormattedMessage>

IntlConsumer component

For cases where multiple translated strings are necessary, stripes-core provides an <IntlConsumer> 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
<IntlConsumer>
  { intl => (
    <SearchAndSort
      columnMapping={{
        name: intl.formatMessage({ id: 'module.mappedName' }),
        status: intl.formatMessage({ id: 'module.mappedStatus' }),
        ...
      }}
    />
  )}
</IntlConsumer>

intl object

The <IntlProvider> 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 (<div>{msg}</div>);
   }
}

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:

<FormattedMessage id={`ui-feeds.urlstatus.${status}`} />

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} <FormattedMessage id="ui-feeds.idWasFound" />
"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:

<FormattedMessage id="ui-feeds.idWasFound" values={{ value: "7162221" }} />
"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 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:

<FormattedMessage id={`ui-feeds.wasFound.${type}`} />
<FormattedMessage id={`ui-feeds.delete.${type}`} />

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",