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