Set kibana locale in kibana.yml config (#21201)

* set kibana locale in kibana.yml config

* remove accept-language-parser

* remove unnecessary tests

* fix readme description, fix description for locale in kibana.yml

* add point, that i18n.locale option should have exact match

* update kbn/i18n README

* Update README.md

* use getUiTranslations in render_mixin, remove i18n_mixin

* move registering translation files to mixin function
This commit is contained in:
Aliaksandr Yankouski 2018-08-02 14:43:22 +03:00 committed by GitHub
parent f43ffbc252
commit 9f3e36b170
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 34 additions and 172 deletions

View file

@ -109,6 +109,5 @@
# metrics. Minimum is 100ms. Defaults to 5000.
#ops.interval: 5000
# The default locale. This locale can be used in certain circumstances to substitute any missing
# translations.
#i18n.defaultLocale: "en"
# Specifies locale to be used for all localizable strings, dates and number formats.
#i18n.locale: "en"

View file

@ -72,25 +72,21 @@ export default function (kibana) {
}
```
The engine uses a locale resolution process similar to that of the built-in
Intl APIs to determine which locale data to use based on the `accept-language`
http header.
The engine uses a `config/kibana.yml` file for locale resolution process. If locale is
defined via `i18n.locale` option in `config/kibana.yml` then it will be used as a base
locale, otherwise i18n engine will fall back to `en`. The `en` locale will also be used
if translation can't be found for the base non-English locale.
The following are the abstract steps i18n engine goes through to resolve the locale value:
- If there's data for the specified locale (localization file is registered in
`uiExports.translations`), then that locale will be resolved.
- If locale data is missing for a leaf locale like `fr-FR`, but there is data
for one of its ancestors, `fr` in this case, then its ancestor will be used.
- If `accept-language` header is not presented or previous steps didn't resolve
the locale, the locale will be resolved to locale defined in `i18n.defaultLocale`
option at `config/kibana.yml` file.
One of our technical requirements is to have default message in the templates
themselves, and that message will always be english, so we don't need interact
with `en.json` file directly. We can generate that file from `defaultMessage`s
One of our technical requirements is to have default messages in the templates
themselves, and those messages will always be in English, so we don't have to keep
`en.json` file in repository. We can generate that file from `defaultMessage`s
defined inline.
__Note:__ locale defined in `i18n.locale` and the one used for translation files should
match exactly, e.g. `i18n.locale: zn` and `.../translations/zh_CN.json` won't match and
default English translations will be used, but `i18n.locale: zh_CN` and`.../translations/zh_CN.json`
or `i18n.locale: zn` and `.../translations/zn.json` will work as expected.
## I18n engine
I18n engine is the platform agnostic abstraction that helps to supply locale

View file

@ -20,7 +20,6 @@
"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",

View file

@ -27,7 +27,6 @@
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 './core/helper';
@ -80,16 +79,6 @@ 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
@ -139,18 +128,6 @@ 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

View file

@ -164,53 +164,6 @@ describe('I18n loader', () => {
});
});
describe('getTranslationsByLanguageHeader', () => {
test('should return empty object if there are no registered locales', async () => {
expect(
await i18nLoader.getTranslationsByLanguageHeader('en-GB,en-US;q=0.9,fr-CA;q=0.7,en;q=0.8')
).toEqual({});
});
test('should return empty object if registered locales do not match to accept-language header', async () => {
i18nLoader.registerTranslationFile(
join(__dirname, './__fixtures__/test_plugin_2/translations/ru.json')
);
expect(
await i18nLoader.getTranslationsByLanguageHeader('en-GB,en-US;q=0.9,fr-CA;q=0.7,en;q=0.8')
).toEqual({});
});
test('should return translation messages for the only matched locale', async () => {
i18nLoader.registerTranslationFile(
join(__dirname, './__fixtures__/test_plugin_1/translations/en.json')
);
expect(
await i18nLoader.getTranslationsByLanguageHeader('en-GB,en-US;q=0.9,fr-CA;q=0.7,en;q=0.8')
).toEqual({
locale: 'en',
['a.b.c']: 'foo',
['d.e.f']: 'bar',
});
});
test('should return translation messages for the best matched locale', async () => {
i18nLoader.registerTranslationFiles([
join(__dirname, './__fixtures__/test_plugin_1/translations/en.json'),
join(__dirname, './__fixtures__/test_plugin_1/translations/en-US.json'),
]);
expect(
await i18nLoader.getTranslationsByLanguageHeader('en-GB,en-US;q=0.9,fr-CA;q=0.7,en;q=0.8')
).toEqual({
locale: 'en-US',
['a.b.c']: 'bar',
['d.e.f']: 'foo',
});
});
});
describe('getAllTranslations', () => {
test('should return translation messages for all registered locales', async () => {
i18nLoader.registerTranslationFiles([

View file

@ -14,10 +14,6 @@ abbrev@1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8"
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"
ansi-regex@^2.0.0:
version "2.1.1"
resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-2.1.1.tgz#c3b33ab5ee360d86e0e628f0468ae7ef27d654df"

View file

@ -269,7 +269,7 @@ export default async () => Joi.object({
}).notes('Deprecated'),
i18n: Joi.object({
defaultLocale: Joi.string().default('en'),
locale: Joi.string().default('en'),
}).default(),
// This is a configuration node that is specifically handled by the config system

View file

@ -1,64 +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.
*/
/**
@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

@ -21,7 +21,6 @@ import { uiExportsMixin } from './ui_exports';
import { fieldFormatsMixin } from './field_formats';
import { tutorialsMixin } from './tutorials_mixin';
import { uiAppsMixin } from './ui_apps';
import { uiI18nMixin } from './ui_i18n';
import { uiBundlesMixin } from './ui_bundles';
import { uiNavLinksMixin } from './ui_nav_links';
import { uiRenderMixin } from './ui_render';
@ -35,6 +34,5 @@ export async function uiMixin(kbnServer) {
await kbnServer.mixin(fieldFormatsMixin);
await kbnServer.mixin(tutorialsMixin);
await kbnServer.mixin(uiNavLinksMixin);
await kbnServer.mixin(uiI18nMixin);
await kbnServer.mixin(uiRenderMixin);
}

View file

@ -21,10 +21,22 @@ 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 { i18n, i18nLoader } from '@kbn/i18n';
import { AppBootstrap } from './bootstrap';
export function uiRenderMixin(kbnServer, server, config) {
const { translationPaths = [] } = kbnServer.uiExports;
i18nLoader.registerTranslationFiles(translationPaths);
async function getUiTranslations() {
const locale = config.get('i18n.locale');
const translations = await i18nLoader.getTranslationsByLocale(locale);
return {
locale,
...translations,
};
}
function replaceInjectedVars(request, injectedVars) {
const { injectedVarsReplacers = [] } = kbnServer.uiExports;
@ -70,7 +82,7 @@ export function uiRenderMixin(kbnServer, server, config) {
bundlePath: `${basePath}/bundles`,
styleSheetPath: app.getStyleSheetUrlPath() ? `${basePath}/${app.getStyleSheetUrlPath()}` : null,
},
translations: await request.getUiTranslations()
translations: await getUiTranslations()
});
const body = await bootstrap.getJsFile();
@ -106,12 +118,12 @@ export function uiRenderMixin(kbnServer, server, config) {
}
});
async function getLegacyKibanaPayload({ app, request, includeUserProvidedConfig, injectedVarsOverrides }) {
async function getLegacyKibanaPayload({ app, translations, request, includeUserProvidedConfig, injectedVarsOverrides }) {
const uiSettings = request.getUiSettingsService();
const translations = await request.getUiTranslations();
return {
app: app,
app,
translations,
bundleId: `app:${app.getId()}`,
nav: server.getUiNavLinks(),
version: kbnServer.version,
@ -121,7 +133,6 @@ export function uiRenderMixin(kbnServer, server, config) {
basePath: config.get('server.basePath'),
serverName: config.get('server.name'),
devMode: config.get('env.dev'),
translations: translations,
uiSettings: await props({
defaults: uiSettings.getDefaults(),
user: includeUserProvidedConfig && uiSettings.getUserProvided()
@ -140,7 +151,7 @@ export function uiRenderMixin(kbnServer, server, config) {
async function renderApp({ app, reply, includeUserProvidedConfig = true, injectedVarsOverrides = {} }) {
try {
const request = reply.request;
const translations = await request.getUiTranslations();
const translations = await getUiTranslations();
const basePath = config.get('server.basePath');
i18n.init(translations);
@ -155,6 +166,7 @@ export function uiRenderMixin(kbnServer, server, config) {
buildNumber: config.get('pkg.buildNum'),
legacyMetadata: await getLegacyKibanaPayload({
app,
translations,
request,
includeUserProvidedConfig,
injectedVarsOverrides

View file

@ -573,10 +573,6 @@ abortcontroller-polyfill@^1.1.9:
version "1.1.9"
resolved "https://registry.yarnpkg.com/abortcontroller-polyfill/-/abortcontroller-polyfill-1.1.9.tgz#9fefe359fda2e9e0932dc85e6106453ac393b2da"
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"
resolved "https://registry.yarnpkg.com/accept/-/accept-2.1.4.tgz#887af54ceee5c7f4430461971ec400c61d09acbb"