1# I18n Guideline
2
3## All Localizers need to know
4
5### Message types
6
7The 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.
8
9Ids should end with:
10
11- Description (in most cases if it's `<p>` tag),
12- Title (if it's `<h1>`, `<h2>`, etc. tags),
13- Label (if it's `<label>` tag),
14- ButtonLabel (if it's `<button>` tag),
15- DropDownOptionLabel (if it'a an option),
16- Placeholder (if it's a placeholder),
17- Tooltip (if it's a tootltip),
18- AriaLabel (if it's `aria-label` tag attribute),
19- ErrorMessage (if it's an error message),
20- LinkText (if it's `<a>` tag),
21- ToggleSwitch and etc.
22
23There is one more complex case, when we have to divide a single expression into different labels.
24
25For example the message before translation looks like:
26
27  ```html
28  <p>
29      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.
30  </p>
31  ```
32
33This phrase contains a variable, which represents languages list, and a link (`Painless`). For such cases we divide the message into two parts: the main message, which contains placeholders, and additional message, which represents inner message.
34
35It is used the following message id naming structure:
361) the main message id has the type on the penultimate position, thereby identifying a divided phrase, and the last segment ends with `Detail`.
37
38```js
39{
40  'kbn.management.editIndexPattern.scripted.deprecationLangLabel.deprecationLangDetail': 'The following deprecated languages are in use: {deprecatedLangsInUse}. Support for these languages will be removed in the next major version of Kibana and Elasticsearch. Convert your scripted fields to {link} to avoid any problems.'
41}
42```
43
442) The inner message id has the type on the penultimate position and the name of the variable from the placeholder in the main message (in this case `link`) as the last segment that ends with own type.
45
46For example:
47
48```js
49{
50  'kbn.management.editIndexPattern.scripted.deprecationLangLabel.painlessLinkLabel': 'Painless'
51}
52```
53
54### Attribute with variables interpolation
55
56Messages can contain placeholders for embedding a value of a variable. For example:
57
58```js
59{
60  'kbn.management.editIndexPattern.scripted.deleteFieldLabel': "Delete scripted field '{fieldName}'?"
61  'kbn.management.editIndexPattern.scripted.noFieldLabel': "'{indexPatternTitle}' index pattern doesn't have a scripted field called '{fieldName}'"
62}
63```
64
65Mostly such placeholders have meaningful name according to the сontent.
66
67### Pluralization
68
69I18n engine supports proper plural forms. It uses the [ICU Message syntax](http://userguide.icu-project.org/formatparse/messages) to define a message that has a plural label and works for all [CLDR languages](http://cldr.unicode.org/) which have pluralization rules defined. The numeric input is mapped to a plural category, some subset of "zero", "one", "two", "few", "many", and "other" depending on the locale and the type of plural.
70
71For example:
72
73```js
74{
75  'kbn.management.createIndexPattern.step.status.successLabel.strongIndicesLabel': '{indicesLength, plural, one {# index} other {# indices}}'
76}
77```
78
79In case when `indicesLength` has value 1, the result string will be "`1 index`". In case when `indicesLength` has value 2 and more, the result string - "`2 indices`".
80
81## Best practices
82
83### Usage of appropriate component
84
85#### In ReactJS
86
87- You should use `<FormattedMessage>` most of the time.
88- In case when the string is expected (`aria-label`, `placeholder`), use `props.intl.formatmessage()` (where `intl` is  passed to `props` by `injectI18n` HOC).
89- 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.
90
91#### In AngularJS
92
93- Use `i18n` service in controllers, directives, services by injected it.
94- Use `i18nId` directive in template.
95- Use `i18n` filter in template for attribute translation.
96- In case if none of the above can not be applied, you can call JS function `i18n.translate()` from `@kbn/i18n` package.
97
98Note: Use one-time binding ("{{:: ... }}") in filters wherever it's possible to prevent unnecessary expression re-evaluation.
99
100#### In JavaScript
101
102- Use `i18n.translate()` in NodeJS or any other framework agnostic code, where `i18n` is the I18n engine from `@kbn/i18n` package.
103
104### Naming convention
105
106The 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.
107Here's a rule of id maning:
108
109`{plugin}.{area}.[{sub-area}].{element}`
110
111- 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.).
112
113    For example:
114
115  ```js
116  'kbn.management.createIndexPattern.stepTime.options.patternHeader'
117  'common.ui.indexPattern.warningLabel'
118  ```
119
120- Use camelCase for naming segments, comprising several words.
121
122- Each message id should end with a type. For example:
123
124  ```js
125  'kbn.management.editIndexPattern.createIndexButtonLabel'
126  'kbn.management.editIndexPattern.mappingConflictTitle'
127  'kbn.management.editIndexPattern.mappingConflictLabel'
128  'kbn.management.editIndexPattern.fields.filterAriaLabel'
129  'kbn.management.editIndexPattern.fields.filterPlaceholder'
130  'kbn.management.editIndexPattern.refreshTooltip'
131  'kbn.management.editIndexPattern.fields.allTypesDropDown'
132  'kbn.management.createIndexPattern.includeSystemIndicesToggleSwitch'
133  'kbn.management.editIndexPattern.wrongTypeErrorMessage'
134  'kbn.management.editIndexPattern.scripted.table.nameDescription'
135  ```
136
137- For complex messages, which are divided into several parts, use the following approach:
138  - the main message id should have the type on the penultimate position, thereby identifying a divided phrase, and the last segment should end with `Detail`,
139  - the inner message id should have the type on the penultimate position and the name of the variable from the placeholder in the main message as the last segment that ends with its own type.
140
141  For example, before the translation there was a message:
142
143  ```js
144  <strong>Success!</strong>
145  Your index pattern matches <strong>{exactMatchedIndices.length} {exactMatchedIndices.length === 1 ? 'index' : 'indices'}</strong>.
146  ```
147
148  After translation we get the following structure:
149
150  ```js
151  <FormattedMessage
152    id="kbn.management.createIndexPattern.step.status.successLabel.successDetail"
153    defaultMessage="{strongSuccess} Your index pattern matches {strongIndices}."
154    values={{
155      strongSuccess: (
156        <strong>
157          <FormattedMessage
158            id="kbn.management.createIndexPattern.step.status.successLabel.strongSuccessLabel"
159            defaultMessage="Success!"
160          />
161        </strong>),
162      strongIndices: (
163        <strong>
164          <FormattedMessage
165            id="kbn.management.createIndexPattern.step.status.successLabel.strongIndicesLabel"
166            defaultMessage="{indicesLength, plural, one {# index} other {# indices}}"
167            values={{ indicesLength: exactMatchedIndices.length }}
168          />
169        </strong>)
170    }}
171  />
172  ```
173
174### Defining type for message
175
176Each message id should end with a type of the message.
177
178| type | example message id |
179| --- | --- |
180| header | `kbn.management.createIndexPatternTitle` |
181| label | `kbn.management.createIndexPatternLabel ` |
182| button | `kbn.management.editIndexPattern.scripted.addFieldButtonLabel` |
183| drop down | `kbn.management.editIndexPattern.fields.allTypesDropDown` |
184| placeholder | `kbn.management.createIndexPattern.stepTime.options.patternPlaceholder` |
185| `aria-label` attribute | `kbn.management.editIndexPattern.removeAriaLabel` |
186| tooltip | `kbn.management.editIndexPattern.removeTooltip` |
187| error message | `kbn.management.createIndexPattern.step.invalidCharactersErrorMessage` |
188| toggleSwitch | `kbn.management.createIndexPattern.includeSystemIndicesToggleSwitch` |
189
190For example:
191
192- for header:
193
194  ```js
195  <h1>
196      <FormattedMessage
197        id="kbn.management.createIndexPatternTitle"
198        defaultMessage="Create index pattern"
199      />
200  </h1>
201  ```
202
203- for label:
204
205  ```js
206  <EuiTextColor color="subdued">
207      <FormattedMessage
208        id="kbn.management.createIndexPatternLabel"
209        defaultMessage="Kibana uses index patterns to retrieve data from Elasticsearch indices for things like visualizations."
210      />
211  </EuiTextColor>
212  ```
213
214- for button:
215
216  ```js
217
218  <EuiButton data-test-subj="addScriptedFieldLink" href={addScriptedFieldUrl}>
219       <FormattedMessage id="kbn.management.editIndexPattern.scripted.addFieldButtonLabel" defaultMessage="Add scripted field"/>
220  </EuiButton>
221  ```
222
223- for dropDown:
224
225  ```js
226  <select ng-model="indexedFieldTypeFilter" ng-options="o for o in indexedFieldTypes">
227      <option value=""
228          i18n-id="kbn.management.editIndexPattern.fields.allTypesDropDown"
229          i18n-default-message="All field types"></option>
230  </select>
231  ```
232
233- for placeholder:
234
235  ```js
236  <EuiFieldText
237      name="indexPatternId"
238      placeholder={intl.formatMessage({
239        id: 'kbn.management.createIndexPattern.stepTime.options.patternPlaceholder',
240        defaultMessage: 'custom-index-pattern-id' })}
241  />
242  ```
243
244- for `aria-label` attribute and tooltip
245
246  ```js
247  <button
248      aria-label="{{ ::'kbn.management.editIndexPattern.removeAriaLabel' | i18n: {defaultMessage: 'Remove index pattern'} }}"
249      tooltip="{{ ::'kbn.management.editIndexPattern.removeTooltip' | i18n: {defaultMessage: 'Remove index pattern'} }}"
250      >
251  </button>
252  ```
253
254- for errorMessage:
255
256  ```js
257  errors.push(
258      intl.formatMessage(
259              {
260                  id: 'kbn.management.createIndexPattern.step.invalidCharactersErrorMessage',
261                  defaultMessage: 'An index pattern cannot contain spaces or the characters: {characterList}'
262              },
263              { characterList }
264      ));
265  ```
266
267- for toggleSwitch
268
269  ```js
270  <EuiSwitch
271      label={<FormattedMessage
272        id="kbn.management.createIndexPattern.includeSystemIndicesToggleSwitch"
273        defaultMessage="Include system indices"
274      />}
275  />
276  ```
277
278### Variety of `values`
279
280- Variables
281
282  ```html
283  <span i18n-id="kbn.management.editIndexPattern.timeFilterHeader"
284    i18n-default-message="Time Filter field name: {timeFieldName}"
285    i18n-values="{ timeFieldName: indexPattern.timeFieldName }"></span>
286  ```
287
288  ```html
289  <FormattedMessage
290    id="kbn.management.createIndexPatternHeader"
291    defaultMessage="Create {indexPatternName}"
292    values={{
293      indexPatternName
294    }}
295  />
296  ```
297
298- Labels and variables in tag
299
300  ```html
301  <span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
302    i18n-default-message="This page lists every field in the {indexPatternTitle} index"
303    i18n-values="{ indexPatternTitle: '<strong>' + indexPattern.title + '</strong>' }"></span>
304  ```
305
306  -----------------------------------------------------------
307  **BUT** we can not use tags that should be compiled:
308
309  ```html
310  <span i18n-id="kbn.management.editIndexPattern.timeFilterLabel.timeFilterDetail"
311    i18n-default-message="This page lists every field in the {indexPatternTitle} index"
312    i18n-values="{ indexPatternTitle: '<div my-directive>' + indexPattern.title + '</div>' }"></span>
313  ```
314
315  To void injections vulnerability, `i18nId` directive doesn't compile its values.
316
317  -----------------------------------------------------------
318
319  ```html
320  <FormattedMessage
321    id="kbn.management.createIndexPattern.step.indexPattern.disallowLabel"
322    defaultMessage="You can't use spaces or the characters {characterList}."
323    values={{ characterList: <strong>{characterList}</strong> }}
324  />
325  ```
326
327  ```html
328  <FormattedMessage
329    id="kbn.management.settings.form.noSearchResultText"
330    defaultMessage="No settings found {clearSearch}"
331    values={{
332      clearSearch: (
333        <EuiLink onClick={clearQuery}>
334          <FormattedMessage
335            id="kbn.management.settings.form.clearNoSearchResultText"
336            defaultMessage="(clear search)"
337          />
338        </EuiLink>
339      ),
340    }}
341  />
342  ```
343
344- Non-translatable text such as property name.
345
346  ```html
347  <FormattedMessage
348    id="xpack.security.management.users.editUser.changePasswordUpdateKibanaTitle"
349    defaultMessage="After you change the password for the kibana user, you must update the {kibana}
350    file and restart Kibana."
351    values={{ kibana: 'kibana.yml' }}
352  />
353  ```
354
355### Text with plurals
356
357The numeric input is mapped to a plural category, some subset of "zero", "one", "two", "few", "many", and "other" depending on the locale and the type of plural. There are languages with multiple plural forms [Language Plural Rules](http://www.unicode.org/cldr/charts/latest/supplemental/language_plural_rules.html).
358
359Here is an example of message translation depending on a plural category:
360
361```html
362<span i18n-id="kbn.management.editIndexPattern.mappingConflictLabel"
363      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."
364      i18n-values="{ conflictFieldsLength: conflictFields.length }"></span>
365```
366
367When `conflictFieldsLength` equals 1, the result string will be `"A field is defined as several types (string, integer, etc) across the indices that match this pattern."`. In cases when `conflictFieldsLength` has value of 2 or more, the result string - `"2 fields are defined as several types (string, integer, etc) across the indices that match this pattern."`.
368
369### Splitting
370
371Splitting sentences into several keys often inadvertently presumes a grammar, a sentence structure, and such composite strings are often very difficult to translate.
372
373- Do not divide a single sentence into different labels unless you have absolutely no other choice.
374- Do not divide sentences that belong together into separate labels.
375
376  For example:
377
378  `The following dialogue box indicates progress. You can close it and the process will continue to run in the background.`
379
380  If this group of sentences is separated it’s possible that the context of the `'it'` in `'close it'` will be lost.
381
382### Unit tests
383
384Testing React component that uses the `injectI18n` higher-order component is more complicated because `injectI18n()` creates a wrapper component around the original component.
385
386With shallow rendering only top level component is rendered, that is a wrapper itself, not the original component. Since we want to test the rendering of the original component, we need to access it via the wrapper's `WrappedComponent` property. Its value will be the component we passed into `injectI18n()`.
387
388When testing such component, use the `shallowWithIntl` helper function defined in `test_utils/enzyme_helpers` and pass the component's `WrappedComponent` property to render the wrapped component. This will shallow render the component with Enzyme and inject the necessary context and props to use the `intl` mock defined in `test_utils/mocks/intl`.
389
390Use the `mountWithIntl` helper function to mount render the component.
391
392For example, there is a component that is wrapped by `injectI18n`, like in the `AddFilter` component:
393
394```js
395// ...
396export const AddFilter = injectI18n(
397  class AddFilterUi extends Component {
398  // ...
399    render() {
400      const { filter } = this.state;
401      return (
402        <EuiFlexGroup>
403          <EuiFlexItem grow={10}>
404            <EuiFieldText
405              fullWidth
406              value={filter}
407              onChange={e => this.setState({ filter: e.target.value.trim() })}
408              placeholder={this.props.intl.formatMessage({
409                id: 'kbn.management.indexPattern.edit.source.placeholder',
410                defaultMessage: 'source filter, accepts wildcards (e.g., `user*` to filter fields starting with \'user\')'
411              })}
412            />
413          </EuiFlexItem>
414        </EuiFlexGroup>
415      );
416    }
417  }
418);
419```
420
421To test the `AddFilter` component it is needed to render its `WrappedComponent` property using `shallowWithIntl` function to pass `intl` object into the `props`.
422
423```js
424// ...
425it('should render normally', async () => {
426    const component = shallowWithIntl(
427      <AddFilter.WrappedComponent onAddFilter={() => {}}/>
428    );
429
430    expect(component).toMatchSnapshot();
431});
432// ...
433```
434
435## Development steps
436
4371. Localize label with the suitable i18n component.
438
4392. Make sure that UI still looks correct and is functioning properly (e.g. click handler is processed, checkbox is checked/unchecked, etc.).
440
4413. Check functionality of an element (button is clicked, checkbox is checked/unchecked, etc.).
442
4434. Run i18n validation/extraction tools and skim through created `en.json`:
444    ```bash
445    $ node scripts/i18n_check --ignore-missing
446    $ node scripts/i18n_extract --output-dir ./
447    ```
448
4495. Run linters and type checker as you normally do.
450
4516. Run tests.
452
4537. 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.
454
455    If you did everything correctly, it should turn into something like this `Ĥéļļļô ŴŴôŕļļð!` assuming your text was `Hello World!`.
456
4578. Check that CI is green.
458