Skip to content

Commit

Permalink
Add draft implementation of I18n engine (#19555) (#21473)
Browse files Browse the repository at this point in the history
* Add draft implementation of I18n engine

* Add i18n loader

* kbn-i18n refactoring

* Fix react i18n context and update doc

* i18n engine refactoring

* Fix locales data loading and add more jsdoc comments

* Fix verify_translations task

* I18n tests refactoring

* Add build scripts to kbn-i18n package

* Fix some bugs

* Move uiI18nMixin into ui_i18n folder

* Add 'browser' field to kbn-i18n package.json

* Get rid of "showError" method

* Make i18n and i18nLoader a singleton object

* Add default locale as fallback if translation files were not registered

* Update yarn.lock

* kbn-i18n fix

* Add default formats

* Try to fix build

* Add more examples into kbn-i18n/README.md

* kbn-i18n fix

* Fix app_bootstrap tests

* Add links to issues in TODO comments
  • Loading branch information
LeanidShutau authored Aug 1, 2018
1 parent 7ac477a commit 1d0e438
Show file tree
Hide file tree
Showing 42 changed files with 3,461 additions and 566 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ module.exports = {
'packages/kbn-pm/**/*',
'packages/kbn-es/**/*',
'packages/kbn-datemath/**/*.js',
'packages/kbn-i18n/**/*',
'packages/kbn-plugin-generator/**/*',
'packages/kbn-test/**/*',
'packages/kbn-eslint-import-resolver-kibana/**/*',
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,11 @@
"@elastic/ui-ace": "0.2.3",
"@kbn/babel-preset": "link:packages/kbn-babel-preset",
"@kbn/datemath": "link:packages/kbn-datemath",
"@kbn/i18n": "link:packages/kbn-i18n",
"@kbn/pm": "link:packages/kbn-pm",
"@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector",
"@kbn/ui-framework": "link:packages/kbn-ui-framework",
"JSONStream": "1.1.1",
"accept-language-parser": "1.2.0",
"abortcontroller-polyfill": "^1.1.9",
"angular": "1.6.9",
"angular-aria": "1.6.6",
Expand Down
10 changes: 10 additions & 0 deletions packages/kbn-i18n/.babelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"env": {
"web": {
"presets": ["@kbn/babel-preset/webpack_preset"]
},
"node": {
"presets": ["@kbn/babel-preset/node_preset"]
}
}
}
114 changes: 80 additions & 34 deletions src/ui/ui_i18n/README.md → packages/kbn-i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,23 @@ data to UI frameworks and provides methods for the direct translation.

Here is the public API exposed by this engine:

- `addMessages(messages: Map<string, string>, [locale: string])` - provides a way to register
translations with the engine
- `getMessages()` - returns messages for the current language
- `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
- `defineFormats(formats: object)` - supplies a set of options to the underlying formatter.
- `setFormats(formats: object)` - supplies a set of options to the underlying formatter.
For the detailed explanation, see the section below
- `translate(id: string, [{values: object, defaultMessage: string}])` – translate message by id
- `getFormats()` - returns current formats
- `getRegisteredLocales()` - returns array of locales having translations
- `translate(id: string, [{values: object, defaultMessage: string, context: string}])`
translate message by id. `context` is optional context comment that will be extracted
by i18n tools and added as a comment next to translation message at `defaultMessages.json`.
- `init(messages: Map<string, string>)` - initializes the engine

#### I18n engine internals

Expand Down Expand Up @@ -179,37 +186,32 @@ React Intl uses the provider pattern to scope an i18n context to a tree of compo
are able to use `FormattedMessage` component in order to translate messages.
`IntlProvider` should wrap react app's root component (inside each react render method).

In order to translate messages we need to pass them into the `IntlProvider`
from I18n engine:
In order to translate messages we need to use `I18nProvider` component that
uses I18n engine under the hood:

```js
import React from 'react';
import ReactDOM from 'react-dom';
import { ReactI18n } from '@kbn/i18n';

import i18n from 'kbn-i18n';
import { IntlProvider } from 'ui/i18n/react-intl';

const locale = i18n.getLocale();
const messages = i18n.getMessages();
const { I18nProvider } = ReactI18n;

ReactDOM.render(
<IntlProvider
locale={locale}
messages={messages}
>
<I18nProvider>
<RootComponent>
...
</RootComponent>
</IntlProvider>,
</I18nProvider>,
document.getElementById('container')
);
```

After that we can use `FormattedMessage` components inside `RootComponent`:
```js
import React, { Component } from 'react';
import { ReactI18n } from '@kbn/i18n';

import { FormattedMessage } from 'ui/i18n/react-intl';
const { FormattedMessage } = ReactI18n;

class RootComponent extends Component {
constructor(props) {
Expand Down Expand Up @@ -244,6 +246,40 @@ class RootComponent extends Component {
}
```

Optionally we can pass `context` prop into `FormattedMessage` component.
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`


#### Attributes translation in React
React wrapper provides an API to inject the imperative formatting API into a React
component by using render callback pattern. 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 components into `I18nContext` component. The child of this component
should be a function that takes `intl` object into parameters:

```js
import React from 'react';
import { ReactI18n } from '@kbn/i18n';

const { I18nContext } = ReactI18n;

const MyComponent = () => (
<I18nContext>
{intl => (
<input
type="text"
placeholder={intl.formatMessage({
id: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
defaultMessage: 'Search',
})}
/>
)}
</I18nContext>
);
```

## Angular

Angular wrapper has 4 entities: translation `provider`, `service`, `directive`
Expand All @@ -260,58 +296,65 @@ language key
- `setDefaultLocale(locale: string)` - tells the library which language to fallback
when missing translations
- `getDefaultLocale()` - returns the default locale
- `defineFormats(formats: object)` - supplies a set of options to the underlying formatter
- `setFormats(formats: object)` - supplies a set of options to the underlying formatter
- `getFormats()` - returns current formats
- `getRegisteredLocales()` - returns array of locales having translations
- `init(messages: Map<string, string>)` - initializes the engine

The translation `service` provides only one method:
- `translate(id: string, [{values: object, defaultMessage: string}])` – translate message by id
- `i18n(id: string, [{values: object, defaultMessage: string, context: string }])`
translate message by id

The translation `filter` is used for attributes translation and has
the following syntax:
```
{{'translationId' | i18n[:{ values: object, defaultMessage: string }]}}
{{'translationId' | i18n[:{ values: object, defaultMessage: string, context: string }]}}
```

Where:
- `translationId` - translation id to be translated
- `values` - values to pass into translation
- `defaultMessage` - will be used unless translation was successful (the final
fallback in english, will be used for generating `en.json`)
- `context` - optional context comment that will be extracted by i18n tools
and added as a comment next to translation message at `defaultMessages.json`

The translation `directive` has the following syntax:
```html
<ANY
i18n-id="{string}"
[i18n-values="{object}"]
[i18n-default-message="{string}"]
[i18n-context="{string}"]
></ANY>
```

Where:
- `i18n-id` - translation id to be translated
- `i18n-values` - values to pass into translation
- `i18n-default-message` - will be used unless translation was successful
- `i18n-context` - optional context comment that will be extracted by i18n tools
and added as a comment next to translation message at `defaultMessages.json`

In order to initialize the translation service, we need to pass locale and
localization messages from I18n engine into the `i18nProvider`:

```js
import { uiModules } from 'ui/modules';
import i18n from 'kbn-i18n';

uiModules.get('kibana').config(function (i18nProvider) {
i18nProvider.addMessages(i18n.getMessages());
i18nProvider.setLocale(i18n.getLocale());
});
```

After that we can use i18n directive in Angular templates:
Angular `I18n` module is placed into `autoload` module, so it will be
loaded automatically. After that we can use i18n directive in Angular templates:
```html
<span
i18n-id="welcome"
i18n-default-message="Hello!"
></span>
```

In order to translate attributes in Angular we should use `i18nFilter`:
```html
<input
type="text"
placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | i18n: {
defaultMessage: 'Search'
} }}"
>
```

## Node.JS

`Intl-messageformat` package assumes that the
Expand All @@ -320,10 +363,13 @@ 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):
for all 200+ languages is loaded along with the library) and pass the translation
messages into `init` method:

```js
import i18n from 'kbn-i18n';
import { i18n } from '@kbn/i18n';

i18n.init(messages);
```

After that we are able to use all methods exposed by the i18n engine
Expand Down
32 changes: 32 additions & 0 deletions packages/kbn-i18n/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"name": "@kbn/i18n",
"browser": "./target/web/browser.js",
"main": "./target/node/index.js",
"module": "./src/index.js",
"version": "1.0.0",
"license": "Apache-2.0",
"private": true,
"scripts": {
"build": "yarn build:web && yarn build:node",
"build:web": "cross-env BABEL_ENV=web babel src --out-dir target/web",
"build:node": "cross-env BABEL_ENV=node babel src --out-dir target/node",
"kbn:bootstrap": "yarn build",
"kbn:watch": "yarn build --watch"
},
"devDependencies": {
"@kbn/babel-preset": "link:../kbn-babel-preset",
"@kbn/dev-utils": "link:../kbn-dev-utils",
"babel-cli": "^6.26.0",
"cross-env": "^5.2.0"
},
"dependencies": {
"accept-language-parser": "^1.5.0",
"intl-format-cache": "^2.1.0",
"intl-messageformat": "^2.2.0",
"intl-relativeformat": "^2.1.0",
"json5": "^1.0.1",
"prop-types": "^15.5.8",
"react": "^16.3.0",
"react-intl": "^2.4.0"
}
}
43 changes: 43 additions & 0 deletions packages/kbn-i18n/src/angular/directive.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export function i18nDirective(i18n) {
return {
restrict: 'A',
scope: {
id: '@i18nId',
defaultMessage: '@i18nDefaultMessage',
values: '=i18nValues',
},
link: function($scope, $element) {
$scope.$watchGroup(['id', 'defaultMessage', 'values'], function([
id,
defaultMessage = '',
values = {},
]) {
$element.html(
i18n(id, {
values,
defaultMessage,
})
);
});
},
};
}
27 changes: 27 additions & 0 deletions packages/kbn-i18n/src/angular/filter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export function i18nFilter(i18n) {
return function(id, { defaultMessage = '', values = {} } = {}) {
return i18n(id, {
values,
defaultMessage,
});
};
}
22 changes: 22 additions & 0 deletions packages/kbn-i18n/src/angular/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

export { i18nProvider } from './provider';
export { i18nFilter } from './filter';
export { i18nDirective } from './directive';
Loading

0 comments on commit 1d0e438

Please sign in to comment.