Add draft implementation of I18n engine (#19555)

* Add draft implementation of I18n engine

* Add i18n loader

* kbn-i18n refactoring

* Fix react i18n context and update doc

* i18n engine refactoring

* Fix locales data loading and add more jsdoc comments

* Fix verify_translations task

* I18n tests refactoring

* Add build scripts to kbn-i18n package

* Fix some bugs

* Move uiI18nMixin into ui_i18n folder

* Add 'browser' field to kbn-i18n package.json

* Get rid of "showError" method

* Make i18n and i18nLoader a singleton object

* Add default locale as fallback if translation files were not registered

* Update yarn.lock

* kbn-i18n fix

* Add default formats

* Try to fix build

* Add more examples into kbn-i18n/README.md

* kbn-i18n fix

* Fix app_bootstrap tests

* Add links to issues in TODO comments
This commit is contained in:
Maxim Tolochko 2018-06-28 12:38:39 +03:00 committed by GitHub
parent 3ea4c3e0bc
commit f522b31ac9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 3478 additions and 566 deletions

View file

@ -31,6 +31,7 @@ module.exports = {
'packages/kbn-pm/**/*',
'packages/kbn-es/**/*',
'packages/kbn-datemath/**/*',
'packages/kbn-i18n/**/*',
'packages/kbn-dev-utils/**/*',
'packages/kbn-plugin-helpers/**/*',
'packages/kbn-plugin-generator/**/*',

View file

@ -83,11 +83,11 @@
"@elastic/ui-ace": "0.2.3",
"@kbn/babel-preset": "link:packages/kbn-babel-preset",
"@kbn/datemath": "link:packages/kbn-datemath",
"@kbn/i18n": "link:packages/kbn-i18n",
"@kbn/pm": "link:packages/kbn-pm",
"@kbn/test-subj-selector": "link:packages/kbn-test-subj-selector",
"@kbn/ui-framework": "link:packages/kbn-ui-framework",
"JSONStream": "1.1.1",
"accept-language-parser": "1.2.0",
"angular": "1.6.9",
"angular-aria": "1.6.6",
"angular-elastic": "2.5.0",

View file

@ -0,0 +1,10 @@
{
"env": {
"web": {
"presets": ["@kbn/babel-preset/webpack_preset"]
},
"node": {
"presets": ["@kbn/babel-preset/node_preset"]
}
}
}

View file

@ -98,6 +98,8 @@ data to UI frameworks and provides methods for the direct translation.
Here is the public API exposed by this engine:
- `addMessages(messages: Map<string, string>, [locale: string])` - provides a way to register
translations with the engine
- `getMessages()` - returns messages for the current language
- `setLocale(locale: string)` - tells the engine which language to use by given
language key
@ -105,9 +107,14 @@ language key
- `setDefaultLocale(locale: string)` - tells the library which language to fallback
when missing translations
- `getDefaultLocale()` - returns the default locale
- `defineFormats(formats: object)` - supplies a set of options to the underlying formatter.
- `setFormats(formats: object)` - supplies a set of options to the underlying formatter.
For the detailed explanation, see the section below
- `translate(id: string, [{values: object, defaultMessage: string}])` translate message by id
- `getFormats()` - returns current formats
- `getRegisteredLocales()` - returns array of locales having translations
- `translate(id: string, [{values: object, defaultMessage: string, context: string}])`
translate message by id. `context` is optional context comment that will be extracted
by i18n tools and added as a comment next to translation message at `defaultMessages.json`.
- `init(messages: Map<string, string>)` - initializes the engine
#### I18n engine internals
@ -179,28 +186,22 @@ React Intl uses the provider pattern to scope an i18n context to a tree of compo
are able to use `FormattedMessage` component in order to translate messages.
`IntlProvider` should wrap react app's root component (inside each react render method).
In order to translate messages we need to pass them into the `IntlProvider`
from I18n engine:
In order to translate messages we need to use `I18nProvider` component that
uses I18n engine under the hood:
```js
import React from 'react';
import ReactDOM from 'react-dom';
import { ReactI18n } from '@kbn/i18n';
import i18n from 'kbn-i18n';
import { IntlProvider } from 'ui/i18n/react-intl';
const locale = i18n.getLocale();
const messages = i18n.getMessages();
const { I18nProvider } = ReactI18n;
ReactDOM.render(
<IntlProvider
locale={locale}
messages={messages}
>
<I18nProvider>
<RootComponent>
...
</RootComponent>
</IntlProvider>,
</I18nProvider>,
document.getElementById('container')
);
```
@ -208,8 +209,9 @@ ReactDOM.render(
After that we can use `FormattedMessage` components inside `RootComponent`:
```js
import React, { Component } from 'react';
import { ReactI18n } from '@kbn/i18n';
import { FormattedMessage } from 'ui/i18n/react-intl';
const { FormattedMessage } = ReactI18n;
class RootComponent extends Component {
constructor(props) {
@ -244,6 +246,40 @@ class RootComponent extends Component {
}
```
Optionally we can pass `context` 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`
#### Attributes translation in React
React wrapper provides an API to inject the imperative formatting API into a React
component by using render callback pattern. 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 components into `I18nContext` component. The child of this component
should be a function that takes `intl` object into parameters:
```js
import React from 'react';
import { ReactI18n } from '@kbn/i18n';
const { I18nContext } = ReactI18n;
const MyComponent = () => (
<I18nContext>
{intl => (
<input
type="text"
placeholder={intl.formatMessage({
id: 'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER',
defaultMessage: 'Search',
})}
/>
)}
</I18nContext>
);
```
## Angular
Angular wrapper has 4 entities: translation `provider`, `service`, `directive`
@ -260,15 +296,19 @@ language key
- `setDefaultLocale(locale: string)` - tells the library which language to fallback
when missing translations
- `getDefaultLocale()` - returns the default locale
- `defineFormats(formats: object)` - supplies a set of options to the underlying formatter
- `setFormats(formats: object)` - supplies a set of options to the underlying formatter
- `getFormats()` - returns current formats
- `getRegisteredLocales()` - returns array of locales having translations
- `init(messages: Map<string, string>)` - initializes the engine
The translation `service` provides only one method:
- `translate(id: string, [{values: object, defaultMessage: string}])` translate message by id
- `i18n(id: string, [{values: object, defaultMessage: string, context: string }])`
translate message by id
The translation `filter` is used for attributes translation and has
the following syntax:
```
{{'translationId' | i18n[:{ values: object, defaultMessage: string }]}}
{{'translationId' | i18n[:{ values: object, defaultMessage: string, context: string }]}}
```
Where:
@ -276,6 +316,8 @@ Where:
- `values` - values to pass into translation
- `defaultMessage` - will be used unless translation was successful (the final
fallback in english, will be used for generating `en.json`)
- `context` - optional context comment that will be extracted by i18n tools
and added as a comment next to translation message at `defaultMessages.json`
The translation `directive` has the following syntax:
```html
@ -283,6 +325,7 @@ The translation `directive` has the following syntax:
i18n-id="{string}"
[i18n-values="{object}"]
[i18n-default-message="{string}"]
[i18n-context="{string}"]
></ANY>
```
@ -290,21 +333,11 @@ Where:
- `i18n-id` - translation id to be translated
- `i18n-values` - values to pass into translation
- `i18n-default-message` - will be used unless translation was successful
- `i18n-context` - optional context comment that will be extracted by i18n tools
and added as a comment next to translation message at `defaultMessages.json`
In order to initialize the translation service, we need to pass locale and
localization messages from I18n engine into the `i18nProvider`:
```js
import { uiModules } from 'ui/modules';
import i18n from 'kbn-i18n';
uiModules.get('kibana').config(function (i18nProvider) {
i18nProvider.addMessages(i18n.getMessages());
i18nProvider.setLocale(i18n.getLocale());
});
```
After that we can use i18n directive in Angular templates:
Angular `I18n` module is placed into `autoload` module, so it will be
loaded automatically. After that we can use i18n directive in Angular templates:
```html
<span
i18n-id="welcome"
@ -312,6 +345,16 @@ After that we can use i18n directive in Angular templates:
></span>
```
In order to translate attributes in Angular we should use `i18nFilter`:
```html
<input
type="text"
placeholder="{{'KIBANA-MANAGEMENT-OBJECTS-SEARCH_PLACEHOLDER' | i18n: {
defaultMessage: 'Search'
} }}"
>
```
## Node.JS
`Intl-messageformat` package assumes that the
@ -320,10 +363,13 @@ global object exists in the runtime. `Intl` is present in all modern
browsers and Node.js 0.10+. In order to load i18n engine
in Node.js we should simply `import` this module (in Node.js, the
[data](https://github.com/yahoo/intl-messageformat/tree/master/dist/locale-data)
for all 200+ languages is loaded along with the library):
for all 200+ languages is loaded along with the library) and pass the translation
messages into `init` method:
```js
import i18n from 'kbn-i18n';
import { i18n } from '@kbn/i18n';
i18n.init(messages);
```
After that we are able to use all methods exposed by the i18n engine

View file

@ -0,0 +1,32 @@
{
"name": "@kbn/i18n",
"browser": "./target/web/browser.js",
"main": "./target/node/index.js",
"module": "./src/index.js",
"version": "1.0.0",
"license": "Apache-2.0",
"private": true,
"scripts": {
"build": "yarn build:web && yarn build:node",
"build:web": "cross-env BABEL_ENV=web babel src --out-dir target/web",
"build:node": "cross-env BABEL_ENV=node babel src --out-dir target/node",
"kbn:bootstrap": "yarn build",
"kbn:watch": "yarn build --watch"
},
"devDependencies": {
"@kbn/babel-preset": "link:../kbn-babel-preset",
"@kbn/dev-utils": "link:../kbn-dev-utils",
"babel-cli": "^6.26.0",
"cross-env": "^5.2.0"
},
"dependencies": {
"accept-language-parser": "^1.5.0",
"intl-format-cache": "^2.1.0",
"intl-messageformat": "^2.2.0",
"intl-relativeformat": "^2.1.0",
"json5": "^1.0.1",
"prop-types": "^15.5.8",
"react": "^16.3.0",
"react-intl": "^2.4.0"
}
}

View file

@ -0,0 +1,43 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export function i18nDirective(i18n) {
return {
restrict: 'A',
scope: {
id: '@i18nId',
defaultMessage: '@i18nDefaultMessage',
values: '=i18nValues',
},
link: function($scope, $element) {
$scope.$watchGroup(['id', 'defaultMessage', 'values'], function([
id,
defaultMessage = '',
values = {},
]) {
$element.html(
i18n(id, {
values,
defaultMessage,
})
);
});
},
};
}

27
packages/kbn-i18n/src/angular/filter.js vendored Normal file
View file

@ -0,0 +1,27 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export function i18nFilter(i18n) {
return function(id, { defaultMessage = '', values = {} } = {}) {
return i18n(id, {
values,
defaultMessage,
});
};
}

22
packages/kbn-i18n/src/angular/index.js vendored Normal file
View file

@ -0,0 +1,22 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export { i18nProvider } from './provider';
export { i18nFilter } from './filter';
export { i18nDirective } from './directive';

View file

@ -0,0 +1,34 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as i18n from '../core/i18n';
export function i18nProvider() {
this.addMessages = i18n.addMessages;
this.getMessages = i18n.getMessages;
this.setLocale = i18n.setLocale;
this.getLocale = i18n.getLocale;
this.setDefaultLocale = i18n.setDefaultLocale;
this.getDefaultLocale = i18n.getDefaultLocale;
this.setFormats = i18n.setFormats;
this.getFormats = i18n.getFormats;
this.getRegisteredLocales = i18n.getRegisteredLocales;
this.init = i18n.init;
this.$get = () => i18n.translate;
}

View file

@ -0,0 +1,28 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as angular from './angular';
import * as react from './react';
import * as i18nCore from './core/i18n';
export { formats } from './core/formats';
export const AngularI18n = angular;
export const ReactI18n = react;
export const i18n = i18nCore;

View file

@ -0,0 +1,86 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
* Default format options used for "en" locale.
* These are used when constructing the internal Intl.NumberFormat
* (`number` formatter) and Intl.DateTimeFormat (`date` and `time` formatters) instances.
* The value of each parameter of `number` formatter is options object which is
* described in `options` section of [NumberFormat constructor].
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat}
* The value of each parameter of `date` and `time` formatters is options object which is
* described in `options` section of [DateTimeFormat constructor].
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat}
*/
export const formats = {
number: {
currency: {
style: 'currency',
},
percent: {
style: 'percent',
},
},
date: {
short: {
month: 'numeric',
day: 'numeric',
year: '2-digit',
},
medium: {
month: 'short',
day: 'numeric',
year: 'numeric',
},
long: {
month: 'long',
day: 'numeric',
year: 'numeric',
},
full: {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
},
},
time: {
short: {
hour: 'numeric',
minute: 'numeric',
},
medium: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
},
long: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZoneName: 'short',
},
full: {
hour: 'numeric',
minute: 'numeric',
second: 'numeric',
timeZoneName: 'short',
},
},
};

View file

@ -0,0 +1,49 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export const isString = value => typeof value === 'string';
export const isObject = value => typeof value === 'object' && value !== null;
export const hasValues = values => Object.keys(values).length > 0;
export const unique = (arr = []) => [...new Set(arr)];
const merge = (a, b) =>
unique([...Object.keys(a), ...Object.keys(b)]).reduce((acc, key) => {
if (
isObject(a[key]) &&
isObject(b[key]) &&
!Array.isArray(a[key]) &&
!Array.isArray(b[key])
) {
return {
...acc,
[key]: merge(a[key], b[key]),
};
}
return {
...acc,
[key]: b[key] === undefined ? a[key] : b[key],
};
}, {});
export const mergeAll = (...sources) =>
sources.filter(isObject).reduce((acc, source) => merge(acc, source));

View file

@ -0,0 +1,243 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
@typedef Messages - messages tree, where leafs are translated strings
@type {object<string, object>}
@property {string} [locale] - locale of the messages
@property {object} [formats] - set of options to the underlying formatter
*/
import IntlMessageFormat from 'intl-messageformat';
import IntlRelativeFormat from 'intl-relativeformat';
import memoizeIntlConstructor from 'intl-format-cache';
import { isString, isObject, hasValues, mergeAll } from './helper';
import { formats as EN_FORMATS } from './formats';
// Add all locale data to `IntlMessageFormat`.
import './locales';
const EN_LOCALE = 'en';
const LOCALE_DELIMITER = '-';
const messages = {};
const getMessageFormat = memoizeIntlConstructor(IntlMessageFormat);
let defaultLocale = EN_LOCALE;
let currentLocale = EN_LOCALE;
let formats = EN_FORMATS;
IntlMessageFormat.defaultLocale = defaultLocale;
IntlRelativeFormat.defaultLocale = defaultLocale;
/**
* Returns message by the given message id.
* @param {string} id - path to the message
* @returns {string} message - translated message from messages tree
*/
function getMessageById(id) {
return getMessages()[id];
}
/**
* Normalizes locale to make it consistent with IntlMessageFormat locales
* @param {string} locale
* @returns {string} normalizedLocale
*/
function normalizeLocale(locale) {
return locale.toLowerCase().replace('_', LOCALE_DELIMITER);
}
/**
* Provides a way to register translations with the engine
* @param {Messages} newMessages
* @param {string} [locale = messages.locale]
*/
export function addMessages(newMessages = {}, locale = newMessages.locale) {
if (!locale || !isString(locale)) {
throw new Error(
'[I18n] A `locale` must be a non-empty string to add messages.'
);
}
const normalizedLocale = normalizeLocale(locale);
messages[normalizedLocale] = {
...messages[normalizedLocale],
...newMessages,
};
}
/**
* Returns messages for the current language
* @returns {Messages} messages
*/
export function getMessages() {
return messages[currentLocale] || {};
}
/**
* Tells the engine which language to use by given language key
* @param {string} locale
*/
export function setLocale(locale) {
if (!locale || !isString(locale)) {
throw new Error('[I18n] A `locale` must be a non-empty string.');
}
currentLocale = normalizeLocale(locale);
}
/**
* Returns the current locale
* @returns {string} locale
*/
export function getLocale() {
return currentLocale;
}
/**
* Tells the library which language to fallback when missing translations
* @param {string} locale
*/
export function setDefaultLocale(locale) {
if (!locale || !isString(locale)) {
throw new Error('[I18n] A `locale` must be a non-empty string.');
}
defaultLocale = normalizeLocale(locale);
IntlMessageFormat.defaultLocale = defaultLocale;
IntlRelativeFormat.defaultLocale = defaultLocale;
}
/**
* Returns the default locale
* @returns {string} defaultLocale
*/
export function getDefaultLocale() {
return defaultLocale;
}
/**
* Supplies a set of options to the underlying formatter
* [Default format options used as the prototype of the formats]
* {@link https://github.com/yahoo/intl-messageformat/blob/master/src/core.js#L62}
* These are used when constructing the internal Intl.NumberFormat
* and Intl.DateTimeFormat instances.
* @param {object} newFormats
* @param {object} [newFormats.number]
* @param {object} [newFormats.date]
* @param {object} [newFormats.time]
*/
export function setFormats(newFormats) {
if (!isObject(newFormats) || !hasValues(newFormats)) {
throw new Error('[I18n] A `formats` must be a non-empty object.');
}
formats = mergeAll(formats, newFormats);
}
/**
* Returns current formats
* @returns {object} formats
*/
export function getFormats() {
return formats;
}
/**
* Returns array of locales having translations
* @returns {string[]} locales
*/
export function getRegisteredLocales() {
return Object.keys(messages);
}
/**
* Translate message by id
* @param {string} id - translation id to be translated
* @param {object} [options]
* @param {object} [options.values] - values to pass into translation
* @param {string} [options.defaultMessage] - will be used unless translation was successful
* @returns {string}
*/
export function translate(id, { values = {}, defaultMessage = '' } = {}) {
if (!id || !isString(id)) {
throw new Error(
'[I18n] An `id` must be a non-empty string to translate a message.'
);
}
const message = getMessageById(id);
if (!message && !defaultMessage) {
throw new Error(
`[I18n] Cannot format message: "${id}". Default message must be provided.`
);
}
if (!hasValues(values)) {
return message || defaultMessage;
}
if (message) {
try {
const msg = getMessageFormat(message, getLocale(), getFormats());
return msg.format(values);
} catch (e) {
throw new Error(
`[I18n] Error formatting message: "${id}" for locale: "${getLocale()}".\n${e}`
);
}
}
try {
const msg = getMessageFormat(
defaultMessage,
getDefaultLocale(),
getFormats()
);
return msg.format(values);
} catch (e) {
throw new Error(
`[I18n] Error formatting the default message for: "${id}".\n${e}`
);
}
}
/**
* Initializes the engine
* @param {Messages} newMessages
*/
export function init(newMessages) {
if (!newMessages) {
return;
}
addMessages(newMessages);
if (newMessages.locale) {
setLocale(newMessages.locale);
}
if (newMessages.formats) {
setFormats(newMessages.formats);
}
}

View file

@ -0,0 +1,207 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
/**
@typedef Messages - messages tree, where leafs are translated strings
@type {object<string, object>}
@property {string} [locale] - locale of the messages
@property {object} [formats] - set of options to the underlying formatter
*/
import path from 'path';
import { readFile } from 'fs';
import { promisify } from 'util';
import { pick } from 'accept-language-parser';
import JSON5 from 'json5';
import { unique } from './helper';
const asyncReadFile = promisify(readFile);
const TRANSLATION_FILE_EXTENSION = '.json';
/**
* Internal property for storing registered translations paths
* @type {Map<string, string[]>|{}} - Key is locale, value is array of registered paths
*/
const translationsRegistry = {};
/**
* Internal property for caching loaded translations files
* @type {Map<string, Messages>|{}} - Key is path to translation file, value is
* object with translation messages
*/
const loadedFiles = {};
/**
* Returns locale by the given translation file name
* @param {string} fullFileName
* @returns {string} locale
* @example
* getLocaleFromFileName('./path/to/translation/ru.json') // => 'ru'
*/
function getLocaleFromFileName(fullFileName) {
if (!fullFileName) {
throw new Error('Filename is empty');
}
const fileExt = path.extname(fullFileName);
if (fileExt !== TRANSLATION_FILE_EXTENSION) {
throw new Error(
`Translations must have 'json' extension. File being registered is ${fullFileName}`
);
}
return path.basename(fullFileName, TRANSLATION_FILE_EXTENSION);
}
/**
* Loads file and parses it as JSON5
* @param {string} pathToFile
* @returns {Promise<object>}
*/
async function loadFile(pathToFile) {
return JSON5.parse(await asyncReadFile(pathToFile, 'utf8'));
}
/**
* Parses the accept-language header from an HTTP request and picks
* the best match of the locale from the registered locales
* @param {string} header - accept-language header from an HTTP request
* @returns {string} locale
*/
function pickLocaleByLanguageHeader(header) {
return pick(getRegisteredLocales(), header);
}
/**
* Loads translations files and adds them into "loadedFiles" cache
* @param {string[]} files
* @returns {Promise<void>}
*/
async function loadAndCacheFiles(files) {
const translations = await Promise.all(files.map(loadFile));
files.forEach((file, index) => {
loadedFiles[file] = translations[index];
});
}
/**
* Registers translation file with i18n loader
* @param {string} translationFilePath - Absolute path to the translation file to register.
*/
export function registerTranslationFile(translationFilePath) {
if (!path.isAbsolute(translationFilePath)) {
throw new TypeError(
'Paths to translation files must be absolute. ' +
`Got relative path: "${translationFilePath}"`
);
}
const locale = getLocaleFromFileName(translationFilePath);
translationsRegistry[locale] = unique([
...(translationsRegistry[locale] || []),
translationFilePath,
]);
}
/**
* Registers array of translation files with i18n loader
* @param {string[]} arrayOfPaths - Array of absolute paths to the translation files to register.
*/
export function registerTranslationFiles(arrayOfPaths = []) {
arrayOfPaths.forEach(registerTranslationFile);
}
/**
* Returns an array of locales that have been registered with i18n loader
* @returns {string[]} registeredTranslations
*/
export function getRegisteredLocales() {
return Object.keys(translationsRegistry);
}
/**
* Returns translations for a suitable locale based on accept-language header.
* This object will contain all registered translations for the highest priority
* locale which is registered with the i18n loader. This object can be empty
* if no locale in the language tags can be matched against the registered locales.
* @param {string} header - accept-language header from an HTTP request
* @returns {Promise<Messages>} translations - translation messages
*/
export async function getTranslationsByLanguageHeader(header) {
return getTranslationsByLocale(pickLocaleByLanguageHeader(header));
}
/**
* Returns translation messages by specified locale
* @param {string} locale
* @returns {Promise<Messages>} translations - translation messages
*/
export async function getTranslationsByLocale(locale) {
const files = translationsRegistry[locale] || [];
const notLoadedFiles = files.filter(file => !loadedFiles[file]);
if (notLoadedFiles.length) {
await loadAndCacheFiles(notLoadedFiles);
}
return files.length
? files.reduce(
(messages, file) => ({
...messages,
...loadedFiles[file],
}),
{ locale }
)
: {};
}
/**
* Returns all translations for registered locales
* @return {Promise<Map<string, Messages>>} translations - A Promise object
* where keys are the locale and values are objects of translation messages
*/
export async function getAllTranslations() {
const locales = getRegisteredLocales();
const translations = await Promise.all(locales.map(getTranslationsByLocale));
return locales.reduce(
(acc, locale, index) => ({
...acc,
[locale]: translations[index],
}),
{}
);
}
/**
* Registers passed translations files, loads them and returns promise with
* all translation messages
* @param {string[]} paths - Array of absolute paths to the translation files
* @returns {Promise<Map<string, Messages>>} translations - A Promise object
* where keys are the locale and values are objects of translation messages
*/
export async function getAllTranslationsFromPaths(paths) {
registerTranslationFiles(paths);
return await getAllTranslations();
}

View file

@ -0,0 +1,566 @@
/* eslint-disable */
// TODO: Get rid of this file once https://github.com/elastic/kibana/pull/20105
// is merged and use dynamic import for asynchronous loading of specific locale data
import IntlMessageFormat from 'intl-messageformat';
import IntlRelativeFormat from 'intl-relativeformat';
function addLocaleData(localeData) {
IntlMessageFormat.__addLocaleData(localeData);
IntlRelativeFormat.__addLocaleData(localeData);
}
addLocaleData({"locale":"af","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"af-NA","parentLocale":"af"});
addLocaleData({"locale":"agq","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ak","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0||n==1?"one":"other"}});
addLocaleData({"locale":"am","pluralRuleFunction":function (n,ord){if(ord)return"other";return n>=0&&n<=1?"one":"other"}});
addLocaleData({"locale":"ar","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n,n100=t0&&s[0].slice(-2);if(ord)return"other";return n==0?"zero":n==1?"one":n==2?"two":n100>=3&&n100<=10?"few":n100>=11&&n100<=99?"many":"other"}});
addLocaleData({"locale":"ar-AE","parentLocale":"ar"});
addLocaleData({"locale":"ar-BH","parentLocale":"ar"});
addLocaleData({"locale":"ar-DJ","parentLocale":"ar"});
addLocaleData({"locale":"ar-DZ","parentLocale":"ar"});
addLocaleData({"locale":"ar-EG","parentLocale":"ar"});
addLocaleData({"locale":"ar-EH","parentLocale":"ar"});
addLocaleData({"locale":"ar-ER","parentLocale":"ar"});
addLocaleData({"locale":"ar-IL","parentLocale":"ar"});
addLocaleData({"locale":"ar-IQ","parentLocale":"ar"});
addLocaleData({"locale":"ar-JO","parentLocale":"ar"});
addLocaleData({"locale":"ar-KM","parentLocale":"ar"});
addLocaleData({"locale":"ar-KW","parentLocale":"ar"});
addLocaleData({"locale":"ar-LB","parentLocale":"ar"});
addLocaleData({"locale":"ar-LY","parentLocale":"ar"});
addLocaleData({"locale":"ar-MA","parentLocale":"ar"});
addLocaleData({"locale":"ar-MR","parentLocale":"ar"});
addLocaleData({"locale":"ar-OM","parentLocale":"ar"});
addLocaleData({"locale":"ar-PS","parentLocale":"ar"});
addLocaleData({"locale":"ar-QA","parentLocale":"ar"});
addLocaleData({"locale":"ar-SA","parentLocale":"ar"});
addLocaleData({"locale":"ar-SD","parentLocale":"ar"});
addLocaleData({"locale":"ar-SO","parentLocale":"ar"});
addLocaleData({"locale":"ar-SS","parentLocale":"ar"});
addLocaleData({"locale":"ar-SY","parentLocale":"ar"});
addLocaleData({"locale":"ar-TD","parentLocale":"ar"});
addLocaleData({"locale":"ar-TN","parentLocale":"ar"});
addLocaleData({"locale":"ar-YE","parentLocale":"ar"});
addLocaleData({"locale":"as","pluralRuleFunction":function (n,ord){if(ord)return n==1||n==5||n==7||n==8||n==9||n==10?"one":n==2||n==3?"two":n==4?"few":n==6?"many":"other";return n>=0&&n<=1?"one":"other"}});
addLocaleData({"locale":"asa","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ast","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"az","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],i10=i.slice(-1),i100=i.slice(-2),i1000=i.slice(-3);if(ord)return i10==1||i10==2||i10==5||i10==7||i10==8||(i100==20||i100==50||i100==70||i100==80)?"one":i10==3||i10==4||(i1000==100||i1000==200||i1000==300||i1000==400||i1000==500||i1000==600||i1000==700||i1000==800||i1000==900)?"few":i==0||i10==6||(i100==40||i100==60||i100==90)?"many":"other";return n==1?"one":"other"}});
addLocaleData({"locale":"az-Arab","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"az-Cyrl","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"az-Latn","parentLocale":"az"});
addLocaleData({"locale":"bas","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"be","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return(n10==2||n10==3)&&n100!=12&&n100!=13?"few":"other";return n10==1&&n100!=11?"one":n10>=2&&n10<=4&&(n100<12||n100>14)?"few":t0&&n10==0||n10>=5&&n10<=9||n100>=11&&n100<=14?"many":"other"}});
addLocaleData({"locale":"bem","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"bez","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"bg","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"bh","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0||n==1?"one":"other"}});
addLocaleData({"locale":"bm","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"bm-Nkoo","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"bn","pluralRuleFunction":function (n,ord){if(ord)return n==1||n==5||n==7||n==8||n==9||n==10?"one":n==2||n==3?"two":n==4?"few":n==6?"many":"other";return n>=0&&n<=1?"one":"other"}});
addLocaleData({"locale":"bn-IN","parentLocale":"bn"});
addLocaleData({"locale":"bo","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"bo-IN","parentLocale":"bo"});
addLocaleData({"locale":"br","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2),n1000000=t0&&s[0].slice(-6);if(ord)return"other";return n10==1&&n100!=11&&n100!=71&&n100!=91?"one":n10==2&&n100!=12&&n100!=72&&n100!=92?"two":(n10==3||n10==4||n10==9)&&(n100<10||n100>19)&&(n100<70||n100>79)&&(n100<90||n100>99)?"few":n!=0&&t0&&n1000000==0?"many":"other"}});
addLocaleData({"locale":"brx","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"bs","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"",v0=!s[1],i10=i.slice(-1),i100=i.slice(-2),f10=f.slice(-1),f100=f.slice(-2);if(ord)return"other";return v0&&i10==1&&i100!=11||f10==1&&f100!=11?"one":v0&&(i10>=2&&i10<=4)&&(i100<12||i100>14)||f10>=2&&f10<=4&&(f100<12||f100>14)?"few":"other"}});
addLocaleData({"locale":"bs-Cyrl","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"bs-Latn","parentLocale":"bs"});
addLocaleData({"locale":"ca","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return n==1||n==3?"one":n==2?"two":n==4?"few":"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"ca-AD","parentLocale":"ca"});
addLocaleData({"locale":"ca-ES-VALENCIA","parentLocale":"ca-ES"});
addLocaleData({"locale":"ca-ES","parentLocale":"ca"});
addLocaleData({"locale":"ca-FR","parentLocale":"ca"});
addLocaleData({"locale":"ca-IT","parentLocale":"ca"});
addLocaleData({"locale":"ce","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"cgg","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"chr","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ckb","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ckb-IR","parentLocale":"ckb"});
addLocaleData({"locale":"cs","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1];if(ord)return"other";return n==1&&v0?"one":i>=2&&i<=4&&v0?"few":!v0?"many":"other"}});
addLocaleData({"locale":"cu","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"cy","pluralRuleFunction":function (n,ord){if(ord)return n==0||n==7||n==8||n==9?"zero":n==1?"one":n==2?"two":n==3||n==4?"few":n==5||n==6?"many":"other";return n==0?"zero":n==1?"one":n==2?"two":n==3?"few":n==6?"many":"other"}});
addLocaleData({"locale":"da","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],t0=Number(s[0])==n;if(ord)return"other";return n==1||!t0&&(i==0||i==1)?"one":"other"}});
addLocaleData({"locale":"da-GL","parentLocale":"da"});
addLocaleData({"locale":"dav","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"de","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"de-AT","parentLocale":"de"});
addLocaleData({"locale":"de-BE","parentLocale":"de"});
addLocaleData({"locale":"de-CH","parentLocale":"de"});
addLocaleData({"locale":"de-LI","parentLocale":"de"});
addLocaleData({"locale":"de-LU","parentLocale":"de"});
addLocaleData({"locale":"dje","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"dsb","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"",v0=!s[1],i100=i.slice(-2),f100=f.slice(-2);if(ord)return"other";return v0&&i100==1||f100==1?"one":v0&&i100==2||f100==2?"two":v0&&(i100==3||i100==4)||(f100==3||f100==4)?"few":"other"}});
addLocaleData({"locale":"dua","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"dv","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"dyo","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"dz","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ebu","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ee","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ee-TG","parentLocale":"ee"});
addLocaleData({"locale":"el","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"el-CY","parentLocale":"el"});
addLocaleData({"locale":"en","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return n10==1&&n100!=11?"one":n10==2&&n100!=12?"two":n10==3&&n100!=13?"few":"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"en-001","parentLocale":"en"});
addLocaleData({"locale":"en-150","parentLocale":"en-001"});
addLocaleData({"locale":"en-AG","parentLocale":"en-001"});
addLocaleData({"locale":"en-AI","parentLocale":"en-001"});
addLocaleData({"locale":"en-AS","parentLocale":"en"});
addLocaleData({"locale":"en-AT","parentLocale":"en-150"});
addLocaleData({"locale":"en-AU","parentLocale":"en-001"});
addLocaleData({"locale":"en-BB","parentLocale":"en-001"});
addLocaleData({"locale":"en-BE","parentLocale":"en-001"});
addLocaleData({"locale":"en-BI","parentLocale":"en"});
addLocaleData({"locale":"en-BM","parentLocale":"en-001"});
addLocaleData({"locale":"en-BS","parentLocale":"en-001"});
addLocaleData({"locale":"en-BW","parentLocale":"en-001"});
addLocaleData({"locale":"en-BZ","parentLocale":"en-001"});
addLocaleData({"locale":"en-CA","parentLocale":"en-001"});
addLocaleData({"locale":"en-CC","parentLocale":"en-001"});
addLocaleData({"locale":"en-CH","parentLocale":"en-150"});
addLocaleData({"locale":"en-CK","parentLocale":"en-001"});
addLocaleData({"locale":"en-CM","parentLocale":"en-001"});
addLocaleData({"locale":"en-CX","parentLocale":"en-001"});
addLocaleData({"locale":"en-CY","parentLocale":"en-001"});
addLocaleData({"locale":"en-DE","parentLocale":"en-150"});
addLocaleData({"locale":"en-DG","parentLocale":"en-001"});
addLocaleData({"locale":"en-DK","parentLocale":"en-150"});
addLocaleData({"locale":"en-DM","parentLocale":"en-001"});
addLocaleData({"locale":"en-Dsrt","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"en-ER","parentLocale":"en-001"});
addLocaleData({"locale":"en-FI","parentLocale":"en-150"});
addLocaleData({"locale":"en-FJ","parentLocale":"en-001"});
addLocaleData({"locale":"en-FK","parentLocale":"en-001"});
addLocaleData({"locale":"en-FM","parentLocale":"en-001"});
addLocaleData({"locale":"en-GB","parentLocale":"en-001"});
addLocaleData({"locale":"en-GD","parentLocale":"en-001"});
addLocaleData({"locale":"en-GG","parentLocale":"en-001"});
addLocaleData({"locale":"en-GH","parentLocale":"en-001"});
addLocaleData({"locale":"en-GI","parentLocale":"en-001"});
addLocaleData({"locale":"en-GM","parentLocale":"en-001"});
addLocaleData({"locale":"en-GU","parentLocale":"en"});
addLocaleData({"locale":"en-GY","parentLocale":"en-001"});
addLocaleData({"locale":"en-HK","parentLocale":"en-001"});
addLocaleData({"locale":"en-IE","parentLocale":"en-001"});
addLocaleData({"locale":"en-IL","parentLocale":"en-001"});
addLocaleData({"locale":"en-IM","parentLocale":"en-001"});
addLocaleData({"locale":"en-IN","parentLocale":"en-001"});
addLocaleData({"locale":"en-IO","parentLocale":"en-001"});
addLocaleData({"locale":"en-JE","parentLocale":"en-001"});
addLocaleData({"locale":"en-JM","parentLocale":"en-001"});
addLocaleData({"locale":"en-KE","parentLocale":"en-001"});
addLocaleData({"locale":"en-KI","parentLocale":"en-001"});
addLocaleData({"locale":"en-KN","parentLocale":"en-001"});
addLocaleData({"locale":"en-KY","parentLocale":"en-001"});
addLocaleData({"locale":"en-LC","parentLocale":"en-001"});
addLocaleData({"locale":"en-LR","parentLocale":"en-001"});
addLocaleData({"locale":"en-LS","parentLocale":"en-001"});
addLocaleData({"locale":"en-MG","parentLocale":"en-001"});
addLocaleData({"locale":"en-MH","parentLocale":"en"});
addLocaleData({"locale":"en-MO","parentLocale":"en-001"});
addLocaleData({"locale":"en-MP","parentLocale":"en"});
addLocaleData({"locale":"en-MS","parentLocale":"en-001"});
addLocaleData({"locale":"en-MT","parentLocale":"en-001"});
addLocaleData({"locale":"en-MU","parentLocale":"en-001"});
addLocaleData({"locale":"en-MW","parentLocale":"en-001"});
addLocaleData({"locale":"en-MY","parentLocale":"en-001"});
addLocaleData({"locale":"en-NA","parentLocale":"en-001"});
addLocaleData({"locale":"en-NF","parentLocale":"en-001"});
addLocaleData({"locale":"en-NG","parentLocale":"en-001"});
addLocaleData({"locale":"en-NL","parentLocale":"en-150"});
addLocaleData({"locale":"en-NR","parentLocale":"en-001"});
addLocaleData({"locale":"en-NU","parentLocale":"en-001"});
addLocaleData({"locale":"en-NZ","parentLocale":"en-001"});
addLocaleData({"locale":"en-PG","parentLocale":"en-001"});
addLocaleData({"locale":"en-PH","parentLocale":"en-001"});
addLocaleData({"locale":"en-PK","parentLocale":"en-001"});
addLocaleData({"locale":"en-PN","parentLocale":"en-001"});
addLocaleData({"locale":"en-PR","parentLocale":"en"});
addLocaleData({"locale":"en-PW","parentLocale":"en-001"});
addLocaleData({"locale":"en-RW","parentLocale":"en-001"});
addLocaleData({"locale":"en-SB","parentLocale":"en-001"});
addLocaleData({"locale":"en-SC","parentLocale":"en-001"});
addLocaleData({"locale":"en-SD","parentLocale":"en-001"});
addLocaleData({"locale":"en-SE","parentLocale":"en-150"});
addLocaleData({"locale":"en-SG","parentLocale":"en-001"});
addLocaleData({"locale":"en-SH","parentLocale":"en-001"});
addLocaleData({"locale":"en-SI","parentLocale":"en-150"});
addLocaleData({"locale":"en-SL","parentLocale":"en-001"});
addLocaleData({"locale":"en-SS","parentLocale":"en-001"});
addLocaleData({"locale":"en-SX","parentLocale":"en-001"});
addLocaleData({"locale":"en-SZ","parentLocale":"en-001"});
addLocaleData({"locale":"en-Shaw","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"en-TC","parentLocale":"en-001"});
addLocaleData({"locale":"en-TK","parentLocale":"en-001"});
addLocaleData({"locale":"en-TO","parentLocale":"en-001"});
addLocaleData({"locale":"en-TT","parentLocale":"en-001"});
addLocaleData({"locale":"en-TV","parentLocale":"en-001"});
addLocaleData({"locale":"en-TZ","parentLocale":"en-001"});
addLocaleData({"locale":"en-UG","parentLocale":"en-001"});
addLocaleData({"locale":"en-UM","parentLocale":"en"});
addLocaleData({"locale":"en-US","parentLocale":"en"});
addLocaleData({"locale":"en-VC","parentLocale":"en-001"});
addLocaleData({"locale":"en-VG","parentLocale":"en-001"});
addLocaleData({"locale":"en-VI","parentLocale":"en"});
addLocaleData({"locale":"en-VU","parentLocale":"en-001"});
addLocaleData({"locale":"en-WS","parentLocale":"en-001"});
addLocaleData({"locale":"en-ZA","parentLocale":"en-001"});
addLocaleData({"locale":"en-ZM","parentLocale":"en-001"});
addLocaleData({"locale":"en-ZW","parentLocale":"en-001"});
addLocaleData({"locale":"eo","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"es","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"es-419","parentLocale":"es"});
addLocaleData({"locale":"es-AR","parentLocale":"es-419"});
addLocaleData({"locale":"es-BO","parentLocale":"es-419"});
addLocaleData({"locale":"es-CL","parentLocale":"es-419"});
addLocaleData({"locale":"es-CO","parentLocale":"es-419"});
addLocaleData({"locale":"es-CR","parentLocale":"es-419"});
addLocaleData({"locale":"es-CU","parentLocale":"es-419"});
addLocaleData({"locale":"es-DO","parentLocale":"es-419"});
addLocaleData({"locale":"es-EA","parentLocale":"es"});
addLocaleData({"locale":"es-EC","parentLocale":"es-419"});
addLocaleData({"locale":"es-GQ","parentLocale":"es"});
addLocaleData({"locale":"es-GT","parentLocale":"es-419"});
addLocaleData({"locale":"es-HN","parentLocale":"es-419"});
addLocaleData({"locale":"es-IC","parentLocale":"es"});
addLocaleData({"locale":"es-MX","parentLocale":"es-419"});
addLocaleData({"locale":"es-NI","parentLocale":"es-419"});
addLocaleData({"locale":"es-PA","parentLocale":"es-419"});
addLocaleData({"locale":"es-PE","parentLocale":"es-419"});
addLocaleData({"locale":"es-PH","parentLocale":"es"});
addLocaleData({"locale":"es-PR","parentLocale":"es-419"});
addLocaleData({"locale":"es-PY","parentLocale":"es-419"});
addLocaleData({"locale":"es-SV","parentLocale":"es-419"});
addLocaleData({"locale":"es-US","parentLocale":"es-419"});
addLocaleData({"locale":"es-UY","parentLocale":"es-419"});
addLocaleData({"locale":"es-VE","parentLocale":"es-419"});
addLocaleData({"locale":"et","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"eu","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ewo","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"fa","pluralRuleFunction":function (n,ord){if(ord)return"other";return n>=0&&n<=1?"one":"other"}});
addLocaleData({"locale":"fa-AF","parentLocale":"fa"});
addLocaleData({"locale":"ff","pluralRuleFunction":function (n,ord){if(ord)return"other";return n>=0&&n<2?"one":"other"}});
addLocaleData({"locale":"ff-CM","parentLocale":"ff"});
addLocaleData({"locale":"ff-GN","parentLocale":"ff"});
addLocaleData({"locale":"ff-MR","parentLocale":"ff"});
addLocaleData({"locale":"fi","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"fil","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"",v0=!s[1],i10=i.slice(-1),f10=f.slice(-1);if(ord)return n==1?"one":"other";return v0&&(i==1||i==2||i==3)||v0&&i10!=4&&i10!=6&&i10!=9||!v0&&f10!=4&&f10!=6&&f10!=9?"one":"other"}});
addLocaleData({"locale":"fo","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"fo-DK","parentLocale":"fo"});
addLocaleData({"locale":"fr","pluralRuleFunction":function (n,ord){if(ord)return n==1?"one":"other";return n>=0&&n<2?"one":"other"}});
addLocaleData({"locale":"fr-BE","parentLocale":"fr"});
addLocaleData({"locale":"fr-BF","parentLocale":"fr"});
addLocaleData({"locale":"fr-BI","parentLocale":"fr"});
addLocaleData({"locale":"fr-BJ","parentLocale":"fr"});
addLocaleData({"locale":"fr-BL","parentLocale":"fr"});
addLocaleData({"locale":"fr-CA","parentLocale":"fr"});
addLocaleData({"locale":"fr-CD","parentLocale":"fr"});
addLocaleData({"locale":"fr-CF","parentLocale":"fr"});
addLocaleData({"locale":"fr-CG","parentLocale":"fr"});
addLocaleData({"locale":"fr-CH","parentLocale":"fr"});
addLocaleData({"locale":"fr-CI","parentLocale":"fr"});
addLocaleData({"locale":"fr-CM","parentLocale":"fr"});
addLocaleData({"locale":"fr-DJ","parentLocale":"fr"});
addLocaleData({"locale":"fr-DZ","parentLocale":"fr"});
addLocaleData({"locale":"fr-GA","parentLocale":"fr"});
addLocaleData({"locale":"fr-GF","parentLocale":"fr"});
addLocaleData({"locale":"fr-GN","parentLocale":"fr"});
addLocaleData({"locale":"fr-GP","parentLocale":"fr"});
addLocaleData({"locale":"fr-GQ","parentLocale":"fr"});
addLocaleData({"locale":"fr-HT","parentLocale":"fr"});
addLocaleData({"locale":"fr-KM","parentLocale":"fr"});
addLocaleData({"locale":"fr-LU","parentLocale":"fr"});
addLocaleData({"locale":"fr-MA","parentLocale":"fr"});
addLocaleData({"locale":"fr-MC","parentLocale":"fr"});
addLocaleData({"locale":"fr-MF","parentLocale":"fr"});
addLocaleData({"locale":"fr-MG","parentLocale":"fr"});
addLocaleData({"locale":"fr-ML","parentLocale":"fr"});
addLocaleData({"locale":"fr-MQ","parentLocale":"fr"});
addLocaleData({"locale":"fr-MR","parentLocale":"fr"});
addLocaleData({"locale":"fr-MU","parentLocale":"fr"});
addLocaleData({"locale":"fr-NC","parentLocale":"fr"});
addLocaleData({"locale":"fr-NE","parentLocale":"fr"});
addLocaleData({"locale":"fr-PF","parentLocale":"fr"});
addLocaleData({"locale":"fr-PM","parentLocale":"fr"});
addLocaleData({"locale":"fr-RE","parentLocale":"fr"});
addLocaleData({"locale":"fr-RW","parentLocale":"fr"});
addLocaleData({"locale":"fr-SC","parentLocale":"fr"});
addLocaleData({"locale":"fr-SN","parentLocale":"fr"});
addLocaleData({"locale":"fr-SY","parentLocale":"fr"});
addLocaleData({"locale":"fr-TD","parentLocale":"fr"});
addLocaleData({"locale":"fr-TG","parentLocale":"fr"});
addLocaleData({"locale":"fr-TN","parentLocale":"fr"});
addLocaleData({"locale":"fr-VU","parentLocale":"fr"});
addLocaleData({"locale":"fr-WF","parentLocale":"fr"});
addLocaleData({"locale":"fr-YT","parentLocale":"fr"});
addLocaleData({"locale":"fur","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"fy","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"ga","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n;if(ord)return n==1?"one":"other";return n==1?"one":n==2?"two":t0&&n>=3&&n<=6?"few":t0&&n>=7&&n<=10?"many":"other"}});
addLocaleData({"locale":"gd","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n;if(ord)return"other";return n==1||n==11?"one":n==2||n==12?"two":t0&&n>=3&&n<=10||t0&&n>=13&&n<=19?"few":"other"}});
addLocaleData({"locale":"gl","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"gsw","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"gsw-FR","parentLocale":"gsw"});
addLocaleData({"locale":"gsw-LI","parentLocale":"gsw"});
addLocaleData({"locale":"gu","pluralRuleFunction":function (n,ord){if(ord)return n==1?"one":n==2||n==3?"two":n==4?"few":n==6?"many":"other";return n>=0&&n<=1?"one":"other"}});
addLocaleData({"locale":"guw","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0||n==1?"one":"other"}});
addLocaleData({"locale":"guz","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"gv","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],i10=i.slice(-1),i100=i.slice(-2);if(ord)return"other";return v0&&i10==1?"one":v0&&i10==2?"two":v0&&(i100==0||i100==20||i100==40||i100==60||i100==80)?"few":!v0?"many":"other"}});
addLocaleData({"locale":"ha","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ha-Arab","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ha-GH","parentLocale":"ha"});
addLocaleData({"locale":"ha-NE","parentLocale":"ha"});
addLocaleData({"locale":"haw","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"he","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1);if(ord)return"other";return n==1&&v0?"one":i==2&&v0?"two":v0&&(n<0||n>10)&&t0&&n10==0?"many":"other"}});
addLocaleData({"locale":"hi","pluralRuleFunction":function (n,ord){if(ord)return n==1?"one":n==2||n==3?"two":n==4?"few":n==6?"many":"other";return n>=0&&n<=1?"one":"other"}});
addLocaleData({"locale":"hr","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"",v0=!s[1],i10=i.slice(-1),i100=i.slice(-2),f10=f.slice(-1),f100=f.slice(-2);if(ord)return"other";return v0&&i10==1&&i100!=11||f10==1&&f100!=11?"one":v0&&(i10>=2&&i10<=4)&&(i100<12||i100>14)||f10>=2&&f10<=4&&(f100<12||f100>14)?"few":"other"}});
addLocaleData({"locale":"hr-BA","parentLocale":"hr"});
addLocaleData({"locale":"hsb","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"",v0=!s[1],i100=i.slice(-2),f100=f.slice(-2);if(ord)return"other";return v0&&i100==1||f100==1?"one":v0&&i100==2||f100==2?"two":v0&&(i100==3||i100==4)||(f100==3||f100==4)?"few":"other"}});
addLocaleData({"locale":"hu","pluralRuleFunction":function (n,ord){if(ord)return n==1||n==5?"one":"other";return n==1?"one":"other"}});
addLocaleData({"locale":"hy","pluralRuleFunction":function (n,ord){if(ord)return n==1?"one":"other";return n>=0&&n<2?"one":"other"}});
addLocaleData({"locale":"id","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ig","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ii","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"in","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"is","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],t0=Number(s[0])==n,i10=i.slice(-1),i100=i.slice(-2);if(ord)return"other";return t0&&i10==1&&i100!=11||!t0?"one":"other"}});
addLocaleData({"locale":"it","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return n==11||n==8||n==80||n==800?"many":"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"it-CH","parentLocale":"it"});
addLocaleData({"locale":"it-SM","parentLocale":"it"});
addLocaleData({"locale":"iu","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":n==2?"two":"other"}});
addLocaleData({"locale":"iu-Latn","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"iw","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1);if(ord)return"other";return n==1&&v0?"one":i==2&&v0?"two":v0&&(n<0||n>10)&&t0&&n10==0?"many":"other"}});
addLocaleData({"locale":"ja","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"jbo","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"jgo","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ji","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"jmc","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"jv","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"jw","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ka","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],i100=i.slice(-2);if(ord)return i==1?"one":i==0||(i100>=2&&i100<=20||i100==40||i100==60||i100==80)?"many":"other";return n==1?"one":"other"}});
addLocaleData({"locale":"kab","pluralRuleFunction":function (n,ord){if(ord)return"other";return n>=0&&n<2?"one":"other"}});
addLocaleData({"locale":"kaj","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"kam","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"kcg","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"kde","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"kea","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"khq","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ki","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"kk","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n,n10=t0&&s[0].slice(-1);if(ord)return n10==6||n10==9||t0&&n10==0&&n!=0?"many":"other";return n==1?"one":"other"}});
addLocaleData({"locale":"kkj","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"kl","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"kln","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"km","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"kn","pluralRuleFunction":function (n,ord){if(ord)return"other";return n>=0&&n<=1?"one":"other"}});
addLocaleData({"locale":"ko","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ko-KP","parentLocale":"ko"});
addLocaleData({"locale":"kok","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ks","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ksb","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ksf","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ksh","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0?"zero":n==1?"one":"other"}});
addLocaleData({"locale":"ku","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"kw","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":n==2?"two":"other"}});
addLocaleData({"locale":"ky","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"lag","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0];if(ord)return"other";return n==0?"zero":(i==0||i==1)&&n!=0?"one":"other"}});
addLocaleData({"locale":"lb","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"lg","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"lkt","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ln","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0||n==1?"one":"other"}});
addLocaleData({"locale":"ln-AO","parentLocale":"ln"});
addLocaleData({"locale":"ln-CF","parentLocale":"ln"});
addLocaleData({"locale":"ln-CG","parentLocale":"ln"});
addLocaleData({"locale":"lo","pluralRuleFunction":function (n,ord){if(ord)return n==1?"one":"other";return"other"}});
addLocaleData({"locale":"lrc","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"lrc-IQ","parentLocale":"lrc"});
addLocaleData({"locale":"lt","pluralRuleFunction":function (n,ord){var s=String(n).split("."),f=s[1]||"",t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return"other";return n10==1&&(n100<11||n100>19)?"one":n10>=2&&n10<=9&&(n100<11||n100>19)?"few":f!=0?"many":"other"}});
addLocaleData({"locale":"lu","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"luo","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"luy","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"lv","pluralRuleFunction":function (n,ord){var s=String(n).split("."),f=s[1]||"",v=f.length,t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2),f100=f.slice(-2),f10=f.slice(-1);if(ord)return"other";return t0&&n10==0||n100>=11&&n100<=19||v==2&&(f100>=11&&f100<=19)?"zero":n10==1&&n100!=11||v==2&&f10==1&&f100!=11||v!=2&&f10==1?"one":"other"}});
addLocaleData({"locale":"mas","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"mas-TZ","parentLocale":"mas"});
addLocaleData({"locale":"mer","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"mfe","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"mg","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0||n==1?"one":"other"}});
addLocaleData({"locale":"mgh","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"mgo","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"mk","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"",v0=!s[1],i10=i.slice(-1),i100=i.slice(-2),f10=f.slice(-1);if(ord)return i10==1&&i100!=11?"one":i10==2&&i100!=12?"two":(i10==7||i10==8)&&i100!=17&&i100!=18?"many":"other";return v0&&i10==1||f10==1?"one":"other"}});
addLocaleData({"locale":"ml","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"mn","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"mn-Mong","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"mo","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n100=t0&&s[0].slice(-2);if(ord)return n==1?"one":"other";return n==1&&v0?"one":!v0||n==0||n!=1&&(n100>=1&&n100<=19)?"few":"other"}});
addLocaleData({"locale":"mr","pluralRuleFunction":function (n,ord){if(ord)return n==1?"one":n==2||n==3?"two":n==4?"few":"other";return n>=0&&n<=1?"one":"other"}});
addLocaleData({"locale":"ms","pluralRuleFunction":function (n,ord){if(ord)return n==1?"one":"other";return"other"}});
addLocaleData({"locale":"ms-Arab","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ms-BN","parentLocale":"ms"});
addLocaleData({"locale":"ms-SG","parentLocale":"ms"});
addLocaleData({"locale":"mt","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n,n100=t0&&s[0].slice(-2);if(ord)return"other";return n==1?"one":n==0||n100>=2&&n100<=10?"few":n100>=11&&n100<=19?"many":"other"}});
addLocaleData({"locale":"mua","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"my","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"mzn","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"nah","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"naq","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":n==2?"two":"other"}});
addLocaleData({"locale":"nb","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"nb-SJ","parentLocale":"nb"});
addLocaleData({"locale":"nd","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ne","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n;if(ord)return t0&&n>=1&&n<=4?"one":"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ne-IN","parentLocale":"ne"});
addLocaleData({"locale":"nl","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"nl-AW","parentLocale":"nl"});
addLocaleData({"locale":"nl-BE","parentLocale":"nl"});
addLocaleData({"locale":"nl-BQ","parentLocale":"nl"});
addLocaleData({"locale":"nl-CW","parentLocale":"nl"});
addLocaleData({"locale":"nl-SR","parentLocale":"nl"});
addLocaleData({"locale":"nl-SX","parentLocale":"nl"});
addLocaleData({"locale":"nmg","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"nn","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"nnh","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"no","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"nqo","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"nr","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"nso","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0||n==1?"one":"other"}});
addLocaleData({"locale":"nus","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ny","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"nyn","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"om","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"om-KE","parentLocale":"om"});
addLocaleData({"locale":"or","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"os","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"os-RU","parentLocale":"os"});
addLocaleData({"locale":"pa","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0||n==1?"one":"other"}});
addLocaleData({"locale":"pa-Arab","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"pa-Guru","parentLocale":"pa"});
addLocaleData({"locale":"pap","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"pl","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],i10=i.slice(-1),i100=i.slice(-2);if(ord)return"other";return n==1&&v0?"one":v0&&(i10>=2&&i10<=4)&&(i100<12||i100>14)?"few":v0&&i!=1&&(i10==0||i10==1)||v0&&(i10>=5&&i10<=9)||v0&&(i100>=12&&i100<=14)?"many":"other"}});
addLocaleData({"locale":"prg","pluralRuleFunction":function (n,ord){var s=String(n).split("."),f=s[1]||"",v=f.length,t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2),f100=f.slice(-2),f10=f.slice(-1);if(ord)return"other";return t0&&n10==0||n100>=11&&n100<=19||v==2&&(f100>=11&&f100<=19)?"zero":n10==1&&n100!=11||v==2&&f10==1&&f100!=11||v!=2&&f10==1?"one":"other"}});
addLocaleData({"locale":"ps","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"pt","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n;if(ord)return"other";return t0&&n>=0&&n<=2&&n!=2?"one":"other"}});
addLocaleData({"locale":"pt-AO","parentLocale":"pt-PT"});
addLocaleData({"locale":"pt-PT","parentLocale":"pt","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"pt-CV","parentLocale":"pt-PT"});
addLocaleData({"locale":"pt-GW","parentLocale":"pt-PT"});
addLocaleData({"locale":"pt-MO","parentLocale":"pt-PT"});
addLocaleData({"locale":"pt-MZ","parentLocale":"pt-PT"});
addLocaleData({"locale":"pt-ST","parentLocale":"pt-PT"});
addLocaleData({"locale":"pt-TL","parentLocale":"pt-PT"});
addLocaleData({"locale":"qu","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"qu-BO","parentLocale":"qu"});
addLocaleData({"locale":"qu-EC","parentLocale":"qu"});
addLocaleData({"locale":"rm","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"rn","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ro","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n100=t0&&s[0].slice(-2);if(ord)return n==1?"one":"other";return n==1&&v0?"one":!v0||n==0||n!=1&&(n100>=1&&n100<=19)?"few":"other"}});
addLocaleData({"locale":"ro-MD","parentLocale":"ro"});
addLocaleData({"locale":"rof","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ru","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],i10=i.slice(-1),i100=i.slice(-2);if(ord)return"other";return v0&&i10==1&&i100!=11?"one":v0&&(i10>=2&&i10<=4)&&(i100<12||i100>14)?"few":v0&&i10==0||v0&&(i10>=5&&i10<=9)||v0&&(i100>=11&&i100<=14)?"many":"other"}});
addLocaleData({"locale":"ru-BY","parentLocale":"ru"});
addLocaleData({"locale":"ru-KG","parentLocale":"ru"});
addLocaleData({"locale":"ru-KZ","parentLocale":"ru"});
addLocaleData({"locale":"ru-MD","parentLocale":"ru"});
addLocaleData({"locale":"ru-UA","parentLocale":"ru"});
addLocaleData({"locale":"rw","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"rwk","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"sah","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"saq","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"sbp","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"sdh","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"se","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":n==2?"two":"other"}});
addLocaleData({"locale":"se-FI","parentLocale":"se"});
addLocaleData({"locale":"se-SE","parentLocale":"se"});
addLocaleData({"locale":"seh","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ses","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"sg","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"sh","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"",v0=!s[1],i10=i.slice(-1),i100=i.slice(-2),f10=f.slice(-1),f100=f.slice(-2);if(ord)return"other";return v0&&i10==1&&i100!=11||f10==1&&f100!=11?"one":v0&&(i10>=2&&i10<=4)&&(i100<12||i100>14)||f10>=2&&f10<=4&&(f100<12||f100>14)?"few":"other"}});
addLocaleData({"locale":"shi","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n;if(ord)return"other";return n>=0&&n<=1?"one":t0&&n>=2&&n<=10?"few":"other"}});
addLocaleData({"locale":"shi-Latn","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"shi-Tfng","parentLocale":"shi"});
addLocaleData({"locale":"si","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"";if(ord)return"other";return n==0||n==1||i==0&&f==1?"one":"other"}});
addLocaleData({"locale":"sk","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1];if(ord)return"other";return n==1&&v0?"one":i>=2&&i<=4&&v0?"few":!v0?"many":"other"}});
addLocaleData({"locale":"sl","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],i100=i.slice(-2);if(ord)return"other";return v0&&i100==1?"one":v0&&i100==2?"two":v0&&(i100==3||i100==4)||!v0?"few":"other"}});
addLocaleData({"locale":"sma","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":n==2?"two":"other"}});
addLocaleData({"locale":"smi","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":n==2?"two":"other"}});
addLocaleData({"locale":"smj","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":n==2?"two":"other"}});
addLocaleData({"locale":"smn","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":n==2?"two":"other"}});
addLocaleData({"locale":"sms","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":n==2?"two":"other"}});
addLocaleData({"locale":"sn","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"so","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"so-DJ","parentLocale":"so"});
addLocaleData({"locale":"so-ET","parentLocale":"so"});
addLocaleData({"locale":"so-KE","parentLocale":"so"});
addLocaleData({"locale":"sq","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return n==1?"one":n10==4&&n100!=14?"many":"other";return n==1?"one":"other"}});
addLocaleData({"locale":"sq-MK","parentLocale":"sq"});
addLocaleData({"locale":"sq-XK","parentLocale":"sq"});
addLocaleData({"locale":"sr","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"",v0=!s[1],i10=i.slice(-1),i100=i.slice(-2),f10=f.slice(-1),f100=f.slice(-2);if(ord)return"other";return v0&&i10==1&&i100!=11||f10==1&&f100!=11?"one":v0&&(i10>=2&&i10<=4)&&(i100<12||i100>14)||f10>=2&&f10<=4&&(f100<12||f100>14)?"few":"other"}});
addLocaleData({"locale":"sr-Cyrl","parentLocale":"sr"});
addLocaleData({"locale":"sr-Cyrl-BA","parentLocale":"sr-Cyrl"});
addLocaleData({"locale":"sr-Cyrl-ME","parentLocale":"sr-Cyrl"});
addLocaleData({"locale":"sr-Cyrl-XK","parentLocale":"sr-Cyrl"});
addLocaleData({"locale":"sr-Latn","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"sr-Latn-BA","parentLocale":"sr-Latn"});
addLocaleData({"locale":"sr-Latn-ME","parentLocale":"sr-Latn"});
addLocaleData({"locale":"sr-Latn-XK","parentLocale":"sr-Latn"});
addLocaleData({"locale":"ss","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ssy","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"st","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"sv","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2);if(ord)return(n10==1||n10==2)&&n100!=11&&n100!=12?"one":"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"sv-AX","parentLocale":"sv"});
addLocaleData({"locale":"sv-FI","parentLocale":"sv"});
addLocaleData({"locale":"sw","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"sw-CD","parentLocale":"sw"});
addLocaleData({"locale":"sw-KE","parentLocale":"sw"});
addLocaleData({"locale":"sw-UG","parentLocale":"sw"});
addLocaleData({"locale":"syr","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ta","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"ta-LK","parentLocale":"ta"});
addLocaleData({"locale":"ta-MY","parentLocale":"ta"});
addLocaleData({"locale":"ta-SG","parentLocale":"ta"});
addLocaleData({"locale":"te","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"teo","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"teo-KE","parentLocale":"teo"});
addLocaleData({"locale":"th","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"ti","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0||n==1?"one":"other"}});
addLocaleData({"locale":"ti-ER","parentLocale":"ti"});
addLocaleData({"locale":"tig","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"tk","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"tl","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],f=s[1]||"",v0=!s[1],i10=i.slice(-1),f10=f.slice(-1);if(ord)return n==1?"one":"other";return v0&&(i==1||i==2||i==3)||v0&&i10!=4&&i10!=6&&i10!=9||!v0&&f10!=4&&f10!=6&&f10!=9?"one":"other"}});
addLocaleData({"locale":"tn","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"to","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"tr","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"tr-CY","parentLocale":"tr"});
addLocaleData({"locale":"ts","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"twq","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"tzm","pluralRuleFunction":function (n,ord){var s=String(n).split("."),t0=Number(s[0])==n;if(ord)return"other";return n==0||n==1||t0&&n>=11&&n<=99?"one":"other"}});
addLocaleData({"locale":"ug","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"uk","pluralRuleFunction":function (n,ord){var s=String(n).split("."),i=s[0],v0=!s[1],t0=Number(s[0])==n,n10=t0&&s[0].slice(-1),n100=t0&&s[0].slice(-2),i10=i.slice(-1),i100=i.slice(-2);if(ord)return n10==3&&n100!=13?"few":"other";return v0&&i10==1&&i100!=11?"one":v0&&(i10>=2&&i10<=4)&&(i100<12||i100>14)?"few":v0&&i10==0||v0&&(i10>=5&&i10<=9)||v0&&(i100>=11&&i100<=14)?"many":"other"}});
addLocaleData({"locale":"ur","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"ur-IN","parentLocale":"ur"});
addLocaleData({"locale":"uz","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"uz-Arab","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"uz-Cyrl","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"uz-Latn","parentLocale":"uz"});
addLocaleData({"locale":"vai","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"vai-Latn","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"vai-Vaii","parentLocale":"vai"});
addLocaleData({"locale":"ve","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"vi","pluralRuleFunction":function (n,ord){if(ord)return n==1?"one":"other";return"other"}});
addLocaleData({"locale":"vo","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"vun","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"wa","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==0||n==1?"one":"other"}});
addLocaleData({"locale":"wae","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"wo","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"xh","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"xog","pluralRuleFunction":function (n,ord){if(ord)return"other";return n==1?"one":"other"}});
addLocaleData({"locale":"yav","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"yi","pluralRuleFunction":function (n,ord){var s=String(n).split("."),v0=!s[1];if(ord)return"other";return n==1&&v0?"one":"other"}});
addLocaleData({"locale":"yo","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"yo-BJ","parentLocale":"yo"});
addLocaleData({"locale":"zgh","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"zh","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"zh-Hans","parentLocale":"zh"});
addLocaleData({"locale":"zh-Hans-HK","parentLocale":"zh-Hans"});
addLocaleData({"locale":"zh-Hans-MO","parentLocale":"zh-Hans"});
addLocaleData({"locale":"zh-Hans-SG","parentLocale":"zh-Hans"});
addLocaleData({"locale":"zh-Hant","pluralRuleFunction":function (n,ord){if(ord)return"other";return"other"}});
addLocaleData({"locale":"zh-Hant-HK","parentLocale":"zh-Hant"});
addLocaleData({"locale":"zh-Hant-MO","parentLocale":"zh-Hant-HK"});
addLocaleData({"locale":"zu","pluralRuleFunction":function (n,ord){if(ord)return"other";return n>=0&&n<=1?"one":"other"}});

View file

@ -0,0 +1,26 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import * as i18nCore from './core/i18n';
import * as loader from './core/loader';
export { formats } from './core/formats';
export const i18n = i18nCore;
export const i18nLoader = loader;

View file

@ -0,0 +1,50 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { intlShape } from 'react-intl';
/**
* Provides intl context to a child component using React render callback pattern
* @example
* <I18nContext>
* {intl => (
* <input
* placeholder={intl.formatMessage({
id: 'my-id',
defaultMessage: 'my default message',
})}
* />
* )}
* </I18nContext>
*/
export class I18nContext extends PureComponent {
static propTypes = {
children: PropTypes.func.isRequired,
};
static contextTypes = {
intl: intlShape,
};
render() {
return this.props.children(this.context.intl);
}
}

View file

@ -0,0 +1,32 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
export {
intlShape,
FormattedDate,
FormattedTime,
FormattedRelative,
FormattedNumber,
FormattedPlural,
FormattedMessage,
FormattedHTMLMessage,
} from 'react-intl';
export { I18nProvider } from './provider';
export { I18nContext } from './context';

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import { IntlProvider } from 'react-intl';
import * as i18n from '../core/i18n';
/**
* The library uses the provider pattern to scope an i18n context to a tree
* of components. This component is used to setup the i18n context for a tree.
* IntlProvider should wrap react app's root component (inside each react render method).
*/
export class I18nProvider extends PureComponent {
static propTypes = {
children: PropTypes.object,
};
render() {
const { children } = this.props;
return (
<IntlProvider
locale={i18n.getLocale()}
messages={i18n.getMessages()}
defaultLocale={i18n.getDefaultLocale()}
formats={i18n.getFormats()}
defaultFormats={i18n.getFormats()}
>
{children}
</IntlProvider>
);
}
}

1745
packages/kbn-i18n/yarn.lock Normal file

File diff suppressed because it is too large Load diff

View file

@ -17,8 +17,6 @@
* under the License.
*/
import { resolve } from 'path';
import Promise from 'bluebird';
import { mkdirp as mkdirpNode } from 'mkdirp';
@ -123,9 +121,7 @@ export default function (kibana) {
};
},
translations: [
resolve(__dirname, './translations/en.json')
],
translations: [],
mappings,
uiSettingDefaults: getUiSettingDefaults(),

View file

@ -1,4 +0,0 @@
{
"UI-WELCOME_MESSAGE": "Loading Kibana",
"UI-WELCOME_ERROR": "Kibana did not load properly. Check the server output for more information."
}

View file

@ -53,3 +53,4 @@ import '../validate_date_interval';
import '../watch_multi';
import '../courier/saved_object/ui/saved_object_save_as_checkbox';
import '../react_components';
import '../i18n';

View file

@ -0,0 +1,36 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { AngularI18n } from '@kbn/i18n';
import { uiModules } from 'ui/modules';
import { metadata } from 'ui/metadata';
const {
i18nProvider,
i18nFilter,
i18nDirective,
} = AngularI18n;
uiModules.get('i18n')
.provider('i18n', i18nProvider)
.filter('i18n', i18nFilter)
.directive('i18nId', i18nDirective)
.config((i18nProvider) => {
i18nProvider.init(metadata.translations);
});

View file

@ -35,9 +35,7 @@ export const UI_EXPORT_DEFAULTS = {
'moment-timezone$': resolve(ROOT, 'webpackShims/moment-timezone')
},
translationPaths: [
resolve(ROOT, 'src/ui/ui_i18n/translations/en.json'),
],
translationPaths: [],
appExtensions: {
fieldFormatEditors: [

View file

@ -1,4 +0,0 @@
{
"test_plugin_1-NO_SSL": "Dont run the DE dev server using HTTPS",
"test_plugin_1-DEV": "Run the DE server with development mode defaults"
}

View file

@ -1,6 +0,0 @@
{
"test_plugin_1-NO_SSL": "Dont run the dev server using HTTPS",
"test_plugin_1-DEV": "Run the server with development mode defaults",
"test_plugin_1-NO_RUN_SERVER": "Dont run the dev server",
"test_plugin_1-HOME": "Run along home now!"
}

View file

@ -1,3 +0,0 @@
{
"test_plugin_1-NO_SSL": "Dont run the es-ES dev server using HTTPS! I am registered afterwards!"
}

View file

@ -1,3 +0,0 @@
{
"test_plugin_1-NO_SSL": "Dont run the DE dev server using HTTPS! I am registered afterwards!"
}

View file

@ -1,6 +0,0 @@
{
"test_plugin_2-XXXXXX": "This is XXXXXX string",
"test_plugin_2-YYYY_PPPP": "This is YYYY_PPPP string",
"test_plugin_2-FFFFFFFFFFFF": "This is FFFFFFFFFFFF string",
"test_plugin_2-ZZZ": "This is ZZZ string"
}

View file

@ -1,245 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import expect from 'expect.js';
import _ from 'lodash';
import { join } from 'path';
import { I18n } from '../';
const FIXTURES = join(__dirname, 'fixtures');
describe('ui/i18n module', function () {
describe('one plugin', function () {
const i18nObj = new I18n();
before('registerTranslations - one plugin', function () {
const pluginName = 'test_plugin_1';
const pluginTranslationPath = join(FIXTURES, 'translations', pluginName);
const translationFiles = [
join(pluginTranslationPath, 'de.json'),
join(pluginTranslationPath, 'en.json')
];
const filesLen = translationFiles.length;
for (let indx = 0; indx < filesLen; indx++) {
i18nObj.registerTranslations(translationFiles[indx]);
}
});
describe('getTranslations', function () {
it('should return the translations for en locale as registered', function () {
const languageTag = ['en'];
const expectedTranslationJson = {
'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS',
'test_plugin_1-DEV': 'Run the server with development mode defaults',
'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server',
'test_plugin_1-HOME': 'Run along home now!'
};
return checkTranslations(expectedTranslationJson, languageTag, i18nObj);
});
it('should return the translations for de locale as registered', function () {
const languageTag = ['de'];
const expectedTranslationJson = {
'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS',
'test_plugin_1-DEV': 'Run the DE server with development mode defaults'
};
return checkTranslations(expectedTranslationJson, languageTag, i18nObj);
});
it('should pick the highest priority language for which translations exist', function () {
const languageTags = ['es-ES', 'de', 'en'];
const expectedTranslations = {
'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS',
'test_plugin_1-DEV': 'Run the DE server with development mode defaults',
};
return checkTranslations(expectedTranslations, languageTags, i18nObj);
});
it('should return translations for highest priority locale where best case match is chosen from registered locales', function () {
const languageTags = ['es', 'de'];
const expectedTranslations = {
'test_plugin_1-NO_SSL': 'Dont run the es-ES dev server using HTTPS! I am registered afterwards!'
};
i18nObj.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_1', 'es-ES.json'));
return checkTranslations(expectedTranslations, languageTags, i18nObj);
});
it('should return an empty object for locales with no translations', function () {
const languageTags = ['ja-JA', 'fr'];
return checkTranslations({}, languageTags, i18nObj);
});
});
describe('getTranslationsForDefaultLocale', function () {
it('should return translations for default locale which is set to the en locale', function () {
const i18nObj1 = new I18n('en');
const expectedTranslations = {
'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS',
'test_plugin_1-DEV': 'Run the server with development mode defaults',
'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server',
'test_plugin_1-HOME': 'Run along home now!'
};
i18nObj1.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_1', 'en.json'));
return checkTranslationsForDefaultLocale(expectedTranslations, i18nObj1);
});
it('should return translations for default locale which is set to the de locale', function () {
const i18nObj1 = new I18n('de');
const expectedTranslations = {
'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS',
'test_plugin_1-DEV': 'Run the DE server with development mode defaults',
};
i18nObj1.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_1', 'de.json'));
return checkTranslationsForDefaultLocale(expectedTranslations, i18nObj1);
});
});
describe('getAllTranslations', function () {
it('should return all translations', function () {
const expectedTranslations = {
de: {
'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS',
'test_plugin_1-DEV': 'Run the DE server with development mode defaults'
},
en: {
'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS',
'test_plugin_1-DEV': 'Run the server with development mode defaults',
'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server',
'test_plugin_1-HOME': 'Run along home now!'
},
'es-ES': {
'test_plugin_1-NO_SSL': 'Dont run the es-ES dev server using HTTPS! I am registered afterwards!'
}
};
return checkAllTranslations(expectedTranslations, i18nObj);
});
});
});
describe('multiple plugins', function () {
const i18nObj = new I18n();
beforeEach('registerTranslations - multiple plugin', function () {
const pluginTranslationPath = join(FIXTURES, 'translations');
const translationFiles = [
join(pluginTranslationPath, 'test_plugin_1', 'de.json'),
join(pluginTranslationPath, 'test_plugin_1', 'en.json'),
join(pluginTranslationPath, 'test_plugin_2', 'en.json')
];
const filesLen = translationFiles.length;
for (let indx = 0; indx < filesLen; indx++) {
i18nObj.registerTranslations(translationFiles[indx]);
}
});
describe('getTranslations', function () {
it('should return the translations for en locale as registered', function () {
const languageTag = ['en'];
const expectedTranslationJson = {
'test_plugin_1-NO_SSL': 'Dont run the dev server using HTTPS',
'test_plugin_1-DEV': 'Run the server with development mode defaults',
'test_plugin_1-NO_RUN_SERVER': 'Dont run the dev server',
'test_plugin_1-HOME': 'Run along home now!',
'test_plugin_2-XXXXXX': 'This is XXXXXX string',
'test_plugin_2-YYYY_PPPP': 'This is YYYY_PPPP string',
'test_plugin_2-FFFFFFFFFFFF': 'This is FFFFFFFFFFFF string',
'test_plugin_2-ZZZ': 'This is ZZZ string'
};
return checkTranslations(expectedTranslationJson, languageTag, i18nObj);
});
it('should return the translations for de locale as registered', function () {
const languageTag = ['de'];
const expectedTranslationJson = {
'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS',
'test_plugin_1-DEV': 'Run the DE server with development mode defaults'
};
return checkTranslations(expectedTranslationJson, languageTag, i18nObj);
});
it('should return the most recently registered translation for a key that has multiple translations', function () {
i18nObj.registerTranslations(join(FIXTURES, 'translations', 'test_plugin_2', 'de.json'));
const languageTag = ['de'];
const expectedTranslationJson = {
'test_plugin_1-NO_SSL': 'Dont run the DE dev server using HTTPS! I am registered afterwards!',
'test_plugin_1-DEV': 'Run the DE server with development mode defaults'
};
return checkTranslations(expectedTranslationJson, languageTag, i18nObj);
});
});
});
describe('registerTranslations', function () {
const i18nObj = new I18n();
it('should throw error when registering relative path', function () {
return expect(i18nObj.registerTranslations).withArgs('./some/path').to.throwError();
});
it('should throw error when registering empty filename', function () {
return expect(i18nObj.registerTranslations).withArgs('').to.throwError();
});
it('should throw error when registering filename with no extension', function () {
return expect(i18nObj.registerTranslations).withArgs('file1').to.throwError();
});
it('should throw error when registering filename with non JSON extension', function () {
return expect(i18nObj.registerTranslations).withArgs('file1.txt').to.throwError();
});
});
});
function checkTranslations(expectedTranslations, languageTags, i18nObj) {
return i18nObj.getTranslations(...languageTags)
.then(function (actualTranslations) {
expect(_.isEqual(actualTranslations, expectedTranslations)).to.be(true);
});
}
function checkAllTranslations(expectedTranslations, i18nObj) {
return i18nObj.getAllTranslations()
.then(function (actualTranslations) {
expect(_.isEqual(actualTranslations, expectedTranslations)).to.be(true);
});
}
function checkTranslationsForDefaultLocale(expectedTranslations, i18nObj) {
return i18nObj.getTranslationsForDefaultLocale()
.then(function (actualTranslations) {
expect(_.isEqual(actualTranslations, expectedTranslations)).to.be(true);
});
}

View file

@ -1,164 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import path from 'path';
import Promise from 'bluebird';
import { readFile } from 'fs';
import _ from 'lodash';
const asyncReadFile = Promise.promisify(readFile);
const TRANSLATION_FILE_EXTENSION = '.json';
function getLocaleFromFileName(fullFileName) {
if (_.isEmpty(fullFileName)) throw new Error('Filename empty');
const fileExt = path.extname(fullFileName);
if (fileExt.length <= 0 || fileExt !== TRANSLATION_FILE_EXTENSION) {
throw new Error('Translations must be in a JSON file. File being registered is ' + fullFileName);
}
return path.basename(fullFileName, TRANSLATION_FILE_EXTENSION);
}
function getBestLocaleMatch(languageTag, registeredLocales) {
if (_.contains(registeredLocales, languageTag)) {
return languageTag;
}
// Find the first registered locale that begins with one of the language codes from the provided language tag.
// For example, if there is an 'en' language code, it would match an 'en-US' registered locale.
const languageCode = _.first(languageTag.split('-')) || [];
return _.find(registeredLocales, (locale) => _.startsWith(locale, languageCode));
}
export class I18n {
static async getAllTranslationsFromPaths(paths) {
const i18n = new I18n();
paths.forEach(path => {
i18n.registerTranslations(path);
});
return await i18n.getAllTranslations();
}
_registeredTranslations = {};
constructor(defaultLocale = 'en') {
this._defaultLocale = defaultLocale;
}
/**
* Return all translations for registered locales
* @return {Promise<Object>} translations - A Promise object where keys are
* the locale and values are Objects
* of translation keys and translations
*/
getAllTranslations() {
const localeTranslations = {};
const locales = this._getRegisteredTranslationLocales();
const translations = _.map(locales, (locale) => {
return this._getTranslationsForLocale(locale)
.then(function (translations) {
localeTranslations[locale] = translations;
});
});
return Promise.all(translations)
.then(() => _.assign({}, localeTranslations));
}
/**
* Return translations for a suitable locale from a user side locale list
* @param {...string} languageTags - BCP 47 language tags. The tags are listed in priority order as set in the Accept-Language header.
* @returns {Promise<Object>} translations - promise for an object where
* keys are translation keys and
* values are translations
* This object will contain all registered translations for the highest priority locale which is registered with the i18n module.
* This object can be empty if no locale in the language tags can be matched against the registered locales.
*/
getTranslations(...languageTags) {
const locale = this._getTranslationLocale(languageTags);
return this._getTranslationsForLocale(locale);
}
/**
* Return all translations registered for the default locale.
* @returns {Promise<Object>} translations - promise for an object where
* keys are translation keys and
* values are translations
*/
getTranslationsForDefaultLocale() {
return this._getTranslationsForLocale(this._defaultLocale);
}
/**
* The translation file is registered with i18n plugin. The plugin contains a list of registered translation file paths per language.
* @param {String} absolutePluginTranslationFilePath - Absolute path to the translation file to register.
*/
registerTranslations(absolutePluginTranslationFilePath) {
if (!path.isAbsolute(absolutePluginTranslationFilePath)) {
throw new TypeError(
'Paths to translation files must be absolute. ' +
`Got relative path: "${absolutePluginTranslationFilePath}"`
);
}
const locale = getLocaleFromFileName(absolutePluginTranslationFilePath);
this._registeredTranslations[locale] =
_.uniq(_.get(this._registeredTranslations, locale, []).concat(absolutePluginTranslationFilePath));
}
_getRegisteredTranslationLocales() {
return Object.keys(this._registeredTranslations);
}
_getTranslationLocale(languageTags) {
let locale = '';
const registeredLocales = this._getRegisteredTranslationLocales();
_.forEach(languageTags, (tag) => {
locale = locale || getBestLocaleMatch(tag, registeredLocales);
});
return locale;
}
_getTranslationsForLocale(locale) {
if (!this._registeredTranslations.hasOwnProperty(locale)) {
return Promise.resolve({});
}
const translationFiles = this._registeredTranslations[locale];
const translations = _.map(translationFiles, (filename) => {
return asyncReadFile(filename, 'utf8')
.then(fileContents => JSON.parse(fileContents))
.catch(SyntaxError, function () {
throw new Error('Invalid json in ' + filename);
})
.catch(function () {
throw new Error('Cannot read file ' + filename);
});
});
return Promise.all(translations)
.then(translations => _.assign({}, ...translations));
}
}

View file

@ -17,5 +17,48 @@
* under the License.
*/
export { I18n } from './i18n';
export { uiI18nMixin } from './ui_i18n_mixin';
/**
@typedef Messages - messages tree, where leafs are translated strings
@type {object<string, object>}
@property {string} [locale] - locale of the messages
@property {object} [formats] - set of options to the underlying formatter
*/
import { i18nLoader } from '@kbn/i18n';
export function uiI18nMixin(kbnServer, server, config) {
const defaultLocale = config.get('i18n.defaultLocale');
const { translationPaths = [] } = kbnServer.uiExports;
i18nLoader.registerTranslationFiles(translationPaths);
/**
* Fetch the translations matching the Accept-Language header for a requests.
* @name request.getUiTranslations
* @returns {Promise<Messages>} translations - translation messages
*/
server.decorate('request', 'getUiTranslations', async function () {
const header = this.headers['accept-language'];
const [defaultTranslations, requestedTranslations] = await Promise.all([
i18nLoader.getTranslationsByLocale(defaultLocale),
i18nLoader.getTranslationsByLanguageHeader(header),
]);
return {
locale: defaultLocale,
...defaultTranslations,
...requestedTranslations,
};
});
/**
* Return all translations for registered locales
* @name server.getAllUiTranslations
* @return {Promise<Map<string, Messages>>} translations - A Promise object
* where keys are the locale and values are objects of translation messages
*/
server.decorate('server', 'getAllUiTranslations', async () => {
return await i18nLoader.getAllTranslations();
});
}

View file

@ -1,4 +0,0 @@
{
"UI-WELCOME_MESSAGE": "Loading",
"UI-WELCOME_ERROR": ""
}

View file

@ -1,68 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import { defaults, compact } from 'lodash';
import langParser from 'accept-language-parser';
import { I18n } from './i18n';
function acceptLanguageHeaderToBCP47Tags(header) {
return langParser.parse(header).map(lang => (
compact([lang.code, lang.region, lang.script]).join('-')
));
}
export function uiI18nMixin(kbnServer, server, config) {
const defaultLocale = config.get('i18n.defaultLocale');
const i18n = new I18n(defaultLocale);
const { translationPaths = [] } = kbnServer.uiExports;
translationPaths.forEach(translationPath => {
i18n.registerTranslations(translationPath);
});
/**
* Fetch the translations matching the Accept-Language header for a requests.
* @name request.getUiTranslations
* @returns {Promise<Object<id:string,value:string>>} translations
*/
server.decorate('request', 'getUiTranslations', async function () {
const header = this.headers['accept-language'];
const tags = acceptLanguageHeaderToBCP47Tags(header);
const requestedTranslations = await i18n.getTranslations(...tags);
const defaultTranslations = await i18n.getTranslationsForDefaultLocale();
return defaults(
{},
requestedTranslations,
defaultTranslations
);
});
/**
* Return all translations for registered locales
* @name server.getAllUiTranslations
* @return {Promise<Object<locale:string,Object<id:string,value:string>>>}
*/
server.decorate('server', 'getAllUiTranslations', async () => {
return await i18n.getAllTranslations();
});
}

View file

@ -17,17 +17,17 @@
* under the License.
*/
import _ from 'lodash';
import Handlebars from 'handlebars';
import { createHash } from 'crypto';
import { readFile } from 'fs';
import { resolve } from 'path';
import { i18n } from '@kbn/i18n';
export class AppBootstrap {
constructor({ templateData, translations }) {
this.templateData = templateData;
this.translations = translations;
this._rawTemplate = undefined;
i18n.init(translations);
}
async getJsFile() {
@ -35,7 +35,7 @@ export class AppBootstrap {
this._rawTemplate = await loadRawTemplate();
}
Handlebars.registerHelper('i18n', key => _.get(this.translations, key, ''));
Handlebars.registerHelper('i18n', (id, options) => i18n.translate(id, JSON.parse(options)));
const template = Handlebars.compile(this._rawTemplate, {
knownHelpers: { i18n: true },
knownHelpersOnly: true,

View file

@ -23,7 +23,7 @@ import { resolve } from 'path';
const mockTemplate = `
{{appId}}
{{bundlePath}}
{{i18n 'foo'}}
{{i18n 'foo' '{"defaultMessage": "bar"}'}}
`;
const templatePath = resolve(__dirname, 'template.js.hbs');
@ -131,6 +131,7 @@ describe('ui_render/AppBootstrap', () => {
const config2 = {
...mockConfig(),
translations: {
locale: 'en',
foo: 'not translated foo'
}
};
@ -147,6 +148,7 @@ describe('ui_render/AppBootstrap', () => {
function mockConfig() {
return {
translations: {
locale: 'en',
foo: 'translated foo'
},
templateData: {

View file

@ -33,7 +33,7 @@ window.onload = function () {
err.style['text-align'] = 'center';
err.style['background'] = '#F44336';
err.style['padding'] = '25px';
err.innerText = '{{i18n 'UI-WELCOME_ERROR'}}';
err.innerText = '{{i18n 'UI-WELCOME_ERROR' '{"defaultMessage": "Kibana did not load properly. Check the server output for more information."}'}}';
document.body.innerHTML = err.outerHTML;
}

View file

@ -17,10 +17,11 @@
* under the License.
*/
import { defaults, get } from 'lodash';
import { defaults } from 'lodash';
import { props, reduce as reduceAsync } from 'bluebird';
import Boom from 'boom';
import { resolve } from 'path';
import { i18n } from '@kbn/i18n';
import { AppBootstrap } from './bootstrap';
export function uiRenderMixin(kbnServer, server, config) {
@ -141,6 +142,8 @@ export function uiRenderMixin(kbnServer, server, config) {
const request = reply.request;
const translations = await request.getUiTranslations();
i18n.init(translations);
return reply.view('ui_app', {
app,
kibanaPayload: await getKibanaPayload({
@ -150,7 +153,7 @@ export function uiRenderMixin(kbnServer, server, config) {
injectedVarsOverrides
}),
bundlePath: `${config.get('server.basePath')}/bundles`,
i18n: key => get(translations, key, ''),
i18n: (id, options) => i18n.translate(id, options),
});
} catch (err) {
reply(err);

View file

@ -108,6 +108,6 @@ block content
.kibanaWelcomeLogoCircle
.kibanaWelcomeLogo
.kibanaWelcomeText
| #{i18n('UI-WELCOME_MESSAGE')}
| #{i18n('UI-WELCOME_MESSAGE', { defaultMessage: 'Loading Kibana' })}
script(src='#{bundlePath}/app/#{app.getId()}/bootstrap.js')

View file

@ -17,11 +17,14 @@
* under the License.
*/
// TODO: Integrate a new tool for translations checking
// https://github.com/elastic/kibana/pull/19826
import { i18nLoader } from '@kbn/i18n';
import { toArray } from 'rxjs/operators';
import { fromRoot, formatListAsProse } from '../src/utils';
import { findPluginSpecs } from '../src/plugin_discovery';
import { collectUiExports } from '../src/ui';
import { I18n } from '../src/ui/ui_i18n/i18n';
import * as i18nVerify from './utils/i18n_verify_keys';
@ -65,7 +68,7 @@ async function verifyTranslations(uiExports) {
}
// get all of the translations from uiExports
const translations = await I18n.getAllTranslationsFromPaths(uiExports.translationPaths);
const translations = await i18nLoader.getAllTranslationsFromPaths(uiExports.translationPaths);
const keysWithoutTranslations = Object.entries(
i18nVerify.getNonTranslatedKeys(keysUsedInViews, translations)
);

View file

@ -144,6 +144,10 @@
version "0.0.0"
uid ""
"@kbn/i18n@link:packages/kbn-i18n":
version "0.0.0"
uid ""
"@kbn/plugin-generator@link:packages/kbn-plugin-generator":
version "0.0.0"
uid ""
@ -441,9 +445,9 @@ abbrev@1.0.x:
version "1.0.9"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135"
accept-language-parser@1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/accept-language-parser/-/accept-language-parser-1.2.0.tgz#6a18942acab3f090a4a09590e03101a99fa22bff"
accept-language-parser@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/accept-language-parser/-/accept-language-parser-1.5.0.tgz#8877c54040a8dcb59e0a07d9c1fde42298334791"
accept@2.x.x:
version "2.1.4"
@ -6447,6 +6451,26 @@ interpret@^1.0.0:
version "1.1.0"
resolved "https://registry.yarnpkg.com/interpret/-/interpret-1.1.0.tgz#7ed1b1410c6a0e0f78cf95d3b8440c63f78b8614"
intl-format-cache@^2.0.5, intl-format-cache@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/intl-format-cache/-/intl-format-cache-2.1.0.tgz#04a369fecbfad6da6005bae1f14333332dcf9316"
intl-messageformat-parser@1.4.0:
version "1.4.0"
resolved "https://registry.yarnpkg.com/intl-messageformat-parser/-/intl-messageformat-parser-1.4.0.tgz#b43d45a97468cadbe44331d74bb1e8dea44fc075"
intl-messageformat@^2.0.0, intl-messageformat@^2.1.0, intl-messageformat@^2.2.0:
version "2.2.0"
resolved "https://registry.yarnpkg.com/intl-messageformat/-/intl-messageformat-2.2.0.tgz#345bcd46de630b7683330c2e52177ff5eab484fc"
dependencies:
intl-messageformat-parser "1.4.0"
intl-relativeformat@^2.0.0, intl-relativeformat@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.1.0.tgz#010f1105802251f40ac47d0e3e1a201348a255df"
dependencies:
intl-messageformat "^2.0.0"
into-stream@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/into-stream/-/into-stream-3.1.0.tgz#96fb0a936c12babd6ff1752a17d05616abd094c6"
@ -6454,7 +6478,7 @@ into-stream@^3.1.0:
from2 "^2.1.1"
p-is-promise "^1.1.0"
invariant@^2.0.0, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
invariant@^2.0.0, invariant@^2.1.1, invariant@^2.2.0, invariant@^2.2.1, invariant@^2.2.2:
version "2.2.4"
resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6"
dependencies:
@ -7596,6 +7620,12 @@ json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
dependencies:
minimist "^1.2.0"
jsonfile@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
@ -10586,6 +10616,15 @@ react-input-range@^1.3.0:
autobind-decorator "^1.3.4"
prop-types "^15.5.8"
react-intl@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.4.0.tgz#66c14dc9df9a73b2fbbfbd6021726e80a613eb15"
dependencies:
intl-format-cache "^2.0.5"
intl-messageformat "^2.1.0"
intl-relativeformat "^2.0.0"
invariant "^2.1.1"
react-is@^16.4.0:
version "16.4.0"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.0.tgz#cc9fdc855ac34d2e7d9d2eb7059bbc240d35ffcf"