• Home
  • History
  • Annotate
Name Date Size #Lines LOC

..22-Nov-2021-

angular/H22-Nov-2021-65

react/H22-Nov-2021-65

scripts/H03-May-2022-

src/H22-Nov-2021-3328

target/H22-Nov-2021-2,2552,255

GUIDELINE.mdH A D22-Nov-202118 KiB464348

README.mdH A D22-Nov-202115.8 KiB445358

package.jsonH A D22-Nov-2021932 3534

README.md

1# I18n
2
3OpenSearch Dashboards relies on several UI frameworks (ReactJS and AngularJS) and
4requires localization in different environments (browser and NodeJS).
5Internationalization engine is framework agnostic and consumable in
6all parts of OpenSearch Dashboards (ReactJS, AngularJS and NodeJS). In order to simplify
7internationalization in UI frameworks, the additional abstractions are
8built around the I18n engine: `react-intl` for React and custom
9components for AngularJS. [React-intl](https://github.com/yahoo/react-intl)
10is built around [intl-messageformat](https://github.com/yahoo/intl-messageformat),
11so both React and AngularJS frameworks use the same engine and the same
12message syntax.
13
14## Localization files
15
16Localization files are JSON files.
17
18Using comments can help to understand which section of the application
19the localization key is used for. Also `namespaces`
20are used in order to simplify message location search. For example, if
21we are going to translate the title of `/management/sections/objects/_objects.html`
22file, we should use message path like this: `'management.objects.objectsTitle'`.
23
24Each OpenSearch Dashboards plugin has a separate folder with translation files located at
25```
26{path/to/plugin}/translations/{locale}.json
27```
28
29where `locale` is [ISO 639 language code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes).
30
31For example:
32```
33src/legacy/core_plugins/opensearch-dashboards/translations/fr.json
34```
35
36The engine scans `src/core_plugins/*/translations`, `plugins/*/translations` and `src/legacy/ui/translations` folders on initialization, so there is no need to register translation files.
37
38The engine uses a `config/opensearch_dashboards.yml` file for locale resolution process. If locale is
39defined via `i18n.locale` option in `config/opensearch_dashboards.yml` then it will be used as a base
40locale, otherwise i18n engine will fall back to `en`. The `en` locale will also be used
41if translation can't be found for the base non-English locale.
42
43One of our technical requirements is to have default messages in the templates
44themselves, and those messages will always be in English, so we don't have to keep
45`en.json` file in repository. We can generate that file from `defaultMessage`s
46defined inline.
47
48__Note:__ locale defined in `i18n.locale` and the one used for translation files should
49match exactly, e.g. `i18n.locale: zh` and `.../translations/zh-CN.json` won't match and
50default English translations will be used, but `i18n.locale: zh-CN` and`.../translations/zh-CN.json`
51or `i18n.locale: zh` and `.../translations/zh.json` will work as expected.
52
53__Note:__ locale should look like `zh-CN` where `zh` - lowercase two-letter or three-letter ISO-639 code
54and `CN` - uppercase two-letter ISO-3166 code (optional).
55[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.
56
57## I18n engine
58
59I18n engine is the platform agnostic abstraction that helps to supply locale
60data to UI frameworks and provides methods for the direct translation.
61
62Here is the public API exposed by this engine:
63
64- `addTranslation(newTranslation: Translation, [locale: string])` - provides a way to register
65translations with the engine
66- `getTranslation()` - returns messages for the current language
67- `setLocale(locale: string)` - tells the engine which language to use by given
68language key
69- `getLocale()` - returns the current locale
70- `setDefaultLocale(locale: string)` - tells the library which language to fallback
71when missing translations
72- `getDefaultLocale()` - returns the default locale
73- `setFormats(formats: object)` - supplies a set of options to the underlying formatter.
74For the detailed explanation, see the section below
75- `getFormats()` - returns current formats
76- `getRegisteredLocales()` - returns array of locales having translations
77- `translate(id: string, { values: object, defaultMessage: string, description: string })` –
78translate message by id. `description` is optional context comment that will be extracted
79by i18n tools and added as a comment next to translation message at `defaultMessages.json`.
80- `init(messages: Map<string, string>)` - initializes the engine
81- `load(translationsUrl: string)` - loads JSON with translations from the specified URL and initializes i18n engine with them.
82
83#### I18n engine internals
84
85The engine uses the ICU Message syntax and works for all CLDR languages which
86have pluralization rules defined. It's built around `intl-messageformat` package
87which exposes `IntlMessageFormat` class. Messages are provided into the constructor
88as a string message, or a pre-parsed AST object.
89
90```js
91import IntlMessageFormat from 'intl-messageformat';
92
93const msg = new IntlMessageFormat(message, locales, [formats]);
94```
95
96The string `message` is parsed, then stored internally in a
97compiled form that is optimized for the `format()` method to
98produce the formatted string for displaying to the user.
99
100```js
101const output = msg.format(values);
102```
103
104`formats` parameter in `IntlMessageFormat` constructor allows formatting numbers
105and dates/times in messages using `Intl.NumberFormat` and `Intl.DateTimeFormat`,
106respectively.
107
108```js
109const msg = new IntlMessageFormat('The price is: {price, number, USD}', 'en-US', {
110  number: {
111    USD: {
112      style   : 'currency',
113      currency: 'USD',
114    },
115  },
116});
117
118const output = msg.format({ price: 100 });
119
120console.log(output); // => "The price is: $100.00"
121```
122
123In this example, we're defining a USD number format style which is passed to
124the underlying `Intl.NumberFormat` instance as its options.
125[Here](https://github.com/yahoo/intl-messageformat/blob/master/src/core.js#L62)
126you can find default format options used as the prototype of the formats
127provided to the constructor.
128
129Creating instances of `IntlMessageFormat` is expensive.
130[Intl-format-cache](https://github.com/yahoo/intl-format-cache)
131library is simply to make it easier to create a cache of format
132instances of a particular type to aid in their reuse. Under the
133hood, this package creates a cache key based on the arguments passed
134to the memoized constructor.
135
136```js
137import memoizeIntlConstructor from 'intl-format-cache';
138
139const getMessageFormat = memoizeIntlConstructor(IntlMessageFormat);
140```
141
142## Vanilla JS
143
144`Intl-messageformat` package assumes that the
145[Intl](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl)
146global object exists in the runtime. `Intl` is present in all modern
147browsers and Node.js 0.10+. In order to load i18n engine
148in Node.js we should simply `import` this module (in Node.js, the
149[data](https://github.com/yahoo/intl-messageformat/tree/master/dist/locale-data)
150for all 200+ languages is loaded along with the library) and pass the translation
151messages into `init` method:
152
153```js
154import { i18n } from '@osd/i18n';
155
156i18n.init(messages);
157```
158
159One common use-case is that of internationalizing a string constant. Here's an
160example of how we'd do that:
161
162```js
163import { i18n } from '@osd/i18n';
164
165export const HELLO_WORLD = i18n.translate('hello.wonderful.world', {
166  defaultMessage: 'Greetings, planet Earth!',
167});
168```
169
170One more example with a parameter:
171
172```js
173import { i18n } from '@osd/i18n';
174
175export function getGreetingMessage(userName) {
176  return i18n.translate('hello.wonderful.world', {
177    defaultMessage: 'Greetings, {name}!',
178    values: { name: userName },
179    description: 'This is greeting message for main screen.'
180  });
181}
182```
183
184We're also able to use all methods exposed by the i18n engine
185(see [I18n engine](#i18n-engine) section above for more details).
186
187## React
188
189[React-intl](https://github.com/yahoo/react-intl) library is used for internalization
190React part of the application. It provides React components and an API to format
191dates, numbers, and strings, including pluralization and handling translations.
192
193React Intl uses the provider pattern to scope an i18n context to a tree of components.
194`IntlProvider` component is used to setup the i18n context for a tree. After that we
195are able to use `FormattedMessage` component in order to translate messages.
196`IntlProvider` should wrap react app's root component (inside each react render method).
197
198In order to translate messages we need to use `I18nProvider` component that
199uses I18n engine under the hood:
200
201```js
202import React from 'react';
203import ReactDOM from 'react-dom';
204import { I18nProvider } from '@osd/i18n/react';
205
206ReactDOM.render(
207  <I18nProvider>
208    <RootComponent>
209      ...
210    </RootComponent>
211  </I18nProvider>,
212  document.getElementById('container')
213);
214```
215
216After that we can use `FormattedMessage` components inside `RootComponent`:
217```jsx
218import React, { Component } from 'react';
219import { FormattedMessage } from '@osd/i18n/react';
220
221class RootComponent extends Component {
222  constructor(props) {
223    super(props);
224
225    this.state = {
226      name: 'Eric',
227      unreadCount: 1000,
228    };
229  }
230
231  render() {
232    const {
233      name,
234      unreadCount,
235    } = this.state;
236
237    return (
238      <p>
239        <FormattedMessage
240          id="welcome"
241          defaultMessage="Hello {name}, you have {unreadCount, number} {unreadCount, plural,
242            one {message}
243            other {messages}
244          }"
245          values={{name: <b>{name}</b>, unreadCount}}
246        />
247        ...
248      </p>
249    );
250  }
251}
252```
253
254Optionally we can pass `description` prop into `FormattedMessage` component.
255This prop is optional context comment that will be extracted by i18n tools
256and added as a comment next to translation message at `defaultMessages.json`
257
258**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`.
259
260### FormattedRelative
261
262`FormattedRelative` expects several attributes (read more [here](https://github.com/yahoo/react-intl/wiki/Components#formattedrelative)), including
263
264- `value` that can be parsed as a date,
265- `formats` that should be one of `'years' | 'months' | 'days' | 'hours' | 'minutes' | 'seconds'` (this options are configured in [`formats.ts`](./src/core/formats.ts))
266-  etc.
267
268If `formats` is not provided then it will be chosen automatically:\
269`x seconds ago` for `x < 60`, `1 minute ago` for `60 <= x < 120`, etc.
270
271```jsx
272<FormattedRelative
273  value={Date.now() - 90000}
274  format="seconds"
275/>
276```
277Initial result: `90 seconds ago`
278```jsx
279<FormattedRelative
280  value={Date.now() - 90000}
281/>
282```
283Initial result: `1 minute ago`
284
285### Attributes translation in React
286
287The long term plan is to rely on using `FormattedMessage` and `i18n.translate()` by statically importing `i18n` from the `@osd/i18n` package. **Avoid using `injectI18n` and rely on `i18n.translate()` instead.**
288
289React 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`.
290
291React component as a pure function:
292
293```js
294import React from 'react';
295import { injectI18n, intlShape } from '@osd/i18n/react';
296
297export const MyComponent = injectI18n({ intl }) => (
298  <input
299    type="text"
300    placeholder={intl.formatMessage(
301      {
302        id: 'welcome',
303        defaultMessage: 'Hello {name}, you have {unreadCount, number}\
304{unreadCount, plural, one {message} other {messages}}',
305        description: 'Message description',
306      },
307      { name, unreadCount }
308    )}
309  />
310));
311
312MyComponent.WrappedComponent.propTypes = {
313  intl: intlShape.isRequired,
314};
315```
316
317React component as a class:
318
319```js
320import React from 'react';
321import { injectI18n, intlShape } from '@osd/i18n/react';
322
323export const MyComponent = injectI18n(
324  class MyComponent extends React.Component {
325    static propTypes = {
326      intl: intlShape.isRequired,
327    };
328
329    render() {
330      const { intl } = this.props;
331
332      return (
333        <input
334          type="text"
335          placeholder={intl.formatMessage({
336            id: 'osd.management.objects.searchPlaceholder',
337            defaultMessage: 'Search',
338          })}
339        />
340      );
341    }
342  }
343);
344```
345
346## AngularJS
347
348The long term plan is to rely on using `i18n.translate()` by statically importing `i18n` from the `@osd/i18n` package. **Avoid using the `i18n` filter and the `i18n` service injected in controllers, directives, services.**
349
350AngularJS wrapper has 4 entities: translation `provider`, `service`, `directive`
351and `filter`. Both the directive and the filter use the translation `service`
352with i18n engine under the hood.
353
354The translation `provider` is used for `service` configuration and
355has the following methods:
356- `addMessages(messages: Map<string, string>, [locale: string])` - provides a way to register
357translations with the library
358- `setLocale(locale: string)` - tells the library which language to use by given
359language key
360- `getLocale()` - returns the current locale
361- `setDefaultLocale(locale: string)` - tells the library which language to fallback
362when missing translations
363- `getDefaultLocale()` - returns the default locale
364- `setFormats(formats: object)` - supplies a set of options to the underlying formatter
365- `getFormats()` - returns current formats
366- `getRegisteredLocales()` - returns array of locales having translations
367- `init(messages: Map<string, string>)` - initializes the engine
368
369The translation `service` provides only one method:
370- `i18n(id: string, { values: object, defaultMessage: string, description: string })` –
371translate message by id
372
373The translation `filter` is used for attributes translation and has
374the following syntax:
375```
376{{ ::'translationId' | i18n: { values: object, defaultMessage: string, description: string } }}
377```
378
379Where:
380- `translationId` - translation id to be translated
381- `values` - values to pass into translation
382- `defaultMessage` - will be used unless translation was successful (the final
383  fallback in english, will be used for generating `en.json`)
384- `description` - optional context comment that will be extracted by i18n tools
385and added as a comment next to translation message at `defaultMessages.json`
386
387The translation `directive` has the following syntax:
388```html
389<ANY
390  i18n-id="{string}"
391  i18n-default-message="{string}"
392  [i18n-values="{object}"]
393  [i18n-description="{string}"]
394></ANY>
395```
396
397Where:
398- `i18n-id` - translation id to be translated
399- `i18n-default-message` - will be used unless translation was successful
400- `i18n-values` - values to pass into translation
401- `i18n-description` - optional context comment that will be extracted by i18n tools
402and added as a comment next to translation message at `defaultMessages.json`
403
404If HTML rendering in `i18n-values` is required then value key in `i18n-values` object
405should have `html_` prefix. Otherwise the value will be inserted to the message without
406HTML rendering.\
407Example:
408```html
409<p
410  i18n-id="namespace.id"
411  i18n-default-message="Text with an emphasized {text}."
412  i18n-values="{
413    html_text: '<em>text</em>',
414  }"
415></p>
416```
417
418Angular `I18n` module is placed into `autoload` module, so it will be
419loaded automatically. After that we can use i18n directive in Angular templates:
420```html
421<span
422  i18n-id="welcome"
423  i18n-default-message="Hello!"
424></span>
425```
426
427In order to translate attributes in AngularJS we should use `i18nFilter`:
428```html
429<input
430  type="text"
431  placeholder="{{ ::'osd.management.objects.searchAriaLabel' | i18n: {
432    defaultMessage: 'Search { title } Object',
433    values: { title }
434  } }}"
435>
436```
437
438## I18n tools
439
440In order to simplify localization process, some additional tools were implemented:
441- tool for verifying all translations have translatable strings and extracting default messages from templates
442- tool for verifying translation files and integrating them to OpenSearch Dashboards
443
444[I18n tools documentation](../../src/dev/i18n/README.md)
445