Skip to content

Latest commit

 

History

History
220 lines (172 loc) · 10.3 KB

i18n.md

File metadata and controls

220 lines (172 loc) · 10.3 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.co to manage the process of providing language-specific strings is FOLIO modules. Developers add keys and English translation strings to the translations/<module-name>/en.json file. New strings are automatically added to the database of strings in Lokalise 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. (If the continuous integration tests for any module fails, all pull requests for that batch job run are deleted. This ensures a consistent state between the Lokalise database and the GitHub repositories.) 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);

Examples

translations/ui-my-module/en.json

{
  "search": "Search",
  "resultCount": "{count, number} {count, plural, one {Record found} other {Records found}}",

  "cv.cannotDeleteTermMessage": "This {type} cannot be deleted, as it is in use by one or more records.",
  "cv.numberOfObjects": "# of {objects}",
  "cv.termDeleted": "The {type} <b>{term}</b> was successfully <b>deleted</b>"
  }
import { FormattedDate, FormattedMessage, injectIntl } from 'react-intl';
import SafeHTMLMessage from '@folio/react-intl-safe-html';

class ControlledVocab extends React.Component {
  static propTypes = {
      intl: PropTypes.object.isRequired
  };

  getHtmlMessage(item) {
    const message = (
      <SafeHTMLMessage
        id="stripes-smart-components.cv.termDeleted"
        values={{
          type: this.props.labelSingular.toLowerCase(),
          term: item[this.state.primaryField],
        }}
      />
    );

    return message;
  }

  getTextMessage(item) {
    const message = (
      <Col xs>
        <FormattedMessage id="stripes-smart-components.cv.cannotDeleteTermMessage" values={{ type: item.type }} />
      </Col>
    );

    return message;
  }

  getTextString(item) {
    return this.props.intl.formatMessage({ id: 'stripes-smart-components.cv.numberOfObjects' }, { objects: this.props.objectLabel });
  }

  getDate(item) {
    return <FormattedDate value={item.metadata.updatedDate} />;
  }
}

export default injectIntl(ControlledVocab);