mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
f43ffbc252
commit
9f3e36b170
11 changed files with 34 additions and 172 deletions
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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([
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue