- Introduction
- TL;DR
- Details
- Don't split sentences
- Use placeholders for dynamic content
- Avoid substituting nouns and verbs for placeholders
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 &%lt;strong>&
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").
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)
.
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".
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.
<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>
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>
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);
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.
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"
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",