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- `.markdown` (if it's markdown)
23
24There is one more complex case, when we have to divide a single expression into different labels.
25
26For example the message before translation looks like:
27
28  ```html
29  <p>
30      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.
31  </p>
32  ```
33
34This 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.
35
36It is used the following message id naming structure:
371) the main message id has the type on the penultimate position, thereby identifying a divided phrase, and the last segment ends with `Detail`.
38
39```js
40{
41  '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.'
42}
43```
44
452) 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.
46
47For example:
48
49```js
50{
51  'kbn.management.editIndexPattern.scripted.deprecationLangLabel.painlessLinkLabel': 'Painless'
52}
53```
54
55### Attribute with variables interpolation
56
57Messages can contain placeholders for embedding a value of a variable. For example:
58
59```js
60{
61  'kbn.management.editIndexPattern.scripted.deleteFieldLabel': "Delete scripted field '{fieldName}'?"
62  'kbn.management.editIndexPattern.scripted.noFieldLabel': "'{indexPatternTitle}' index pattern doesn't have a scripted field called '{fieldName}'"
63}
64```
65
66Mostly such placeholders have meaningful name according to the content.
67
68### Pluralization
69
70I18n 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.
71
72For example:
73
74```js
75{
76  'kbn.management.createIndexPattern.step.status.successLabel.strongIndicesLabel': '{indicesLength, plural, one {# index} other {# indices}}'
77}
78```
79
80In 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`".
81
82## Best practices
83
84### Usage of appropriate component
85
86#### In ReactJS
87
88The long term plan is to rely on using `FormattedMessage` and `i18n.translate()` by statically importing `i18n` from the `@kbn/i18n` package. **Avoid using `injectI18n` and use `i18n.translate()` instead.**
89
90- You should use `<FormattedMessage>` most of the time.
91- In the case where the string is expected (`aria-label`, `placeholder`), Call JS function `i18n.translate()` from the`@kbn/i18n` package.
92
93Currently, we support the following ReactJS `i18n` tools, but they will be removed in future releases:
94- Usage of `props.intl.formatmessage()` (where `intl` is  passed to `props` by `injectI18n` HOC).
95
96#### In JavaScript
97
98- Use `i18n.translate()` in NodeJS or any other framework agnostic code, where `i18n` is the I18n engine from `@kbn/i18n` package.
99
100### Naming convention
101
102The 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.
103Here's a rule of id naming:
104
105`{plugin}.{area}.[{sub-area}].{element}`
106
107- 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.).
108
109    For example:
110
111  ```js
112  'kbn.management.createIndexPattern.stepTime.options.patternHeader'
113  'common.ui.indexPattern.warningLabel'
114  ```
115
116- Use camelCase for naming segments, comprising several words.
117
118- Each message id should end with a type. For example:
119
120  ```js
121  'kbn.management.editIndexPattern.createIndexButtonLabel'
122  'kbn.management.editIndexPattern.mappingConflictTitle'
123  'kbn.management.editIndexPattern.mappingConflictLabel'
124  'kbn.management.editIndexPattern.fields.filterAriaLabel'
125  'kbn.management.editIndexPattern.fields.filterPlaceholder'
126  'kbn.management.editIndexPattern.refreshTooltip'
127  'kbn.management.editIndexPattern.fields.allTypesDropDown'
128  'kbn.management.createIndexPattern.includeSystemIndicesToggleSwitch'
129  'kbn.management.editIndexPattern.wrongTypeErrorMessage'
130  'kbn.management.editIndexPattern.scripted.table.nameDescription'
131  'xpack.lens.formulaDocumentation.filterRatioDescription.markdown'
132  ```
133
134- For complex messages, which are divided into several parts, use the following approach:
135  - 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`,
136  - 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.
137
138  For example, before the translation there was a message:
139
140  ```js
141  <strong>Success!</strong>
142  Your index pattern matches <strong>{exactMatchedIndices.length} {exactMatchedIndices.length === 1 ? 'index' : 'indices'}</strong>.
143  ```
144
145  After translation we get the following structure:
146
147  ```js
148  <FormattedMessage
149    id="kbn.management.createIndexPattern.step.status.successLabel.successDetail"
150    defaultMessage="{strongSuccess} Your index pattern matches {strongIndices}."
151    values={{
152      strongSuccess: (
153        <strong>
154          <FormattedMessage
155            id="kbn.management.createIndexPattern.step.status.successLabel.strongSuccessLabel"
156            defaultMessage="Success!"
157          />
158        </strong>),
159      strongIndices: (
160        <strong>
161          <FormattedMessage
162            id="kbn.management.createIndexPattern.step.status.successLabel.strongIndicesLabel"
163            defaultMessage="{indicesLength, plural, one {# index} other {# indices}}"
164            values={{ indicesLength: exactMatchedIndices.length }}
165          />
166        </strong>)
167    }}
168  />
169  ```
170
171### Defining type for message
172
173Each message id should end with a type of the message.
174
175| type | example message id |
176| --- | --- |
177| header | `kbn.management.createIndexPatternTitle` |
178| label | `kbn.management.createIndexPatternLabel ` |
179| button | `kbn.management.editIndexPattern.scripted.addFieldButtonLabel` |
180| drop down | `kbn.management.editIndexPattern.fields.allTypesDropDown` |
181| placeholder | `kbn.management.createIndexPattern.stepTime.options.patternPlaceholder` |
182| `aria-label` attribute | `kbn.management.editIndexPattern.removeAriaLabel` |
183| tooltip | `kbn.management.editIndexPattern.removeTooltip` |
184| error message | `kbn.management.createIndexPattern.step.invalidCharactersErrorMessage` |
185| toggleSwitch | `kbn.management.createIndexPattern.includeSystemIndicesToggleSwitch` |
186| markdown | `xpack.lens.formulaDocumentation.filterRatioDescription.markdown` |
187
188For example:
189
190- for header:
191
192  ```js
193  <h1>
194      <FormattedMessage
195        id="kbn.management.createIndexPatternTitle"
196        defaultMessage="Create index pattern"
197      />
198  </h1>
199  ```
200
201- for label:
202
203  ```js
204  <EuiTextColor color="subdued">
205      <FormattedMessage
206        id="kbn.management.createIndexPatternLabel"
207        defaultMessage="Kibana uses index patterns to retrieve data from Elasticsearch indices for things like visualizations."
208      />
209  </EuiTextColor>
210  ```
211
212- for button:
213
214  ```js
215  <EuiButton data-test-subj="addScriptedFieldLink" href={addScriptedFieldUrl}>
216       <FormattedMessage id="kbn.management.editIndexPattern.scripted.addFieldButtonLabel" defaultMessage="Add scripted field"/>
217  </EuiButton>
218  ```
219
220- for dropDown:
221
222  ```js
223    <option value={
224      i18n.translate('kbn.management.editIndexPattern.fields.allTypesDropDown', {
225        defaultMessage: 'All field types',
226      })
227    }
228  ```
229
230- for placeholder:
231
232  ```js
233  <EuiFieldText
234      name="indexPatternId"
235      placeholder={intl.formatMessage({
236        id: 'kbn.management.createIndexPattern.stepTime.options.patternPlaceholder',
237        defaultMessage: 'custom-index-pattern-id' })}
238  />
239  ```
240
241- for `aria-label` attribute and tooltip
242
243  ```js
244  <button
245      aria-label="{{ ::'kbn.management.editIndexPattern.removeAriaLabel' | i18n: {defaultMessage: 'Remove index pattern'} }}"
246      tooltip="{{ ::'kbn.management.editIndexPattern.removeTooltip' | i18n: {defaultMessage: 'Remove index pattern'} }}"
247      >
248  </button>
249  ```
250
251- for errorMessage:
252
253  ```js
254  errors.push(
255      intl.formatMessage(
256              {
257                  id: 'kbn.management.createIndexPattern.step.invalidCharactersErrorMessage',
258                  defaultMessage: 'An index pattern cannot contain spaces or the characters: {characterList}'
259              },
260              { characterList }
261      ));
262  ```
263
264- for toggleSwitch
265
266  ```js
267  <EuiSwitch
268      label={<FormattedMessage
269        id="kbn.management.createIndexPattern.includeSystemIndicesToggleSwitch"
270        defaultMessage="Include system indices"
271      />}
272  />
273  ```
274
275- for markdown
276  ```js
277  import { Markdown } from '@elastic/eui';
278
279  <Markdown
280      markdown={
281        i18n.translate('xpack.lens.formulaDocumentation.filterRatioDescription.markdown', {
282        defaultMessage: `### Filter ratio:
283
284          Use \`kql=''\` to filter one set of documents and compare it to other documents within the same grouping.
285          For example, to see how the error rate changes over time:
286
287          \`\`\`
288          count(kql='response.status_code > 400') / count()
289          \`\`\`
290        `,
291        })
292      }
293  />
294  ```
295
296### Variety of `values`
297
298- Variables
299
300  ```html
301  <FormattedMessage
302    id="kbn.management.createIndexPatternHeader"
303    defaultMessage="Create {indexPatternName}"
304    values={{
305      indexPatternName
306    }}
307  />
308  ```
309
310- Labels and variables in tag
311
312  ```html
313  <FormattedMessage
314    id="kbn.management.createIndexPattern.step.indexPattern.disallowLabel"
315    defaultMessage="You can't use spaces or the characters {characterList}."
316    values={{ characterList: <strong>{characterList}</strong> }}
317  />
318  ```
319
320  ```html
321  <FormattedMessage
322    id="kbn.management.settings.form.noSearchResultText"
323    defaultMessage="No settings found {clearSearch}"
324    values={{
325      clearSearch: (
326        <EuiLink onClick={clearQuery}>
327          <FormattedMessage
328            id="kbn.management.settings.form.clearNoSearchResultText"
329            defaultMessage="(clear search)"
330          />
331        </EuiLink>
332      ),
333    }}
334  />
335  ```
336
337- Non-translatable text such as property name.
338
339  ```html
340  <FormattedMessage
341    id="xpack.security.management.users.editUser.changePasswordUpdateKibanaTitle"
342    defaultMessage="After you change the password for the kibana user, you must update the {kibana}
343    file and restart Kibana."
344    values={{ kibana: 'kibana.yml' }}
345  />
346  ```
347
348### Text with plurals
349
350The 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).
351
352Here is an example of message translation depending on a plural category:
353
354```jsx
355  <FormattedMessage
356    id="kbn.management.editIndexPattern.mappingConflictLabel"
357    defaultMessage="{conflictFieldsLength, plural, one {A field is} other {# fields are}} defined as several types (string, integer, etc) across the indices that match this pattern."
358    values={{ conflictFieldsLength: conflictFields.length }}
359  />
360```
361
362When `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."`.
363
364### Text with markdown
365
366There is some support for using markdown and you can use any of the following syntax:
367
368#### Headers
369
370```md
371# This is an <h1> tag
372## This is an <h2> tag
373###### This is an <h6> tag
374```
375
376#### Emphasis
377
378```md
379*This text will be italic*
380_This will also be italic_
381
382**This text will be bold**
383__This will also be bold__
384
385_You **can** combine them_
386```
387
388#### Lists
389  ##### Unordered
390
391```md
392* Item 1
393* Item 2
394  * Item 2a
395  * Item 2b
396```
397  ##### Ordered
398
399```md
4001. Item 1
4011. Item 2
4021. Item 3
403  1. Item 3a
404  1. Item 3b
405```
406#### Images
407
408```md
409![Github Logo](/images/logo.png)
410Format: ![Alt Text](url)
411```
412
413#### Links
414
415```md
416http://github.com - automatic!
417[GitHub](http://github.com)
418```
419
420#### Blockquotes
421
422```md
423As Kanye West said:
424
425> We're living the future so
426> the present is our past.
427```
428#### Code Blocks
429
430```md
431var a = 13;
432```
433
434#### Inline code
435
436```md
437I think you should use an
438`<addr>` element here instead
439```
440### Splitting
441
442Splitting sentences into several keys often inadvertently presumes a grammar, a sentence structure, and such composite strings are often very difficult to translate.
443
444- Do not divide a single sentence into different labels unless you have absolutely no other choice.
445- Do not divide sentences that belong together into separate labels.
446
447  For example:
448
449  `The following dialogue box indicates progress. You can close it and the process will continue to run in the background.`
450
451  If this group of sentences is separated it’s possible that the context of the `'it'` in `'close it'` will be lost.
452
453### Large paragraphs
454
455Try to avoid using large paragraphs of text. They are difficult to maintain and often need small changes when the information becomes out of date.
456
457If you have no other choice, you can split paragraphs into a _few_ i18n chunks. Chunks should be split at logical points to ensure they contain enough context to be intelligible on their own.
458
459### Unit tests
460
461Testing React component that uses the `injectI18n` higher-order component is more complicated because `injectI18n()` creates a wrapper component around the original component.
462
463With 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()`.
464
465When testing such component, use the `shallowWithIntl` helper function defined in `@kbn/test/jest` 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`.
466
467Use the `mountWithIntl` helper function to mount render the component.
468
469For example, there is a component that is wrapped by `injectI18n`, like in the `AddFilter` component:
470
471```js
472// ...
473export const AddFilter = injectI18n(
474  class AddFilterUi extends Component {
475  // ...
476    render() {
477      const { filter } = this.state;
478      return (
479        <EuiFlexGroup>
480          <EuiFlexItem grow={10}>
481            <EuiFieldText
482              fullWidth
483              value={filter}
484              onChange={e => this.setState({ filter: e.target.value.trim() })}
485              placeholder={this.props.intl.formatMessage({
486                id: 'kbn.management.indexPattern.edit.source.placeholder',
487                defaultMessage: 'source filter, accepts wildcards (e.g., `user*` to filter fields starting with \'user\')'
488              })}
489            />
490          </EuiFlexItem>
491        </EuiFlexGroup>
492      );
493    }
494  }
495);
496```
497
498To test the `AddFilter` component it is needed to render its `WrappedComponent` property using `shallowWithIntl` function to pass `intl` object into the `props`.
499
500```js
501// ...
502it('should render normally', async () => {
503    const component = shallowWithIntl(
504      <AddFilter.WrappedComponent onAddFilter={() => {}}/>
505    );
506
507    expect(component).toMatchSnapshot();
508});
509// ...
510```
511
512## Development steps
513
5141. Localize label with the suitable i18n component.
515
5162. Make sure that UI still looks correct and is functioning properly (e.g. click handler is processed, checkbox is checked/unchecked, etc.).
517
5183. Check functionality of an element (button is clicked, checkbox is checked/unchecked, etc.).
519
5204. Run i18n validation/extraction tools and skim through created `en.json`:
521    ```bash
522    $ node scripts/i18n_check --ignore-missing
523    $ node scripts/i18n_extract --output-dir ./
524    ```
525
5265. Run linters and type checker as you normally do.
527
5286. Run tests.
529
5307. 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.
531
532    If you did everything correctly, it should turn into something like this `Ĥéļļļô ŴŴôŕļļð!` assuming your text was `Hello World!`.
533
5348. Check that CI is green.
535