Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[6.x] Add draft implementation of I18n engine (#19555) #21473

Merged
merged 1 commit into from
Aug 1, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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