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