[i18n] Update guideline (#25098) (#26524)

* Update guideline

* Fix code review comments
This commit is contained in:
Maryia Lapata 2018-12-04 13:50:33 +03:00 committed by GitHub
parent 3d72127138
commit 2b6a6a470b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
2 changed files with 177 additions and 28 deletions

View file

@ -6,23 +6,25 @@
The message ids chosen for message keys are descriptive of the string, and its role in the interface (button, label, header, etc.). Each message id ends with a descriptive type. Types are defined at the end of message id by combining to the last segment using camel case.
The following types are supported:
- Title
- Label
- ButtonLabel
- DropDown
- Placeholder
- Tooltip
- AriaLabel
- ErrorMessage
- ToggleSwitch
- LinkLabel and etc.
Ids should end with:
- Description (in most cases if it's `<p>` tag),
- Title (if it's `<h1>`, `<h2>`, etc. tags),
- Label (if it's `<label>` tag),
- ButtonLabel (if it's `<button>` tag),
- DropDownOptionLabel (if it'a an option),
- Placeholder (if it's a placeholder),
- Tooltip (if it's a tootltip),
- AriaLabel (if it's `aria-label` tag attribute),
- ErrorMessage (if it's an error message),
- LinkText (if it's `<a>` tag),
- ToggleSwitch and etc.
There is one more complex case, when we have to divide a single expression into different labels.
For example the message before translation looks like:
```js
```html
<p>
The following deprecated languages are in use: {deprecatedLangsInUse.join(', ')}. Support for these languages will be removed in the next major version of Kibana and Elasticsearch. Convert your scripted fields to <EuiLink href={painlessDocLink}>Painless</EuiLink> to avoid any problems.
</p>
@ -78,10 +80,33 @@ In case when `indicesLength` has value 1, the result string will be "`1 index`".
## Best practices
### Usage of appropriate component
#### In ReactJS
- You should use `<FormattedMessage>` most of the time.
- In case when the string is expected (`aria-label`, `placeholder`), use `props.intl.formatmessage()` (where `intl` is passed to `props` by `injectI18n` HOC).
- In case if none of the above can not be applied (e.g. it's needed to translate any code that doesn't have access to the component props), you can call JS function `i18n.translate()` from `@kbn/i18n` package.
#### In AngularJS
- Use `i18n` service in controllers, directives, services by injected it.
- Use `i18nId` directive in template.
- Use `i18n` filter in template for attribute translation.
- In case if none of the above can not be applied, you can call JS function `i18n.translate()` from `@kbn/i18n` package.
Note: Use one-time binding ("{{:: ... }}") in filters wherever it's possible to prevent unnecessary expression re-evaluation.
#### In JavaScript
- Use `i18n.translate()` in NodeJS or any other framework agnostic code, where `i18n` is the I18n engine from `@kbn/i18n` package.
### Naming convention
The message ids chosen for message keys should always be descriptive of the string, and its role in the interface (button label, title, etc.). Think of them as long variable names. When you have to change a message id, adding a progressive number to the existing key should always be used as a last resort.
Here's a rule of id maning:
`{plugin}.{area}.[{sub-area}].{element}`
- Message id should start with namespace that identifies a functional area of the app (`common.ui` or `common.server`) or a plugin (`kbn`, `vega`, etc.).
@ -146,7 +171,6 @@ The message ids chosen for message keys should always be descriptive of the stri
/>
```
### Defining type for message
Each message id should end with a type of the message.
@ -251,6 +275,82 @@ For example:
/>
```
### Variety of `values`
- Variables
```html
<span i18n-id="kbn.management.editIndexPattern.timeFilterHeader"
i18n-default-message="Time Filter field name: {timeFieldName}"
i18n-values="{ timeFieldName: indexPattern.timeFieldName }"></span>
```
```html
<FormattedMessage
id="kbn.management.createIndexPatternHeader"
defaultMessage="Create {indexPatternName}"
values={{
indexPatternName
}}
/>
```
- Labels and variables in tag
```html
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
i18n-default-message="This page lists every field in the {indexPatternTitle} index"
i18n-values="{ indexPatternTitle: '<strong>' + indexPattern.title + '</strong>' }"></span>
```
-----------------------------------------------------------
**BUT** we can not use tags that should be compiled:
```html
<span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
i18n-default-message="This page lists every field in the {indexPatternTitle} index"
i18n-values="{ indexPatternTitle: '<div my-directive>' + indexPattern.title + '</div>' }"></span>
```
To void injections vulnerability, `i18nId` directive doesn't compile its values.
-----------------------------------------------------------
```html
<FormattedMessage
id="kbn.management.createIndexPattern.step.indexPattern.disallowLabel"
defaultMessage="You can't use spaces or the characters {characterList}."
values={{ characterList: <strong>{characterList}</strong> }}
/>
```
```html
<FormattedMessage
id="kbn.management.settings.form.noSearchResultText"
defaultMessage="No settings found {clearSearch}"
values={{
clearSearch: (
<EuiLink onClick={clearQuery}>
<FormattedMessage
id="kbn.management.settings.form.clearNoSearchResultText"
defaultMessage="(clear search)"
/>
</EuiLink>
),
}}
/>
```
- Non-translatable text such as property name.
```html
<FormattedMessage
id="xpack.security.management.users.editUser.changePasswordUpdateKibanaTitle"
defaultMessage="After you change the password for the kibana user, you must update the {kibana}
file and restart Kibana."
values={{ kibana: 'kibana.yml' }}
/>
```
### Text with plurals
@ -258,7 +358,7 @@ The numeric input is mapped to a plural category, some subset of "zero", "one",
Here is an example of message translation depending on a plural category:
```js
```html
<span i18n-id="kbn.management.editIndexPattern.mappingConflictLabel"
i18n-default-message="{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern."
i18n-values="{ conflictFieldsLength: conflictFields.length }"></span>
@ -279,8 +379,6 @@ Splitting sentences into several keys often inadvertently presumes a grammar, a
If this group of sentences is separated its possible that the context of the `'it'` in `'close it'` will be lost.
### Unit tests
Testing React component that uses the `injectI18n` higher-order component is more complicated because `injectI18n()` creates a wrapper component around the original component.
@ -333,3 +431,26 @@ it('should render normally', async () => {
});
// ...
```
## Development steps
1. Localize label with the suitable i18n component.
2. Make sure that UI still looks correct and is functioning properly (e.g. click handler is processed, checkbox is checked/unchecked, etc.).
3. Check functionality of an element (button is clicked, checkbox is checked/unchecked, etc.).
4. Run i18n validation tool and skim through created `en.json`:
```js
node scripts/i18n_check --output ./
```
5. Run linters and type checker as you normally do.
6. Run tests.
7. Run Kibana with enabled pseudo-locale (either pass `--i18n.locale=en-xa` as a command-line argument or add it to the `kibana.yml`) and observe the text you've just localized.
If you did everything correctly, it should turn into something like this `Ĥéļļļô ŴŴôŕļļð!` assuming your text was `Hello World!`.
8. Check that CI is green.

View file

@ -1,14 +1,14 @@
# I18n
Kibana relies on several UI frameworks (React and Angular) and
Kibana relies on several UI frameworks (ReactJS and AngularJS) and
requires localization in different environments (browser and NodeJS).
Internationalization engine is framework agnostic and consumable in
all parts of Kibana (React, Angular and NodeJS). In order to simplify
all parts of Kibana (ReactJS, AngularJS and NodeJS). In order to simplify
internationalization in UI frameworks, the additional abstractions are
built around the I18n engine: `react-intl` for React and custom
components for Angular. [React-intl](https://github.com/yahoo/react-intl)
components for AngularJS. [React-intl](https://github.com/yahoo/react-intl)
is built around [intl-messageformat](https://github.com/yahoo/intl-messageformat),
so both React and Angular frameworks use the same engine and the same
so both React and AngularJS frameworks use the same engine and the same
message syntax.
## Localization files
@ -42,7 +42,7 @@ 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`
file, we should use message path like this: `'MANAGEMENT.OBJECTS.TITLE'`.
file, we should use message path like this: `'management.objects.objectsTitle'`.
Each Kibana plugin has a separate folder with translation files located at
```
@ -199,6 +199,20 @@ export const HELLO_WORLD = i18n.translate('hello.wonderful.world', {
}),
```
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 },
context: 'This is greeting message for main screen.'
});
}
```
We're also able to use all methods exposed by the i18n engine
(see [I18n engine](#i18n-engine) section above for more details).
@ -273,8 +287,21 @@ Optionally we can pass `description` 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`
In case when ReactJS component is rendered with the help of `reactDirective` AngularJS service, it's necessary to use React HOC `injectI18nProvider` to pass `intl` object to `FormattedMessage` component via context.
#### Attributes translation in React
```js
import { injectI18nProvider } from '@kbn/i18n/react';
import { Header } from './components/header';
module.directive('headerGlobalNav', (reactDirective) => {
return reactDirective(injectI18nProvider(Header));
});
```
**NOTE:** To minimize the chance of having multiple `I18nProvider` components in the React tree, try to use `injectI18nProvider` or `I18nProvider` only to wrap the topmost component that you render, e.g. the one that's passed to `reactDirective` or `ReactDOM.render`.
### Attributes translation in React
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`.
@ -324,7 +351,7 @@ class MyComponentContent extends React.Component {
<input
type="text"
placeholder={intl.formatMessage({
id: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
id: 'kbn.management.objects.searchPlaceholder',
defaultMessage: 'Search',
})}
/>
@ -335,9 +362,9 @@ class MyComponentContent extends React.Component {
export const MyComponent = injectI18n(MyComponentContent);
```
## Angular
## AngularJS
Angular wrapper has 4 entities: translation `provider`, `service`, `directive`
AngularJS wrapper has 4 entities: translation `provider`, `service`, `directive`
and `filter`. Both the directive and the filter use the translation `service`
with i18n engine under the hood.
@ -414,12 +441,13 @@ loaded automatically. After that we can use i18n directive in Angular templates:
></span>
```
In order to translate attributes in Angular we should use `i18nFilter`:
In order to translate attributes in AngularJS we should use `i18nFilter`:
```html
<input
type="text"
placeholder="{{ ::'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | i18n: {
defaultMessage: 'Search'
placeholder="{{ ::'kbn.management.objects.searchAriaLabel' | i18n: {
defaultMessage: 'Search { title } Object',
values: { title }
} }}"
>
```