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