diff --git a/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en-US.json b/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en-US.json index a25da0165b407d..5cd8dd88e36a26 100644 --- a/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en-US.json +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en-US.json @@ -1,4 +1,6 @@ { - "a.b.c": "bar", - "d.e.f": "foo" + "messages": { + "a.b.c": "bar", + "d.e.f": "foo" + } } diff --git a/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en.json b/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en.json index eaec1a6d8a0964..36aa3aa450c8f9 100644 --- a/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en.json +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en.json @@ -1,4 +1,6 @@ { - "a.b.c": "foo", - "d.e.f": "bar" + "messages": { + "a.b.c": "foo", + "d.e.f": "bar" + } } diff --git a/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/en.json b/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/en.json index 85a62e3df1a48f..9530a5967eb99b 100644 --- a/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/en.json +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/en.json @@ -1,4 +1,6 @@ { - "a.b.c.custom": "foo.custom", - "d.e.f.custom": "bar.custom" + "messages": { + "a.b.c.custom": "foo.custom", + "d.e.f.custom": "bar.custom" + } } diff --git a/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/fr.json b/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/fr.json index 04e20542e75e39..88d3f27d9126c8 100644 --- a/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/fr.json +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/fr.json @@ -1,3 +1,5 @@ { - test: 'test' // JSON5 test + messages: { + test: 'test' // JSON5 test + } } diff --git a/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/ru.json b/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/ru.json index d0ae716dbe4c32..e6207873f600ac 100644 --- a/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/ru.json +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/ru.json @@ -1,3 +1,5 @@ { - "test": "test" + "messages": { + "test": "test" + } } diff --git a/packages/kbn-i18n/src/angular/provider.ts b/packages/kbn-i18n/src/angular/provider.ts index b239afece4d291..44f0974ac81432 100644 --- a/packages/kbn-i18n/src/angular/provider.ts +++ b/packages/kbn-i18n/src/angular/provider.ts @@ -22,8 +22,8 @@ import * as i18n from '../core'; export type I18nServiceType = ReturnType; export class I18nProvider implements angular.IServiceProvider { - public addMessages = i18n.addMessages; - public getMessages = i18n.getMessages; + public addTranslation = i18n.addTranslation; + public getTranslation = i18n.getTranslation; public setLocale = i18n.setLocale; public getLocale = i18n.getLocale; public setDefaultLocale = i18n.setDefaultLocale; diff --git a/packages/kbn-i18n/src/core/__snapshots__/i18n.test.ts.snap b/packages/kbn-i18n/src/core/__snapshots__/i18n.test.ts.snap index 895cd575d3513b..30d4f3f8850536 100644 --- a/packages/kbn-i18n/src/core/__snapshots__/i18n.test.ts.snap +++ b/packages/kbn-i18n/src/core/__snapshots__/i18n.test.ts.snap @@ -4,7 +4,7 @@ exports[`I18n engine addMessages should throw error if locale is not specified o exports[`I18n engine addMessages should throw error if locale is not specified or empty 2`] = `"[I18n] A \`locale\` must be a non-empty string to add messages."`; -exports[`I18n engine addMessages should throw error if locale specified in messages is different from one provided as second argument 1`] = `"[I18n] A \`locale\` in the messages object is different from the one provided as a second argument."`; +exports[`I18n engine addMessages should throw error if locale specified in messages is different from one provided as second argument 1`] = `"[I18n] A \`locale\` in the translation object is different from the one provided as a second argument."`; exports[`I18n engine translate should throw error if id is not a non-empty string 1`] = `"[I18n] An \`id\` must be a non-empty string to translate a message."`; diff --git a/packages/kbn-i18n/src/core/i18n.test.ts b/packages/kbn-i18n/src/core/i18n.test.ts index 24c771aeeecba2..c6c41c9325992a 100644 --- a/packages/kbn-i18n/src/core/i18n.test.ts +++ b/packages/kbn-i18n/src/core/i18n.test.ts @@ -33,46 +33,54 @@ describe('I18n engine', () => { describe('addMessages', () => { test('should throw error if locale is not specified or empty', () => { - expect(() => i18n.addMessages({ foo: 'bar' })).toThrowErrorMatchingSnapshot(); - expect(() => i18n.addMessages({ locale: '' })).toThrowErrorMatchingSnapshot(); + expect(() => + i18n.addTranslation({ messages: { foo: 'bar' } }) + ).toThrowErrorMatchingSnapshot(); + expect(() => + i18n.addTranslation({ locale: '', messages: {} }) + ).toThrowErrorMatchingSnapshot(); }); test('should throw error if locale specified in messages is different from one provided as second argument', () => { expect(() => - i18n.addMessages({ foo: 'bar', locale: 'en' }, 'ru') + i18n.addTranslation({ messages: { foo: 'bar' }, locale: 'en' }, 'ru') ).toThrowErrorMatchingSnapshot(); }); test('should add messages if locale prop is passed as second argument', () => { const locale = 'ru'; - expect(i18n.getMessages()).toEqual({}); + expect(i18n.getTranslation()).toEqual({ messages: {} }); - i18n.addMessages({ foo: 'bar' }, locale); + i18n.addTranslation({ messages: { foo: 'bar' } }, locale); - expect(i18n.getMessages()).toEqual({}); + expect(i18n.getTranslation()).toEqual({ messages: {} }); i18n.setLocale(locale); - expect(i18n.getMessages()).toEqual({ foo: 'bar' }); + expect(i18n.getTranslation()).toEqual({ messages: { foo: 'bar' } }); }); test('should add messages if locale prop is passed as messages property', () => { const locale = 'ru'; - expect(i18n.getMessages()).toEqual({}); + expect(i18n.getTranslation()).toEqual({ messages: {} }); - i18n.addMessages({ + i18n.addTranslation({ locale, - foo: 'bar', + messages: { + foo: 'bar', + }, }); - expect(i18n.getMessages()).toEqual({}); + expect(i18n.getTranslation()).toEqual({ messages: {} }); i18n.setLocale(locale); - expect(i18n.getMessages()).toEqual({ - foo: 'bar', + expect(i18n.getTranslation()).toEqual({ + messages: { + foo: 'bar', + }, locale: 'ru', }); }); @@ -81,25 +89,33 @@ describe('I18n engine', () => { const locale = 'ru'; i18n.setLocale(locale); - i18n.addMessages({ + i18n.addTranslation({ locale, - ['a.b.c']: 'foo', + messages: { + ['a.b.c']: 'foo', + }, }); - expect(i18n.getMessages()).toEqual({ + expect(i18n.getTranslation()).toEqual({ locale: 'ru', - ['a.b.c']: 'foo', + messages: { + ['a.b.c']: 'foo', + }, }); - i18n.addMessages({ + i18n.addTranslation({ locale, - ['d.e.f']: 'bar', + messages: { + ['d.e.f']: 'bar', + }, }); - expect(i18n.getMessages()).toEqual({ + expect(i18n.getTranslation()).toEqual({ locale: 'ru', - ['a.b.c']: 'foo', - ['d.e.f']: 'bar', + messages: { + ['a.b.c']: 'foo', + ['d.e.f']: 'bar', + }, }); }); @@ -107,24 +123,32 @@ describe('I18n engine', () => { const locale = 'ru'; i18n.setLocale(locale); - i18n.addMessages({ + i18n.addTranslation({ locale, - ['a.b.c']: 'foo', + messages: { + ['a.b.c']: 'foo', + }, }); - expect(i18n.getMessages()).toEqual({ + expect(i18n.getTranslation()).toEqual({ locale: 'ru', - ['a.b.c']: 'foo', + messages: { + ['a.b.c']: 'foo', + }, }); - i18n.addMessages({ + i18n.addTranslation({ locale, - ['a.b.c']: 'bar', + messages: { + ['a.b.c']: 'bar', + }, }); - expect(i18n.getMessages()).toEqual({ + expect(i18n.getTranslation()).toEqual({ locale: 'ru', - ['a.b.c']: 'bar', + messages: { + ['a.b.c']: 'bar', + }, }); }); @@ -132,52 +156,64 @@ describe('I18n engine', () => { const locale = 'en-us'; i18n.setLocale(locale); - i18n.addMessages( + i18n.addTranslation( { - ['a.b.c']: 'bar', + messages: { + ['a.b.c']: 'bar', + }, }, 'en_US' ); expect(i18n.getLocale()).toBe(locale); - expect(i18n.getMessages()).toEqual({ - ['a.b.c']: 'bar', + expect(i18n.getTranslation()).toEqual({ + messages: { + ['a.b.c']: 'bar', + }, }); }); }); - describe('getMessages', () => { + describe('getTranslation', () => { test('should return messages for the current language', () => { - i18n.addMessages({ + i18n.addTranslation({ locale: 'ru', - foo: 'bar', + messages: { + foo: 'bar', + }, }); - i18n.addMessages({ + i18n.addTranslation({ locale: 'en', - bar: 'foo', + messages: { + bar: 'foo', + }, }); i18n.setLocale('ru'); - expect(i18n.getMessages()).toEqual({ + expect(i18n.getTranslation()).toEqual({ locale: 'ru', - foo: 'bar', + messages: { + foo: 'bar', + }, }); i18n.setLocale('en'); - expect(i18n.getMessages()).toEqual({ + expect(i18n.getTranslation()).toEqual({ locale: 'en', - bar: 'foo', + messages: { + bar: 'foo', + }, }); }); test('should return an empty object if messages for current locale are not specified', () => { - expect(i18n.getMessages()).toEqual({}); + expect(i18n.getTranslation()).toEqual({ messages: {} }); i18n.setLocale('fr'); - expect(i18n.getMessages()).toEqual({}); + expect(i18n.getTranslation()).toEqual({ messages: {} }); i18n.setLocale('en'); - expect(i18n.getMessages()).toEqual({}); + expect(i18n.getTranslation()).toEqual({ messages: {} }); }); }); @@ -352,22 +388,25 @@ describe('I18n engine', () => { }); test('should return array of registered locales', () => { - i18n.addMessages({ + i18n.addTranslation({ locale: 'en', + messages: {}, }); expect(i18n.getRegisteredLocales()).toEqual(['en']); - i18n.addMessages({ + i18n.addTranslation({ locale: 'ru', + messages: {}, }); expect(i18n.getRegisteredLocales()).toContain('en'); expect(i18n.getRegisteredLocales()).toContain('ru'); expect(i18n.getRegisteredLocales().length).toBe(2); - i18n.addMessages({ + i18n.addTranslation({ locale: 'fr', + messages: {}, }); expect(i18n.getRegisteredLocales()).toContain('en'); @@ -394,7 +433,9 @@ describe('I18n engine', () => { test('should return message as is if values are not provided', () => { i18n.init({ locale: 'en', - ['a.b.c']: 'foo', + messages: { + ['a.b.c']: 'foo', + }, }); expect(i18n.translate('a.b.c')).toBe('foo'); @@ -407,7 +448,9 @@ describe('I18n engine', () => { test('should not return defaultMessage as is if values are provided', () => { i18n.init({ locale: 'en', - ['a.b.c']: 'foo', + messages: { + ['a.b.c']: 'foo', + }, }); expect(i18n.translate('a.b.c', { defaultMessage: 'bar' })).toBe('foo'); }); @@ -415,8 +458,10 @@ describe('I18n engine', () => { test('should interpolate variables', () => { i18n.init({ locale: 'en', - ['a.b.c']: 'foo {a}, {b}, {c} bar', - ['d.e.f']: '{foo}', + messages: { + ['a.b.c']: 'foo {a}, {b}, {c} bar', + ['d.e.f']: '{foo}', + }, }); expect( @@ -440,11 +485,13 @@ describe('I18n engine', () => { test('should format pluralized messages', () => { i18n.init({ locale: 'en', - ['a.b.c']: `You have {numPhotos, plural, + messages: { + ['a.b.c']: `You have {numPhotos, plural, =0 {no photos.} =1 {one photo.} other {# photos.} }`, + }, }); expect(i18n.translate('a.b.c', { values: { numPhotos: 0 } })).toBe('You have no photos.'); @@ -494,11 +541,13 @@ describe('I18n engine', () => { test('should throw error if wrong context is provided to the translation string', () => { i18n.init({ locale: 'en', - ['a.b.c']: `You have {numPhotos, plural, + messages: { + ['a.b.c']: `You have {numPhotos, plural, =0 {no photos.} =1 {one photo.} other {# photos.} }`, + }, }); i18n.setDefaultLocale('en'); @@ -519,7 +568,9 @@ describe('I18n engine', () => { test('should format messages with percent formatter', () => { i18n.init({ locale: 'en', - ['a.b.c']: 'Result: {result, number, percent}', + messages: { + ['a.b.c']: 'Result: {result, number, percent}', + }, }); i18n.setDefaultLocale('en'); @@ -536,10 +587,12 @@ describe('I18n engine', () => { test('should format messages with date formatter', () => { i18n.init({ locale: 'en', - ['a.short']: 'Sale begins {start, date, short}', - ['a.medium']: 'Sale begins {start, date, medium}', - ['a.long']: 'Sale begins {start, date, long}', - ['a.full']: 'Sale begins {start, date, full}', + messages: { + ['a.short']: 'Sale begins {start, date, short}', + ['a.medium']: 'Sale begins {start, date, medium}', + ['a.long']: 'Sale begins {start, date, long}', + ['a.full']: 'Sale begins {start, date, full}', + }, }); expect( @@ -602,8 +655,10 @@ describe('I18n engine', () => { test('should format messages with time formatter', () => { i18n.init({ locale: 'en', - ['a.short']: 'Coupon expires at {expires, time, short}', - ['a.medium']: 'Coupon expires at {expires, time, medium}', + messages: { + ['a.short']: 'Coupon expires at {expires, time, short}', + ['a.medium']: 'Coupon expires at {expires, time, medium}', + }, }); expect( @@ -645,8 +700,10 @@ describe('I18n engine', () => { usd: { style: 'currency', currency: 'USD' }, }, }, - ['a.b.c']: 'Your total is {total, number, usd}', - ['d.e.f']: 'Your total is {total, number, eur}', + messages: { + ['a.b.c']: 'Your total is {total, number, usd}', + ['d.e.f']: 'Your total is {total, number, eur}', + }, }); expect(i18n.translate('a.b.c', { values: { total: 1000 } })).toBe('Your total is $1,000.00'); @@ -670,6 +727,7 @@ describe('I18n engine', () => { usd: { style: 'currency', currency: 'USD' }, }, }, + messages: {}, }); i18n.setDefaultLocale('en'); @@ -704,7 +762,9 @@ describe('I18n engine', () => { test('should use default format if passed format option is not specified', () => { i18n.init({ locale: 'en', - ['a.b.c']: 'Your total is {total, number, usd}', + messages: { + ['a.b.c']: 'Your total is {total, number, usd}', + }, }); i18n.setDefaultLocale('en'); @@ -721,7 +781,9 @@ describe('I18n engine', () => { test('should throw error if used format is not specified', () => { i18n.init({ locale: 'en', - ['a.b.c']: 'Your total is {total, foo}', + messages: { + ['a.b.c']: 'Your total is {total, foo}', + }, }); i18n.setDefaultLocale('en'); @@ -741,28 +803,32 @@ describe('I18n engine', () => { describe('init', () => { test('should not initialize the engine if messages are not specified', () => { i18n.init(); - expect(i18n.getMessages()).toEqual({}); + expect(i18n.getTranslation()).toEqual({ messages: {} }); }); test('should throw error if messages are empty', () => { - expect(() => i18n.init({})).toThrow(); - expect(i18n.getMessages()).toEqual({}); + expect(() => i18n.init({ messages: {} })).toThrow(); + expect(i18n.getTranslation()).toEqual({ messages: {} }); }); test('should add messages if locale is specified', () => { i18n.init({ locale: 'en', - foo: 'bar', + messages: { + foo: 'bar', + }, }); - expect(i18n.getMessages()).toEqual({ + expect(i18n.getTranslation()).toEqual({ locale: 'en', - foo: 'bar', + messages: { + foo: 'bar', + }, }); }); test('should set the current locale', () => { - i18n.init({ locale: 'ru' }); + i18n.init({ locale: 'ru', messages: {} }); expect(i18n.getLocale()).toBe('ru'); }); @@ -778,6 +844,7 @@ describe('I18n engine', () => { }, }, }, + messages: {}, }); expect((i18n.getFormats().date as any).custom).toEqual({ diff --git a/packages/kbn-i18n/src/core/i18n.ts b/packages/kbn-i18n/src/core/i18n.ts index 027c3411d0ed63..beae2a486130d3 100644 --- a/packages/kbn-i18n/src/core/i18n.ts +++ b/packages/kbn-i18n/src/core/i18n.ts @@ -21,7 +21,7 @@ import memoizeIntlConstructor from 'intl-format-cache'; import IntlMessageFormat from 'intl-messageformat'; import IntlRelativeFormat from 'intl-relativeformat'; -import { Messages, PlainMessages } from '../messages'; +import { Translation } from '../translation'; import { Formats, formats as EN_FORMATS } from './formats'; import { hasValues, isObject, isString, mergeAll } from './helper'; import { isPseudoLocale, translateUsingPseudoLocale } from './pseudo_locale'; @@ -31,7 +31,7 @@ import './locales.js'; const EN_LOCALE = 'en'; const LOCALE_DELIMITER = '-'; -const messages: Messages = {}; +const translationsForLocale: Record = {}; const getMessageFormat = memoizeIntlConstructor(IntlMessageFormat); let defaultLocale = EN_LOCALE; @@ -45,8 +45,9 @@ IntlRelativeFormat.defaultLocale = defaultLocale; * Returns message by the given message id. * @param id - path to the message */ -function getMessageById(id: string): string { - return getMessages()[id]; +function getMessageById(id: string): string | undefined { + const translation = getTranslation(); + return translation.messages ? translation.messages[id] : undefined; } /** @@ -59,33 +60,38 @@ function normalizeLocale(locale: string) { /** * Provides a way to register translations with the engine - * @param newMessages + * @param newTranslation * @param [locale = messages.locale] */ -export function addMessages(newMessages: PlainMessages = {}, locale = newMessages.locale) { +export function addTranslation(newTranslation: Translation, locale = newTranslation.locale) { if (!locale || !isString(locale)) { throw new Error('[I18n] A `locale` must be a non-empty string to add messages.'); } - if (newMessages.locale && newMessages.locale !== locale) { + if (newTranslation.locale && newTranslation.locale !== locale) { throw new Error( - '[I18n] A `locale` in the messages object is different from the one provided as a second argument.' + '[I18n] A `locale` in the translation object is different from the one provided as a second argument.' ); } const normalizedLocale = normalizeLocale(locale); - - messages[normalizedLocale] = { - ...messages[normalizedLocale], - ...newMessages, + const existingTranslation = translationsForLocale[normalizedLocale] || { messages: {} }; + + translationsForLocale[normalizedLocale] = { + formats: newTranslation.formats || existingTranslation.formats, + locale: newTranslation.locale || existingTranslation.locale, + messages: { + ...existingTranslation.messages, + ...newTranslation.messages, + }, }; } /** * Returns messages for the current language */ -export function getMessages(): PlainMessages { - return messages[currentLocale] || {}; +export function getTranslation(): Translation { + return translationsForLocale[currentLocale] || { messages: {} }; } /** @@ -155,7 +161,7 @@ export function getFormats() { * Returns array of locales having translations */ export function getRegisteredLocales() { - return Object.keys(messages); + return Object.keys(translationsForLocale); } interface TranslateArguments { @@ -218,20 +224,20 @@ export function translate( /** * Initializes the engine - * @param newMessages + * @param newTranslation */ -export function init(newMessages?: PlainMessages) { - if (!newMessages) { +export function init(newTranslation?: Translation) { + if (!newTranslation) { return; } - addMessages(newMessages); + addTranslation(newTranslation); - if (newMessages.locale) { - setLocale(newMessages.locale); + if (newTranslation.locale) { + setLocale(newTranslation.locale); } - if (newMessages.formats) { - setFormats(newMessages.formats); + if (newTranslation.formats) { + setFormats(newTranslation.formats); } } diff --git a/packages/kbn-i18n/src/loader.test.ts b/packages/kbn-i18n/src/loader.test.ts index df99c22ede71b2..72cfeb09974ac9 100644 --- a/packages/kbn-i18n/src/loader.test.ts +++ b/packages/kbn-i18n/src/loader.test.ts @@ -100,8 +100,10 @@ describe('I18n loader', () => { expect(await i18nLoader.getTranslationsByLocale('en')).toEqual({ locale: 'en', - ['a.b.c']: 'foo', - ['d.e.f']: 'bar', + messages: { + ['a.b.c']: 'foo', + ['d.e.f']: 'bar', + }, }); }); @@ -110,7 +112,7 @@ describe('I18n loader', () => { join(__dirname, './__fixtures__/test_plugin_1/translations/en.json') ); - expect(await i18nLoader.getTranslationsByLocale('ru')).toEqual({}); + expect(await i18nLoader.getTranslationsByLocale('ru')).toEqual({ messages: {} }); }); test('should return translation messages from a couple of files by specified locale', async () => { @@ -121,10 +123,12 @@ describe('I18n loader', () => { expect(await i18nLoader.getTranslationsByLocale('en')).toEqual({ locale: 'en', - ['a.b.c']: 'foo', - ['d.e.f']: 'bar', - ['a.b.c.custom']: 'foo.custom', - ['d.e.f.custom']: 'bar.custom', + messages: { + ['a.b.c']: 'foo', + ['d.e.f']: 'bar', + ['a.b.c.custom']: 'foo.custom', + ['d.e.f.custom']: 'bar.custom', + }, }); }); @@ -138,21 +142,27 @@ describe('I18n loader', () => { expect(await i18nLoader.getTranslationsByLocale('en')).toEqual({ locale: 'en', - ['a.b.c']: 'foo', - ['d.e.f']: 'bar', - ['a.b.c.custom']: 'foo.custom', - ['d.e.f.custom']: 'bar.custom', + messages: { + ['a.b.c']: 'foo', + ['d.e.f']: 'bar', + ['a.b.c.custom']: 'foo.custom', + ['d.e.f.custom']: 'bar.custom', + }, }); expect(await i18nLoader.getTranslationsByLocale('en-US')).toEqual({ locale: 'en-US', - ['a.b.c']: 'bar', - ['d.e.f']: 'foo', + messages: { + ['a.b.c']: 'bar', + ['d.e.f']: 'foo', + }, }); expect(await i18nLoader.getTranslationsByLocale('ru')).toEqual({ locale: 'ru', - test: 'test', + messages: { + test: 'test', + }, }); }); @@ -163,7 +173,9 @@ describe('I18n loader', () => { expect(await i18nLoader.getTranslationsByLocale('fr')).toEqual({ locale: 'fr', - test: 'test', + messages: { + test: 'test', + }, }); }); }); @@ -180,19 +192,25 @@ describe('I18n loader', () => { expect(await i18nLoader.getAllTranslations()).toEqual({ en: { locale: 'en', - ['a.b.c']: 'foo', - ['d.e.f']: 'bar', - ['a.b.c.custom']: 'foo.custom', - ['d.e.f.custom']: 'bar.custom', + messages: { + ['a.b.c']: 'foo', + ['d.e.f']: 'bar', + ['a.b.c.custom']: 'foo.custom', + ['d.e.f.custom']: 'bar.custom', + }, }, ['en-US']: { locale: 'en-US', - ['a.b.c']: 'bar', - ['d.e.f']: 'foo', + messages: { + ['a.b.c']: 'bar', + ['d.e.f']: 'foo', + }, }, ru: { locale: 'ru', - test: 'test', + messages: { + test: 'test', + }, }, }); }); @@ -214,19 +232,25 @@ describe('I18n loader', () => { ).toEqual({ en: { locale: 'en', - ['a.b.c']: 'foo', - ['d.e.f']: 'bar', - ['a.b.c.custom']: 'foo.custom', - ['d.e.f.custom']: 'bar.custom', + messages: { + ['a.b.c']: 'foo', + ['d.e.f']: 'bar', + ['a.b.c.custom']: 'foo.custom', + ['d.e.f.custom']: 'bar.custom', + }, }, ['en-US']: { locale: 'en-US', - ['a.b.c']: 'bar', - ['d.e.f']: 'foo', + messages: { + ['a.b.c']: 'bar', + ['d.e.f']: 'foo', + }, }, ru: { locale: 'ru', - test: 'test', + messages: { + test: 'test', + }, }, }); }); diff --git a/packages/kbn-i18n/src/loader.ts b/packages/kbn-i18n/src/loader.ts index e45964b94e3f97..6efafc18ae79bf 100644 --- a/packages/kbn-i18n/src/loader.ts +++ b/packages/kbn-i18n/src/loader.ts @@ -23,7 +23,7 @@ import * as path from 'path'; import { promisify } from 'util'; import { unique } from './core/helper'; -import { Messages, PlainMessages } from './messages'; +import { Translation } from './translation'; const asyncReadFile = promisify(readFile); @@ -39,7 +39,7 @@ const translationsRegistry: { [key: string]: string[] } = {}; * Internal property for caching loaded translations files. * Key is path to translation file, value is object with translation messages */ -const loadedFiles: { [key: string]: PlainMessages } = {}; +const loadedFiles: { [key: string]: Translation } = {}; /** * Returns locale by the given translation file name @@ -69,7 +69,7 @@ function getLocaleFromFileName(fullFileName: string) { * @param pathToFile * @returns */ -async function loadFile(pathToFile: string): Promise { +async function loadFile(pathToFile: string): Promise { return JSON5.parse(await asyncReadFile(pathToFile, 'utf8')); } @@ -127,7 +127,7 @@ export function getRegisteredLocales() { * @param locale * @returns translation messages */ -export async function getTranslationsByLocale(locale: string): Promise { +export async function getTranslationsByLocale(locale: string): Promise { const files = translationsRegistry[locale] || []; const notLoadedFiles = files.filter(file => !loadedFiles[file]); @@ -135,15 +135,21 @@ export async function getTranslationsByLocale(locale: string): Promise ({ - ...messages, - ...loadedFiles[file], - }), - { locale } - ) - : {}; + if (!files.length) { + return { messages: {} }; + } + + return files.reduce( + (translation: Translation, file) => ({ + locale: loadedFiles[file].locale || translation.locale, + formats: loadedFiles[file].formats || translation.formats, + messages: { + ...loadedFiles[file].messages, + ...translation.messages, + }, + }), + { locale, messages: {} } + ); } /** @@ -151,7 +157,7 @@ export async function getTranslationsByLocale(locale: string): Promise { +export async function getAllTranslations(): Promise<{ [key: string]: Translation }> { const locales = getRegisteredLocales(); const translations = await Promise.all(locales.map(getTranslationsByLocale)); diff --git a/packages/kbn-i18n/src/react/__snapshots__/inject_i18n_provider.test.tsx.snap b/packages/kbn-i18n/src/react/__snapshots__/inject_i18n_provider.test.tsx.snap index df3ea6450d0be8..7ad7a6fbc1f00c 100644 --- a/packages/kbn-i18n/src/react/__snapshots__/inject_i18n_provider.test.tsx.snap +++ b/packages/kbn-i18n/src/react/__snapshots__/inject_i18n_provider.test.tsx.snap @@ -66,62 +66,7 @@ Object { "formatPlural": [Function], "formatRelative": [Function], "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, + "formats": Object {}, "formatters": Object { "getDateTimeFormat": [Function], "getMessageFormat": [Function], diff --git a/packages/kbn-i18n/src/react/__snapshots__/provider.test.tsx.snap b/packages/kbn-i18n/src/react/__snapshots__/provider.test.tsx.snap index 0ce4cf1a774768..67701d6c14697d 100644 --- a/packages/kbn-i18n/src/react/__snapshots__/provider.test.tsx.snap +++ b/packages/kbn-i18n/src/react/__snapshots__/provider.test.tsx.snap @@ -66,62 +66,7 @@ Object { "formatPlural": [Function], "formatRelative": [Function], "formatTime": [Function], - "formats": Object { - "date": Object { - "full": Object { - "day": "numeric", - "month": "long", - "weekday": "long", - "year": "numeric", - }, - "long": Object { - "day": "numeric", - "month": "long", - "year": "numeric", - }, - "medium": Object { - "day": "numeric", - "month": "short", - "year": "numeric", - }, - "short": Object { - "day": "numeric", - "month": "numeric", - "year": "2-digit", - }, - }, - "number": Object { - "currency": Object { - "style": "currency", - }, - "percent": Object { - "style": "percent", - }, - }, - "time": Object { - "full": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "long": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - "timeZoneName": "short", - }, - "medium": Object { - "hour": "numeric", - "minute": "numeric", - "second": "numeric", - }, - "short": Object { - "hour": "numeric", - "minute": "numeric", - }, - }, - }, + "formats": Object {}, "formatters": Object { "getDateTimeFormat": [Function], "getMessageFormat": [Function], diff --git a/packages/kbn-i18n/src/react/provider.tsx b/packages/kbn-i18n/src/react/provider.tsx index b1468ff8733fce..42718fd689ac33 100644 --- a/packages/kbn-i18n/src/react/provider.tsx +++ b/packages/kbn-i18n/src/react/provider.tsx @@ -58,9 +58,9 @@ export class I18nProvider extends React.PureComponent { return ( diff --git a/packages/kbn-i18n/src/messages.ts b/packages/kbn-i18n/src/translation.ts similarity index 78% rename from packages/kbn-i18n/src/messages.ts rename to packages/kbn-i18n/src/translation.ts index edfea322c7436a..20a27ca72a72c3 100644 --- a/packages/kbn-i18n/src/messages.ts +++ b/packages/kbn-i18n/src/translation.ts @@ -19,21 +19,17 @@ import { Formats } from './core/formats'; -/** - * Messages tree, where leafs are translated strings - */ -export interface Messages { - [key: string]: PlainMessages; -} - -export interface PlainMessages { - [key: string]: any; +export interface Translation { + /** + * Actual translated messages. + */ + messages: Record; /** - * locale of the messages + * Locale of the translated messages. */ locale?: string; /** - * set of options to the underlying formatter + * Set of options to the underlying formatter. */ formats?: Formats; } diff --git a/src/dev/i18n/serializers/__snapshots__/json.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap index c35e91e25cbb68..decfeb17c05683 100644 --- a/src/dev/i18n/serializers/__snapshots__/json.test.js.snap +++ b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap @@ -58,10 +58,12 @@ exports[`dev/i18n/serializers/json should serialize default messages to JSON 1`] } } }, - \\"plugin1.message.id-1\\": \\"Message text 1 \\", - \\"plugin2.message.id-2\\": { - \\"text\\": \\"Message text 2\\", - \\"comment\\": \\"Message context\\" + \\"messages\\": { + \\"plugin1.message.id-1\\": \\"Message text 1 \\", + \\"plugin2.message.id-2\\": { + \\"text\\": \\"Message text 2\\", + \\"comment\\": \\"Message context\\" + } } }" `; diff --git a/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap index 2166b32f28fd16..d643661ce58a5e 100644 --- a/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap +++ b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap @@ -58,8 +58,10 @@ exports[`dev/i18n/serializers/json5 should serialize default messages to JSON5 1 }, }, }, - 'plugin1.message.id-1': 'Message text 1', - 'plugin2.message.id-2': 'Message text 2', // Message context + messages: { + 'plugin1.message.id-1': 'Message text 1', + 'plugin2.message.id-2': 'Message text 2', // Message context + }, } " `; diff --git a/src/dev/i18n/serializers/json.js b/src/dev/i18n/serializers/json.js index 8e615af1e81d32..149bb0236a63eb 100644 --- a/src/dev/i18n/serializers/json.js +++ b/src/dev/i18n/serializers/json.js @@ -20,13 +20,13 @@ import { i18n } from '@kbn/i18n'; export function serializeToJson(defaultMessages) { - const resultJsonObject = { formats: i18n.formats }; + const resultJsonObject = { formats: i18n.formats, messages: {} }; for (const [mapKey, mapValue] of defaultMessages) { if (mapValue.context) { - resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; + resultJsonObject.messages[mapKey] = { text: mapValue.message, comment: mapValue.context }; } else { - resultJsonObject[mapKey] = mapValue.message; + resultJsonObject.messages[mapKey] = mapValue.message; } } diff --git a/src/dev/i18n/serializers/json5.js b/src/dev/i18n/serializers/json5.js index 0156053d5f43b5..9b2c013df7724d 100644 --- a/src/dev/i18n/serializers/json5.js +++ b/src/dev/i18n/serializers/json5.js @@ -23,9 +23,11 @@ import { i18n } from '@kbn/i18n'; const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; export function serializeToJson5(defaultMessages) { - // .slice(0, -1): remove closing curly brace from json to append messages + // .slice(0, -4): remove closing curly braces from json to append messages let jsonBuffer = Buffer.from( - JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) + JSON5.stringify({ formats: i18n.formats, messages: {} }, { quote: `'`, space: 2 }) + .slice(0, -4) + .concat('\n') ); for (const [mapKey, mapValue] of defaultMessages) { @@ -36,13 +38,13 @@ export function serializeToJson5(defaultMessages) { jsonBuffer = Buffer.concat([ jsonBuffer, - Buffer.from(` '${mapKey}': '${formattedMessage}',`), + Buffer.from(` '${mapKey}': '${formattedMessage}',`), Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), ]); } - // append previously removed closing curly brace - jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); + // append previously removed closing curly braces + jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from(' },\n}\n')]); return jsonBuffer; }