2018-05-28 09:25:40 +02:00
# I18n
2018-12-03 14:02:47 +01:00
Kibana relies on several UI frameworks (ReactJS and AngularJS) and
2018-05-28 09:25:40 +02:00
requires localization in different environments (browser and NodeJS).
Internationalization engine is framework agnostic and consumable in
2018-12-03 14:02:47 +01:00
all parts of Kibana (ReactJS, AngularJS and NodeJS). In order to simplify
2018-05-28 09:25:40 +02:00
internationalization in UI frameworks, the additional abstractions are
built around the I18n engine: `react-intl` for React and custom
2018-12-03 14:02:47 +01:00
components for AngularJS. [React-intl ](https://github.com/yahoo/react-intl )
2018-05-28 09:25:40 +02:00
is built around [intl-messageformat ](https://github.com/yahoo/intl-messageformat ),
2018-12-03 14:02:47 +01:00
so both React and AngularJS frameworks use the same engine and the same
2018-05-28 09:25:40 +02:00
message syntax.
## Localization files
2018-12-05 08:36:05 +01:00
Localization files are JSON files.
2018-05-28 09:25:40 +02:00
Using comments can help to understand which section of the application
the localization key is used for. Also `namespaces`
are used in order to simplify message location search. For example, if
we are going to translate the title of `/management/sections/objects/_objects.html`
2018-12-03 14:02:47 +01:00
file, we should use message path like this: `'management.objects.objectsTitle'` .
2018-05-28 09:25:40 +02:00
Each Kibana plugin has a separate folder with translation files located at
```
{path/to/plugin}/translations/{locale}.json
```
where `locale` is [ISO 639 language code ](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes ).
For example:
```
2018-12-03 12:13:29 +01:00
src/legacy/core_plugins/kibana/translations/fr.json
2018-05-28 09:25:40 +02:00
```
2019-06-18 20:45:53 +02:00
The engine scans `x-pack/legacy/plugins/*/translations` , `src/core_plugins/*/translations` , `plugins/*/translations` and `src/legacy/ui/translations` folders on initialization, so there is no need to register translation files.
2018-05-28 09:25:40 +02:00
2018-08-02 13:43:22 +02:00
The engine uses a `config/kibana.yml` file for locale resolution process. If locale is
defined via `i18n.locale` option in `config/kibana.yml` then it will be used as a base
locale, otherwise i18n engine will fall back to `en` . The `en` locale will also be used
if translation can't be found for the base non-English locale.
One of our technical requirements is to have default messages in the templates
themselves, and those messages will always be in English, so we don't have to keep
`en.json` file in repository. We can generate that file from `defaultMessage` s
2018-05-28 09:25:40 +02:00
defined inline.
2018-08-02 13:43:22 +02:00
__Note:__ locale defined in `i18n.locale` and the one used for translation files should
2019-01-21 11:51:43 +01:00
match exactly, e.g. `i18n.locale: zh` and `.../translations/zh-CN.json` won't match and
default English translations will be used, but `i18n.locale: zh-CN` and`.../translations/zh-CN.json`
or `i18n.locale: zh` and `.../translations/zh.json` will work as expected.
__Note:__ locale should look like `zh-CN` where `zh` - lowercase two-letter or three-letter ISO-639 code
and `CN` - uppercase two-letter ISO-3166 code (optional).
[ISO-639 ](https://www.iso.org/iso-639-language-codes.html ) and [ISO-3166 ](https://www.iso.org/iso-3166-country-codes.html ) codes should be separated with `-` character.
2018-08-02 13:43:22 +02:00
2018-05-28 09:25:40 +02:00
## I18n engine
I18n engine is the platform agnostic abstraction that helps to supply locale
data to UI frameworks and provides methods for the direct translation.
Here is the public API exposed by this engine:
2019-10-11 17:53:36 +02:00
- `addTranslation(newTranslation: Translation, [locale: string])` - provides a way to register
2018-06-28 11:38:39 +02:00
translations with the engine
2019-10-11 17:53:36 +02:00
- `getTranslation()` - returns messages for the current language
2018-05-28 09:25:40 +02:00
- `setLocale(locale: string)` - tells the engine which language to use by given
language key
- `getLocale()` - returns the current locale
- `setDefaultLocale(locale: string)` - tells the library which language to fallback
when missing translations
- `getDefaultLocale()` - returns the default locale
2018-06-28 11:38:39 +02:00
- `setFormats(formats: object)` - supplies a set of options to the underlying formatter.
2018-05-28 09:25:40 +02:00
For the detailed explanation, see the section below
2018-06-28 11:38:39 +02:00
- `getFormats()` - returns current formats
- `getRegisteredLocales()` - returns array of locales having translations
2018-12-05 08:36:05 +01:00
- `translate(id: string, { values: object, defaultMessage: string, description: string })` –
2018-11-06 13:54:05 +01:00
translate message by id. `description` is optional context comment that will be extracted
2018-06-28 11:38:39 +02:00
by i18n tools and added as a comment next to translation message at `defaultMessages.json` .
- `init(messages: Map<string, string>)` - initializes the engine
2019-10-11 17:53:36 +02:00
- `load(translationsUrl: string)` - loads JSON with translations from the specified URL and initializes i18n engine with them.
2018-05-28 09:25:40 +02:00
#### I18n engine internals
The engine uses the ICU Message syntax and works for all CLDR languages which
have pluralization rules defined. It's built around `intl-messageformat` package
which exposes `IntlMessageFormat` class. Messages are provided into the constructor
as a string message, or a pre-parsed AST object.
```js
import IntlMessageFormat from 'intl-messageformat';
const msg = new IntlMessageFormat(message, locales, [formats]);
```
The string `message` is parsed, then stored internally in a
compiled form that is optimized for the `format()` method to
produce the formatted string for displaying to the user.
```js
const output = msg.format(values);
```
`formats` parameter in `IntlMessageFormat` constructor allows formatting numbers
and dates/times in messages using `Intl.NumberFormat` and `Intl.DateTimeFormat` ,
respectively.
```js
const msg = new IntlMessageFormat('The price is: {price, number, USD}', 'en-US', {
number: {
USD: {
style : 'currency',
currency: 'USD',
},
},
});
const output = msg.format({ price: 100 });
console.log(output); // => "The price is: $100.00"
```
In this example, we're defining a USD number format style which is passed to
the underlying `Intl.NumberFormat` instance as its options.
[Here ](https://github.com/yahoo/intl-messageformat/blob/master/src/core.js#L62 )
you can find default format options used as the prototype of the formats
provided to the constructor.
Creating instances of `IntlMessageFormat` is expensive.
[Intl-format-cache ](https://github.com/yahoo/intl-format-cache )
library is simply to make it easier to create a cache of format
instances of a particular type to aid in their reuse. Under the
hood, this package creates a cache key based on the arguments passed
to the memoized constructor.
```js
import memoizeIntlConstructor from 'intl-format-cache';
const getMessageFormat = memoizeIntlConstructor(IntlMessageFormat);
```
2018-09-27 16:16:52 +02:00
## Vanilla JS
`Intl-messageformat` package assumes that the
[Intl ](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl )
global object exists in the runtime. `Intl` is present in all modern
browsers and Node.js 0.10+. In order to load i18n engine
in Node.js we should simply `import` this module (in Node.js, the
[data ](https://github.com/yahoo/intl-messageformat/tree/master/dist/locale-data )
for all 200+ languages is loaded along with the library) and pass the translation
messages into `init` method:
```js
import { i18n } from '@kbn/i18n';
i18n.init(messages);
```
2018-10-10 14:46:53 +02:00
One common use-case is that of internationalizing a string constant. Here's an
2018-09-27 16:16:52 +02:00
example of how we'd do that:
```js
import { i18n } from '@kbn/i18n';
export const HELLO_WORLD = i18n.translate('hello.wonderful.world', {
defaultMessage: 'Greetings, planet Earth!',
2019-02-04 13:38:00 +01:00
});
2018-09-27 16:16:52 +02:00
```
2018-12-03 14:02:47 +01:00
One more example with a parameter:
```js
import { i18n } from '@kbn/i18n';
export function getGreetingMessage(userName) {
return i18n.translate('hello.wonderful.world', {
defaultMessage: 'Greetings, {name}!',
values: { name: userName },
2019-02-12 14:29:46 +01:00
description: 'This is greeting message for main screen.'
2018-12-03 14:02:47 +01:00
});
}
```
2018-09-27 16:16:52 +02:00
We're also able to use all methods exposed by the i18n engine
(see [I18n engine ](#i18n-engine ) section above for more details).
2018-05-28 09:25:40 +02:00
## React
[React-intl ](https://github.com/yahoo/react-intl ) library is used for internalization
React part of the application. It provides React components and an API to format
dates, numbers, and strings, including pluralization and handling translations.
React Intl uses the provider pattern to scope an i18n context to a tree of components.
`IntlProvider` component is used to setup the i18n context for a tree. After that we
are able to use `FormattedMessage` component in order to translate messages.
`IntlProvider` should wrap react app's root component (inside each react render method).
2018-06-28 11:38:39 +02:00
In order to translate messages we need to use `I18nProvider` component that
uses I18n engine under the hood:
2018-05-28 09:25:40 +02:00
```js
import React from 'react';
import ReactDOM from 'react-dom';
2018-07-16 16:02:06 +02:00
import { I18nProvider } from '@kbn/i18n/react';
2018-05-28 09:25:40 +02:00
ReactDOM.render(
2018-06-28 11:38:39 +02:00
< I18nProvider >
2018-05-28 09:25:40 +02:00
< RootComponent >
...
< / RootComponent >
2018-06-28 11:38:39 +02:00
< / I18nProvider > ,
2018-05-28 09:25:40 +02:00
document.getElementById('container')
);
```
After that we can use `FormattedMessage` components inside `RootComponent` :
2018-12-13 09:19:33 +01:00
```jsx
2018-05-28 09:25:40 +02:00
import React, { Component } from 'react';
2018-07-16 16:02:06 +02:00
import { FormattedMessage } from '@kbn/i18n/react';
2018-05-28 09:25:40 +02:00
class RootComponent extends Component {
constructor(props) {
super(props);
this.state = {
name: 'Eric',
unreadCount: 1000,
};
}
render() {
const {
name,
unreadCount,
} = this.state;
return (
< p >
< FormattedMessage
id="welcome"
2018-12-13 09:19:33 +01:00
defaultMessage="Hello {name}, you have {unreadCount, number} {unreadCount, plural,
2018-05-28 09:25:40 +02:00
one {message}
other {messages}
2018-12-13 09:19:33 +01:00
}"
2018-05-28 09:25:40 +02:00
values={{name: < b > {name}< / b > , unreadCount}}
/>
...
< / p >
);
}
}
```
2018-11-06 13:54:05 +01:00
Optionally we can pass `description` prop into `FormattedMessage` component.
2018-06-28 11:38:39 +02:00
This prop is optional context comment that will be extracted by i18n tools
and added as a comment next to translation message at `defaultMessages.json`
2019-02-04 13:38:00 +01:00
**NOTE:** To minimize the chance of having multiple `I18nProvider` components in the React tree, try to use `I18nProvider` only to wrap the topmost component that you render, e.g. the one that's passed to `reactDirective` or `ReactDOM.render` .
2018-06-28 11:38:39 +02:00
2018-12-18 11:25:16 +01:00
### FormattedRelative
`FormattedRelative` expects several attributes (read more [here ](https://github.com/yahoo/react-intl/wiki/Components#formattedrelative )), including
- `value` that can be parsed as a date,
- `formats` that should be one of `'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds'` (this options are configured in [`formats.ts` ](./src/core/formats.ts ))
- etc.
If `formats` is not provided then it will be chosen automatically:\
`x seconds ago` for `x < 60` , `1 minute ago` for `60 <= x < 120` , etc.
```jsx
< FormattedRelative
value={Date.now() - 90000}
format="seconds"
/>
```
Initial result: `90 seconds ago`
```jsx
< FormattedRelative
value={Date.now() - 90000}
/>
```
Initial result: `1 minute ago`
2018-12-03 14:02:47 +01:00
### Attributes translation in React
2018-07-13 11:21:50 +02:00
2019-06-04 14:01:11 +02:00
The long term plan is to rely on using `FormattedMessage` and `i18n.translate()` by statically importing `i18n` from the `@kbn/i18n` package. **Avoid using `injectI18n` and rely on `i18n.translate()` instead.**
2018-07-13 11:21:50 +02:00
React wrapper provides an ability to inject the imperative formatting API into a React component via its props using `injectI18n` Higher-Order Component. This should be used when your React component needs to format data to a string value where a React element is not suitable; e.g., a `title` or `aria` attribute. In order to use it you should wrap your component with `injectI18n` Higher-Order Component. The formatting API will be provided to the wrapped component via `props.intl` .
React component as a pure function:
```js
import React from 'react';
2018-07-16 16:02:06 +02:00
import { injectI18n, intlShape } from '@kbn/i18n/react';
2018-07-13 11:21:50 +02:00
2018-12-21 10:39:42 +01:00
export const MyComponent = injectI18n({ intl }) => (
2018-07-13 11:21:50 +02:00
< input
type="text"
2018-10-25 13:09:23 +02:00
placeholder={intl.formatMessage(
{
id: 'welcome',
defaultMessage: 'Hello {name}, you have {unreadCount, number}\
{unreadCount, plural, one {message} other {messages}}',
2018-11-06 13:54:05 +01:00
description: 'Message description',
2018-10-25 13:09:23 +02:00
},
{ name, unreadCount }
)}
2018-07-13 11:21:50 +02:00
/>
2018-12-21 10:39:42 +01:00
));
2018-07-13 11:21:50 +02:00
2018-12-21 10:39:42 +01:00
MyComponent.WrappedComponent.propTypes = {
2018-07-13 11:21:50 +02:00
intl: intlShape.isRequired,
};
```
React component as a class:
2018-06-28 11:38:39 +02:00
```js
import React from 'react';
2018-07-16 16:02:06 +02:00
import { injectI18n, intlShape } from '@kbn/i18n/react';
2018-07-13 11:21:50 +02:00
2018-12-21 10:39:42 +01:00
export const MyComponent = injectI18n(
class MyComponent extends React.Component {
static propTypes = {
intl: intlShape.isRequired,
};
2018-06-28 11:38:39 +02:00
2018-12-21 10:39:42 +01:00
render() {
const { intl } = this.props;
2018-07-13 11:21:50 +02:00
2018-12-21 10:39:42 +01:00
return (
< input
type="text"
placeholder={intl.formatMessage({
id: 'kbn.management.objects.searchPlaceholder',
defaultMessage: 'Search',
})}
/>
);
}
2018-07-13 11:21:50 +02:00
}
2018-12-21 10:39:42 +01:00
);
2018-06-28 11:38:39 +02:00
```
2018-12-05 08:36:05 +01:00
## I18n tools
In order to simplify localization process, some additional tools were implemented:
- tool for verifying all translations have translatable strings and extracting default messages from templates
- tool for verifying translation files and integrating them to Kibana
[I18n tools documentation ](../../src/dev/i18n/README.md )