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 new file mode 100644 index 0000000000000..a25da0165b407 --- /dev/null +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en-US.json @@ -0,0 +1,4 @@ +{ + "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 new file mode 100644 index 0000000000000..eaec1a6d8a096 --- /dev/null +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_1/translations/en.json @@ -0,0 +1,4 @@ +{ + "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 new file mode 100644 index 0000000000000..85a62e3df1a48 --- /dev/null +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/en.json @@ -0,0 +1,4 @@ +{ + "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 new file mode 100644 index 0000000000000..04e20542e75e3 --- /dev/null +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/fr.json @@ -0,0 +1,3 @@ +{ + 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 new file mode 100644 index 0000000000000..d0ae716dbe4c3 --- /dev/null +++ b/packages/kbn-i18n/src/__fixtures__/test_plugin_2/translations/ru.json @@ -0,0 +1,3 @@ +{ + "test": "test" +} diff --git a/packages/kbn-i18n/src/__snapshots__/loader.test.js.snap b/packages/kbn-i18n/src/__snapshots__/loader.test.js.snap new file mode 100644 index 0000000000000..37209d29367a2 --- /dev/null +++ b/packages/kbn-i18n/src/__snapshots__/loader.test.js.snap @@ -0,0 +1,5 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`I18n loader registerTranslationFile should throw error if path to translation file is not an absolute 1`] = `"Paths to translation files must be absolute. Got relative path: \\"./en.json\\""`; + +exports[`I18n loader registerTranslationFile should throw error if path to translation file is not specified 1`] = `"Path must be a string. Received undefined"`; diff --git a/packages/kbn-i18n/src/angular/directive.test.js b/packages/kbn-i18n/src/angular/directive.test.js new file mode 100644 index 0000000000000..6f28c0b453236 --- /dev/null +++ b/packages/kbn-i18n/src/angular/directive.test.js @@ -0,0 +1,77 @@ +/* + * 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 angular from 'angular'; +import 'angular-mocks'; +import { i18nDirective } from './directive'; +import { i18nProvider } from './provider'; + +angular + .module('app', []) + .provider('i18n', i18nProvider) + .directive('i18nId', i18nDirective); + +describe('i18nDirective', () => { + let compile; + let scope; + + beforeEach(angular.mock.module('app')); + beforeEach( + angular.mock.inject(($compile, $rootScope) => { + compile = $compile; + scope = $rootScope.$new(); + }) + ); + + it('inserts correct translation html content', () => { + const id = 'id'; + const defaultMessage = 'default-message'; + + const element = angular.element( + `
` + ); + + compile(element)(scope); + scope.$digest(); + + expect(element.html()).toEqual(defaultMessage); + }); + + it('inserts correct translation html content with values', () => { + const id = 'id'; + const defaultMessage = 'default-message {word}'; + const compiledContent = 'default-message word'; + + const element = angular.element( + `` + ); + + compile(element)(scope); + scope.$digest(); + + expect(element.html()).toEqual(compiledContent); + }); +}); diff --git a/packages/kbn-i18n/src/angular/filter.test.js b/packages/kbn-i18n/src/angular/filter.test.js new file mode 100644 index 0000000000000..b1ec5b8c3ad4c --- /dev/null +++ b/packages/kbn-i18n/src/angular/filter.test.js @@ -0,0 +1,59 @@ +/* + * 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 angular from 'angular'; +import 'angular-mocks'; +import { i18nProvider } from './provider'; +import { i18nFilter } from './filter'; +import * as i18n from '../core/i18n'; + +jest.mock('../core/i18n', () => ({ + translate: jest.fn().mockImplementation(() => 'translation'), +})); + +angular + .module('app', []) + .provider('i18n', i18nProvider) + .filter('i18n', i18nFilter); + +describe('i18nFilter', () => { + let filter; + + beforeEach(angular.mock.module('app')); + beforeEach( + angular.mock.inject(i18nFilter => { + filter = i18nFilter; + }) + ); + afterEach(() => { + jest.resetAllMocks(); + }); + + it('provides wrapper around i18n engine', () => { + const id = 'id'; + const defaultMessage = 'default-message'; + const values = {}; + + const result = filter(id, { defaultMessage, values }); + + expect(i18n.translate).toHaveBeenCalledTimes(1); + expect(i18n.translate).toHaveBeenCalledWith(id, { defaultMessage, values }); + expect(result).toEqual('translation'); + }); +}); diff --git a/packages/kbn-i18n/src/angular/provider.test.js b/packages/kbn-i18n/src/angular/provider.test.js new file mode 100644 index 0000000000000..3246a6f1964d6 --- /dev/null +++ b/packages/kbn-i18n/src/angular/provider.test.js @@ -0,0 +1,57 @@ +/* + * 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 angular from 'angular'; +import 'angular-mocks'; +import { i18nProvider } from './provider'; +import * as i18n from '../core/i18n'; + +angular.module('app', []).provider('i18n', i18nProvider); + +describe('i18nProvider', () => { + let provider; + let service; + + beforeEach( + angular.mock.module('app', [ + 'i18nProvider', + i18n => { + service = i18n; + }, + ]) + ); + beforeEach( + angular.mock.inject(i18n => { + provider = i18n; + }) + ); + + it('provides wrapper around i18n engine', () => { + expect(provider).toEqual(i18n.translate); + }); + + it('provides service wrapper around i18n engine', () => { + const serviceMethodNames = Object.keys(service); + const pluginMethodNames = Object.keys(i18n); + + expect([...serviceMethodNames, 'translate'].sort()).toEqual( + [...pluginMethodNames, '$get'].sort() + ); + }); +}); diff --git a/packages/kbn-i18n/src/core/__snapshots__/i18n.test.js.snap b/packages/kbn-i18n/src/core/__snapshots__/i18n.test.js.snap new file mode 100644 index 0000000000000..e28975a7c1e9e --- /dev/null +++ b/packages/kbn-i18n/src/core/__snapshots__/i18n.test.js.snap @@ -0,0 +1,49 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`I18n engine addMessages should throw error if locale is not specified or empty 1`] = `"[I18n] A \`locale\` must be a non-empty string to add messages."`; + +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 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."`; + +exports[`I18n engine translate should throw error if id is not a non-empty string 2`] = `"[I18n] An \`id\` must be a non-empty string to translate a message."`; + +exports[`I18n engine translate should throw error if id is not a non-empty string 3`] = `"[I18n] An \`id\` must be a non-empty string to translate a message."`; + +exports[`I18n engine translate should throw error if id is not a non-empty string 4`] = `"[I18n] An \`id\` must be a non-empty string to translate a message."`; + +exports[`I18n engine translate should throw error if id is not a non-empty string 5`] = `"[I18n] An \`id\` must be a non-empty string to translate a message."`; + +exports[`I18n engine translate should throw error if id is not a non-empty string 6`] = `"[I18n] An \`id\` must be a non-empty string to translate a message."`; + +exports[`I18n engine translate should throw error if translation message and defaultMessage are not provided 1`] = `"[I18n] Cannot format message: \\"foo\\". Default message must be provided."`; + +exports[`I18n engine translate should throw error if used format is not specified 1`] = ` +"[I18n] Error formatting message: \\"a.b.c\\" for locale: \\"en\\". +SyntaxError: Expected \\"date\\", \\"number\\", \\"plural\\", \\"select\\", \\"selectordinal\\" or \\"time\\" but \\"f\\" found." +`; + +exports[`I18n engine translate should throw error if used format is not specified 2`] = ` +"[I18n] Error formatting the default message for: \\"d.e.f\\". +SyntaxError: Expected \\"date\\", \\"number\\", \\"plural\\", \\"select\\", \\"selectordinal\\" or \\"time\\" but \\"b\\" found." +`; + +exports[`I18n engine translate should throw error if wrong context is provided to the translation string 1`] = ` +"[I18n] Error formatting message: \\"a.b.c\\" for locale: \\"en\\". +Error: The intl string context variable 'numPhotos' was not provided to the string 'You have {numPhotos, plural, + =0 {no photos.} + =1 {one photo.} + other {# photos.} + }'" +`; + +exports[`I18n engine translate should throw error if wrong context is provided to the translation string 2`] = ` +"[I18n] Error formatting the default message for: \\"d.e.f\\". +Error: The intl string context variable 'numPhotos' was not provided to the string 'You have {numPhotos, plural, + =0 {no photos.} + =1 {one photo.} + other {# photos.} + }'" +`; diff --git a/packages/kbn-i18n/src/core/helper.test.js b/packages/kbn-i18n/src/core/helper.test.js new file mode 100644 index 0000000000000..cb54be97ca9e7 --- /dev/null +++ b/packages/kbn-i18n/src/core/helper.test.js @@ -0,0 +1,197 @@ +/* + * 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 { isString, isObject, hasValues, unique, mergeAll } from './helper'; + +describe('I18n helper', () => { + describe('isString', () => { + test('should return true for string literal', () => { + expect(isString('test')).toBe(true); + }); + + test('should return false for string object', () => { + expect(isString(new String('test'))).toBe(false); + }); + + test('should return false for non-string values', () => { + expect(isString(undefined)).toBe(false); + expect(isString(null)).toBe(false); + expect(isString(0)).toBe(false); + expect(isString(true)).toBe(false); + expect(isString({})).toBe(false); + }); + }); + + describe('isObject', () => { + test('should return true for object literal', () => { + expect(isObject({})).toBe(true); + }); + + test('should return true for array literal', () => { + expect(isObject([])).toBe(true); + }); + + test('should return false for primitives', () => { + expect(isObject(undefined)).toBe(false); + expect(isObject(null)).toBe(false); + expect(isObject(0)).toBe(false); + expect(isObject(true)).toBe(false); + expect(isObject('test')).toBe(false); + }); + }); + + describe('hasValues', () => { + test('should return false for empty object', () => { + expect(hasValues({})).toBe(false); + }); + + test('should return true for non-empty object', () => { + expect(hasValues({ foo: 'bar' })).toBe(true); + }); + + test('should throw error for null and undefined', () => { + expect(() => hasValues(undefined)).toThrow(); + expect(() => hasValues(null)).toThrow(); + }); + + test('should return false for number and boolean', () => { + expect(hasValues(true)).toBe(false); + expect(hasValues(0)).toBe(false); + }); + + test('should return false for empty string', () => { + expect(hasValues('')).toBe(false); + }); + + test('should return true for non-empty string', () => { + expect(hasValues('test')).toBe(true); + }); + + test('should return false for empty array', () => { + expect(hasValues([])).toBe(false); + }); + + test('should return true for non-empty array', () => { + expect(hasValues([1, 2, 3])).toBe(true); + }); + }); + + describe('unique', () => { + test('should return an array with unique values', () => { + expect(unique([1, 2, 7, 2, 6, 7, 1])).toEqual([1, 2, 7, 6]); + }); + + test('should create a new array', () => { + const value = [1, 2, 3]; + + expect(unique(value)).toEqual(value); + expect(unique(value)).not.toBe(value); + }); + + test('should filter unique values only by reference', () => { + expect(unique([{ foo: 'bar' }, { foo: 'bar' }])).toEqual([{ foo: 'bar' }, { foo: 'bar' }]); + + const value = { foo: 'bar' }; + + expect(unique([value, value])).toEqual([value]); + }); + }); + + describe('mergeAll', () => { + test('should throw error for empty arguments', () => { + expect(() => mergeAll()).toThrow(); + }); + + test('should merge only objects', () => { + expect(mergeAll(undefined, null, true, 5, '5', { foo: 'bar' })).toEqual({ + foo: 'bar', + }); + }); + + test('should return the only argument as is', () => { + const value = { foo: 'bar' }; + + expect(mergeAll(value)).toBe(value); + }); + + test('should return a deep merge of 2 objects nested objects', () => { + expect( + mergeAll( + { + foo: { bar: 3 }, + array: [ + { + does: 'work', + too: [1, 2, 3], + }, + ], + }, + { + foo: { baz: 4 }, + quux: 5, + array: [ + { + does: 'work', + too: [4, 5, 6], + }, + { + really: 'yes', + }, + ], + } + ) + ).toEqual({ + foo: { + bar: 3, + baz: 4, + }, + array: [ + { + does: 'work', + too: [4, 5, 6], + }, + { + really: 'yes', + }, + ], + quux: 5, + }); + }); + + test('should override arrays', () => { + expect(mergeAll({ foo: [1, 2] }, { foo: [3, 4] })).toEqual({ + foo: [3, 4], + }); + }); + + test('should merge any number of objects', () => { + expect(mergeAll({ a: 1 }, { b: 2 }, { c: 3 })).toEqual({ + a: 1, + b: 2, + c: 3, + }); + expect(mergeAll({ a: 1 }, { b: 2 }, { c: 3 }, { d: 4 })).toEqual({ + a: 1, + b: 2, + c: 3, + d: 4, + }); + }); + }); +}); diff --git a/packages/kbn-i18n/src/core/i18n.js b/packages/kbn-i18n/src/core/i18n.js index bde687b763978..3d76d13a72d63 100644 --- a/packages/kbn-i18n/src/core/i18n.js +++ b/packages/kbn-i18n/src/core/i18n.js @@ -73,6 +73,12 @@ export function addMessages(newMessages = {}, locale = newMessages.locale) { throw new Error('[I18n] A `locale` must be a non-empty string to add messages.'); } + if (newMessages.locale && newMessages.locale !== locale) { + throw new Error( + '[I18n] A `locale` in the messages object is different from the one provided as a second argument.' + ); + } + const normalizedLocale = normalizeLocale(locale); messages[normalizedLocale] = { diff --git a/packages/kbn-i18n/src/core/i18n.test.js b/packages/kbn-i18n/src/core/i18n.test.js new file mode 100644 index 0000000000000..695b65a3e2d3e --- /dev/null +++ b/packages/kbn-i18n/src/core/i18n.test.js @@ -0,0 +1,774 @@ +/* + * 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. + */ + +describe('I18n engine', () => { + let i18n; + + beforeEach(() => { + i18n = require('./i18n'); + }); + + afterEach(() => { + // isolate modules for every test so that local module state doesn't conflict between tests + jest.resetModules(); + }); + + describe('addMessages', () => { + test('should throw error if locale is not specified or empty', () => { + expect(() => i18n.addMessages({ foo: 'bar' })).toThrowErrorMatchingSnapshot(); + expect(() => i18n.addMessages({ locale: '' })).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') + ).toThrowErrorMatchingSnapshot(); + }); + + test('should add messages if locale prop is passed as second argument', () => { + const locale = 'ru'; + + expect(i18n.getMessages()).toEqual({}); + + i18n.addMessages({ foo: 'bar' }, locale); + + expect(i18n.getMessages()).toEqual({}); + + i18n.setLocale(locale); + + expect(i18n.getMessages()).toEqual({ foo: 'bar' }); + }); + + test('should add messages if locale prop is passed as messages property', () => { + const locale = 'ru'; + + expect(i18n.getMessages()).toEqual({}); + + i18n.addMessages({ + locale, + foo: 'bar', + }); + + expect(i18n.getMessages()).toEqual({}); + + i18n.setLocale(locale); + + expect(i18n.getMessages()).toEqual({ + foo: 'bar', + locale: 'ru', + }); + }); + + test('should merge messages with the same locale', () => { + const locale = 'ru'; + + i18n.setLocale(locale); + i18n.addMessages({ + locale, + ['a.b.c']: 'foo', + }); + + expect(i18n.getMessages()).toEqual({ + locale: 'ru', + ['a.b.c']: 'foo', + }); + + i18n.addMessages({ + locale, + ['d.e.f']: 'bar', + }); + + expect(i18n.getMessages()).toEqual({ + locale: 'ru', + ['a.b.c']: 'foo', + ['d.e.f']: 'bar', + }); + }); + + test('should override messages with the same locale and id', () => { + const locale = 'ru'; + + i18n.setLocale(locale); + i18n.addMessages({ + locale, + ['a.b.c']: 'foo', + }); + + expect(i18n.getMessages()).toEqual({ + locale: 'ru', + ['a.b.c']: 'foo', + }); + + i18n.addMessages({ + locale, + ['a.b.c']: 'bar', + }); + + expect(i18n.getMessages()).toEqual({ + locale: 'ru', + ['a.b.c']: 'bar', + }); + }); + + test('should add messages with normalized passed locale', () => { + const locale = 'en-us'; + i18n.setLocale(locale); + + i18n.addMessages( + { + ['a.b.c']: 'bar', + }, + 'en_US' + ); + + expect(i18n.getLocale()).toBe(locale); + expect(i18n.getMessages()).toEqual({ + ['a.b.c']: 'bar', + }); + }); + }); + + describe('getMessages', () => { + test('should return messages for the current language', () => { + i18n.addMessages({ + locale: 'ru', + foo: 'bar', + }); + i18n.addMessages({ + locale: 'en', + bar: 'foo', + }); + + i18n.setLocale('ru'); + expect(i18n.getMessages()).toEqual({ + locale: 'ru', + foo: 'bar', + }); + + i18n.setLocale('en'); + expect(i18n.getMessages()).toEqual({ + locale: 'en', + bar: 'foo', + }); + }); + + test('should return an empty object if messages for current locale are not specified', () => { + expect(i18n.getMessages()).toEqual({}); + + i18n.setLocale('fr'); + expect(i18n.getMessages()).toEqual({}); + + i18n.setLocale('en'); + expect(i18n.getMessages()).toEqual({}); + }); + }); + + describe('setLocale', () => { + test('should throw error if locale is not a non-empty string', () => { + expect(() => i18n.setLocale(undefined)).toThrow(); + expect(() => i18n.setLocale(null)).toThrow(); + expect(() => i18n.setLocale(true)).toThrow(); + expect(() => i18n.setLocale(5)).toThrow(); + expect(() => i18n.setLocale({})).toThrow(); + expect(() => i18n.setLocale('')).toThrow(); + }); + + test('should update current locale', () => { + expect(i18n.getLocale()).not.toBe('foo'); + i18n.setLocale('foo'); + expect(i18n.getLocale()).toBe('foo'); + }); + + test('should normalize passed locale', () => { + i18n.setLocale('en_US'); + expect(i18n.getLocale()).toBe('en-us'); + }); + }); + + describe('getLocale', () => { + test('should return "en" locale by default', () => { + expect(i18n.getLocale()).toBe('en'); + }); + + test('should return updated locale', () => { + i18n.setLocale('foo'); + expect(i18n.getLocale()).toBe('foo'); + }); + }); + + describe('setDefaultLocale', () => { + test('should throw error if locale is not a non-empty string', () => { + expect(() => i18n.setDefaultLocale(undefined)).toThrow(); + expect(() => i18n.setDefaultLocale(null)).toThrow(); + expect(() => i18n.setDefaultLocale(true)).toThrow(); + expect(() => i18n.setDefaultLocale(5)).toThrow(); + expect(() => i18n.setDefaultLocale({})).toThrow(); + expect(() => i18n.setDefaultLocale('')).toThrow(); + }); + + test('should update the default locale', () => { + expect(i18n.getDefaultLocale()).not.toBe('foo'); + i18n.setDefaultLocale('foo'); + expect(i18n.getDefaultLocale()).toBe('foo'); + }); + + test('should normalize passed locale', () => { + i18n.setDefaultLocale('en_US'); + expect(i18n.getDefaultLocale()).toBe('en-us'); + }); + + test('should set "en" locale as default for IntlMessageFormat and IntlRelativeFormat', () => { + const IntlMessageFormat = require('intl-messageformat'); + const IntlRelativeFormat = require('intl-relativeformat'); + + expect(IntlMessageFormat.defaultLocale).toBe('en'); + expect(IntlRelativeFormat.defaultLocale).toBe('en'); + }); + + test('should update defaultLocale for IntlMessageFormat and IntlRelativeFormat', () => { + const IntlMessageFormat = require('intl-messageformat'); + const IntlRelativeFormat = require('intl-relativeformat'); + + i18n.setDefaultLocale('foo'); + + expect(IntlMessageFormat.defaultLocale).toBe('foo'); + expect(IntlRelativeFormat.defaultLocale).toBe('foo'); + }); + }); + + describe('getDefaultLocale', () => { + test('should return "en" locale by default', () => { + expect(i18n.getDefaultLocale()).toBe('en'); + }); + + test('should return updated locale', () => { + i18n.setDefaultLocale('foo'); + expect(i18n.getDefaultLocale()).toBe('foo'); + }); + }); + + describe('setFormats', () => { + test('should throw error if formats parameter is not a non-empty object', () => { + expect(() => i18n.setFormats(undefined)).toThrow(); + expect(() => i18n.setFormats(null)).toThrow(); + expect(() => i18n.setFormats(true)).toThrow(); + expect(() => i18n.setFormats(5)).toThrow(); + expect(() => i18n.setFormats('foo')).toThrow(); + expect(() => i18n.setFormats({})).toThrow(); + }); + + test('should merge current formats with a passed formats', () => { + expect(i18n.getFormats().date.short).not.toEqual({ + month: 'short', + day: 'numeric', + year: 'numeric', + }); + + i18n.setFormats({ + date: { + short: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + }, + }); + + expect(i18n.getFormats().date.short).toEqual({ + month: 'short', + day: 'numeric', + year: 'numeric', + }); + + i18n.setFormats({ + date: { + short: { + month: 'long', + }, + }, + }); + + expect(i18n.getFormats().date.short).toEqual({ + month: 'long', + day: 'numeric', + year: 'numeric', + }); + }); + }); + + describe('getFormats', () => { + test('should return "en" formats by default', () => { + const { formats } = require('./formats'); + + expect(i18n.getFormats()).toEqual(formats); + }); + + test('should return updated formats', () => { + const { formats } = require('./formats'); + + i18n.setFormats({ + foo: 'bar', + }); + + expect(i18n.getFormats()).toEqual({ + ...formats, + foo: 'bar', + }); + }); + }); + + describe('getRegisteredLocales', () => { + test('should return empty array by default', () => { + expect(i18n.getRegisteredLocales()).toEqual([]); + }); + + test('should return array of registered locales', () => { + i18n.addMessages({ + locale: 'en', + }); + + expect(i18n.getRegisteredLocales()).toEqual(['en']); + + i18n.addMessages({ + locale: 'ru', + }); + + expect(i18n.getRegisteredLocales()).toContain('en', 'ru'); + expect(i18n.getRegisteredLocales().length).toBe(2); + + i18n.addMessages({ + locale: 'fr', + }); + + expect(i18n.getRegisteredLocales()).toContain('en', 'ru', 'fr'); + expect(i18n.getRegisteredLocales().length).toBe(3); + }); + }); + + describe('translate', () => { + test('should throw error if id is not a non-empty string', () => { + expect(() => i18n.translate(undefined)).toThrowErrorMatchingSnapshot(); + expect(() => i18n.translate(null)).toThrowErrorMatchingSnapshot(); + expect(() => i18n.translate(true)).toThrowErrorMatchingSnapshot(); + expect(() => i18n.translate(5)).toThrowErrorMatchingSnapshot(); + expect(() => i18n.translate({})).toThrowErrorMatchingSnapshot(); + expect(() => i18n.translate('')).toThrowErrorMatchingSnapshot(); + }); + + test('should throw error if translation message and defaultMessage are not provided', () => { + expect(() => i18n.translate('foo')).toThrowErrorMatchingSnapshot(); + }); + + test('should return message as is if values are not provided', () => { + i18n.init({ + locale: 'en', + ['a.b.c']: 'foo', + }); + + expect(i18n.translate('a.b.c')).toBe('foo'); + }); + + test('should return default message as is if values are not provided', () => { + expect(i18n.translate('a.b.c', { defaultMessage: 'foo' })).toBe('foo'); + }); + + test('should not return defaultMessage as is if values are provided', () => { + i18n.init({ + locale: 'en', + ['a.b.c']: 'foo', + }); + expect(i18n.translate('a.b.c', { defaultMessage: 'bar' })).toBe('foo'); + }); + + test('should interpolate variables', () => { + i18n.init({ + locale: 'en', + ['a.b.c']: 'foo {a}, {b}, {c} bar', + ['d.e.f']: '{foo}', + }); + + expect( + i18n.translate('a.b.c', { + values: { a: 1, b: 2, c: 3 }, + }) + ).toBe('foo 1, 2, 3 bar'); + + expect(i18n.translate('d.e.f', { values: { foo: 'bar' } })).toBe('bar'); + }); + + test('should interpolate variables for default messages', () => { + expect( + i18n.translate('a.b.c', { + defaultMessage: 'foo {a}, {b}, {c} bar', + values: { a: 1, b: 2, c: 3 }, + }) + ).toBe('foo 1, 2, 3 bar'); + }); + + test('should format pluralized messages', () => { + i18n.init({ + locale: 'en', + ['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.'); + expect(i18n.translate('a.b.c', { values: { numPhotos: 1 } })).toBe('You have one photo.'); + expect(i18n.translate('a.b.c', { values: { numPhotos: 1000 } })).toBe( + 'You have 1,000 photos.' + ); + }); + + test('should format pluralized default messages', () => { + i18n.setDefaultLocale('en'); + + expect( + i18n.translate('a.b.c', { + values: { numPhotos: 0 }, + defaultMessage: `You have {numPhotos, plural, + =0 {no photos.} + =1 {one photo.} + other {# photos.} + }`, + }) + ).toBe('You have no photos.'); + + expect( + i18n.translate('a.b.c', { + values: { numPhotos: 1 }, + defaultMessage: `You have {numPhotos, plural, + =0 {no photos.} + =1 {one photo.} + other {# photos.} + }`, + }) + ).toBe('You have one photo.'); + + expect( + i18n.translate('a.b.c', { + values: { numPhotos: 1000 }, + defaultMessage: `You have {numPhotos, plural, + =0 {no photos.} + =1 {one photo.} + other {# photos.} + }`, + }) + ).toBe('You have 1,000 photos.'); + }); + + test('should throw error if wrong context is provided to the translation string', () => { + i18n.init({ + locale: 'en', + ['a.b.c']: `You have {numPhotos, plural, + =0 {no photos.} + =1 {one photo.} + other {# photos.} + }`, + }); + i18n.setDefaultLocale('en'); + + expect(() => i18n.translate('a.b.c', { values: { foo: 0 } })).toThrowErrorMatchingSnapshot(); + + expect(() => + i18n.translate('d.e.f', { + values: { bar: 1000 }, + defaultMessage: `You have {numPhotos, plural, + =0 {no photos.} + =1 {one photo.} + other {# photos.} + }`, + }) + ).toThrowErrorMatchingSnapshot(); + }); + + test('should format messages with percent formatter', () => { + i18n.init({ + locale: 'en', + ['a.b.c']: 'Result: {result, number, percent}', + }); + i18n.setDefaultLocale('en'); + + expect(i18n.translate('a.b.c', { values: { result: 0.15 } })).toBe('Result: 15%'); + + expect( + i18n.translate('d.e.f', { + values: { result: 0.15 }, + defaultMessage: 'Result: {result, number, percent}', + }) + ).toBe('Result: 15%'); + }); + + 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}', + }); + + expect( + i18n.translate('a.short', { + values: { start: new Date(2018, 5, 20) }, + }) + ).toBe('Sale begins 6/20/18'); + + expect( + i18n.translate('a.medium', { + values: { start: new Date(2018, 5, 20) }, + }) + ).toBe('Sale begins Jun 20, 2018'); + + expect( + i18n.translate('a.long', { + values: { start: new Date(2018, 5, 20) }, + }) + ).toBe('Sale begins June 20, 2018'); + + expect( + i18n.translate('a.full', { + values: { start: new Date(2018, 5, 20) }, + }) + ).toBe('Sale begins Wednesday, June 20, 2018'); + }); + + test('should format default messages with date formatter', () => { + i18n.setDefaultLocale('en'); + + expect( + i18n.translate('foo', { + defaultMessage: 'Sale begins {start, date, short}', + values: { start: new Date(2018, 5, 20) }, + }) + ).toBe('Sale begins 6/20/18'); + + expect( + i18n.translate('foo', { + defaultMessage: 'Sale begins {start, date, medium}', + values: { start: new Date(2018, 5, 20) }, + }) + ).toBe('Sale begins Jun 20, 2018'); + + expect( + i18n.translate('foo', { + defaultMessage: 'Sale begins {start, date, long}', + values: { start: new Date(2018, 5, 20) }, + }) + ).toBe('Sale begins June 20, 2018'); + + expect( + i18n.translate('foo', { + defaultMessage: 'Sale begins {start, date, full}', + values: { start: new Date(2018, 5, 20) }, + }) + ).toBe('Sale begins Wednesday, June 20, 2018'); + }); + + 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}', + }); + + expect( + i18n.translate('a.short', { + values: { expires: new Date(2018, 5, 20, 18, 40, 30, 50) }, + }) + ).toBe('Coupon expires at 6:40 PM'); + + expect( + i18n.translate('a.medium', { + values: { expires: new Date(2018, 5, 20, 18, 40, 30, 50) }, + }) + ).toBe('Coupon expires at 6:40:30 PM'); + }); + + test('should format default messages with time formatter', () => { + i18n.setDefaultLocale('en'); + + expect( + i18n.translate('foo', { + defaultMessage: 'Coupon expires at {expires, time, short}', + values: { expires: new Date(2018, 5, 20, 18, 40, 30, 50) }, + }) + ).toBe('Coupon expires at 6:40 PM'); + + expect( + i18n.translate('foo', { + defaultMessage: 'Coupon expires at {expires, time, medium}', + values: { expires: new Date(2018, 5, 20, 18, 40, 30, 50) }, + }) + ).toBe('Coupon expires at 6:40:30 PM'); + }); + + test('should format message with a custom format', () => { + i18n.init({ + locale: 'en', + formats: { + number: { + usd: { style: 'currency', currency: 'USD' }, + }, + }, + ['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'); + + i18n.setFormats({ + number: { + eur: { style: 'currency', currency: 'EUR' }, + }, + }); + + expect(i18n.translate('a.b.c', { values: { total: 1000 } })).toBe('Your total is $1,000.00'); + + expect(i18n.translate('d.e.f', { values: { total: 1000 } })).toBe('Your total is €1,000.00'); + }); + + test('should format default message with a custom format', () => { + i18n.init({ + locale: 'en', + formats: { + number: { + usd: { style: 'currency', currency: 'USD' }, + }, + }, + }); + i18n.setDefaultLocale('en'); + + expect( + i18n.translate('a.b.c', { + values: { total: 1000 }, + defaultMessage: 'Your total is {total, number, usd}', + }) + ).toBe('Your total is $1,000.00'); + + i18n.setFormats({ + number: { + eur: { style: 'currency', currency: 'EUR' }, + }, + }); + + expect( + i18n.translate('a.b.c', { + values: { total: 1000 }, + defaultMessage: 'Your total is {total, number, usd}', + }) + ).toBe('Your total is $1,000.00'); + + expect( + i18n.translate('d.e.f', { + values: { total: 1000 }, + defaultMessage: 'Your total is {total, number, eur}', + }) + ).toBe('Your total is €1,000.00'); + }); + + 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}', + }); + i18n.setDefaultLocale('en'); + + expect(i18n.translate('a.b.c', { values: { total: 1000 } })).toBe('Your total is 1,000'); + + expect( + i18n.translate('d.e.f', { + values: { total: 1000 }, + defaultMessage: 'Your total is {total, number, foo}', + }) + ).toBe('Your total is 1,000'); + }); + + test('should throw error if used format is not specified', () => { + i18n.init({ + locale: 'en', + ['a.b.c']: 'Your total is {total, foo}', + }); + i18n.setDefaultLocale('en'); + + expect(() => + i18n.translate('a.b.c', { values: { total: 1 } }) + ).toThrowErrorMatchingSnapshot(); + + expect(() => + i18n.translate('d.e.f', { + values: { total: 1000 }, + defaultMessage: 'Your total is {total, bar}', + }) + ).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('init', () => { + test('should not initialize the engine if messages are not specified', () => { + i18n.init(); + expect(i18n.getMessages()).toEqual({}); + }); + + test('should throw error if messages are empty', () => { + expect(() => i18n.init({})).toThrow(); + expect(i18n.getMessages()).toEqual({}); + }); + + test('should add messages if locale is specified', () => { + i18n.init({ + locale: 'en', + foo: 'bar', + }); + + expect(i18n.getMessages()).toEqual({ + locale: 'en', + foo: 'bar', + }); + }); + + test('should set the current locale', () => { + i18n.init({ locale: 'ru' }); + expect(i18n.getLocale()).toBe('ru'); + }); + + test('should add custom formats', () => { + i18n.init({ + locale: 'ru', + formats: { + date: { + custom: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + }, + }, + }); + + expect(i18n.getFormats().date.custom).toEqual({ + month: 'short', + day: 'numeric', + year: 'numeric', + }); + }); + }); +}); diff --git a/packages/kbn-i18n/src/loader.test.js b/packages/kbn-i18n/src/loader.test.js new file mode 100644 index 0000000000000..ddc630d8a6724 --- /dev/null +++ b/packages/kbn-i18n/src/loader.test.js @@ -0,0 +1,281 @@ +/* + * 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 { join } from 'path'; + +describe('I18n loader', () => { + let i18nLoader; + + beforeEach(() => { + i18nLoader = require('./loader'); + }); + + afterEach(() => { + // isolate modules for every test so that local module state doesn't conflict between tests + jest.resetModules(); + }); + + describe('registerTranslationFile', () => { + test('should throw error if path to translation file is not specified', () => { + expect(() => i18nLoader.registerTranslationFile()).toThrowErrorMatchingSnapshot(); + }); + + test('should throw error if path to translation file is not an absolute', () => { + expect(() => i18nLoader.registerTranslationFile('./en.json')).toThrowErrorMatchingSnapshot(); + }); + + test('should throw error if path to translation file does not have an extension', () => { + expect(() => + i18nLoader.registerTranslationFile( + join(__dirname, './__fixtures__/test_plugin_1/translations/en') + ) + ).toThrow(); + }); + + test('should throw error if translation file is not a json', () => { + expect(() => + i18nLoader.registerTranslationFile( + join(__dirname, './__fixtures__/test_plugin_1/translations/en.txt') + ) + ).toThrow(); + }); + + test('should register a translation file', () => { + expect(i18nLoader.getRegisteredLocales()).toEqual([]); + + i18nLoader.registerTranslationFile( + join(__dirname, './__fixtures__/test_plugin_1/translations/en.json') + ); + + expect(i18nLoader.getRegisteredLocales()).toEqual(['en']); + + i18nLoader.registerTranslationFile( + join(__dirname, './__fixtures__/test_plugin_1/translations/en-US.json') + ); + + expect(i18nLoader.getRegisteredLocales()).toContain('en', 'en-US'); + expect(i18nLoader.getRegisteredLocales().length).toBe(2); + }); + }); + + describe('registerTranslationFiles', () => { + test('should register array of translation files', () => { + expect(i18nLoader.getRegisteredLocales()).toEqual([]); + + i18nLoader.registerTranslationFiles([ + join(__dirname, './__fixtures__/test_plugin_1/translations/en.json'), + join(__dirname, './__fixtures__/test_plugin_1/translations/en-US.json'), + ]); + + expect(i18nLoader.getRegisteredLocales()).toContain('en', 'en-US'); + expect(i18nLoader.getRegisteredLocales().length).toBe(2); + }); + }); + + describe('getTranslationsByLocale', () => { + test('should return translation messages by specified locale', async () => { + i18nLoader.registerTranslationFile( + join(__dirname, './__fixtures__/test_plugin_1/translations/en.json') + ); + + expect(await i18nLoader.getTranslationsByLocale('en')).toEqual({ + locale: 'en', + ['a.b.c']: 'foo', + ['d.e.f']: 'bar', + }); + }); + + test('should return empty object if passed locale is not registered', async () => { + i18nLoader.registerTranslationFile( + join(__dirname, './__fixtures__/test_plugin_1/translations/en.json') + ); + + expect(await i18nLoader.getTranslationsByLocale('ru')).toEqual({}); + }); + + test('should return translation messages from a couple of files by specified locale', async () => { + i18nLoader.registerTranslationFiles([ + join(__dirname, './__fixtures__/test_plugin_1/translations/en.json'), + join(__dirname, './__fixtures__/test_plugin_2/translations/en.json'), + ]); + + 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', + }); + }); + + test('should return translation messages for different locales', async () => { + i18nLoader.registerTranslationFiles([ + join(__dirname, './__fixtures__/test_plugin_1/translations/en.json'), + join(__dirname, './__fixtures__/test_plugin_1/translations/en-US.json'), + join(__dirname, './__fixtures__/test_plugin_2/translations/en.json'), + join(__dirname, './__fixtures__/test_plugin_2/translations/ru.json'), + ]); + + 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', + }); + + expect(await i18nLoader.getTranslationsByLocale('en-US')).toEqual({ + locale: 'en-US', + ['a.b.c']: 'bar', + ['d.e.f']: 'foo', + }); + + expect(await i18nLoader.getTranslationsByLocale('ru')).toEqual({ + locale: 'ru', + test: 'test', + }); + }); + + test('should return translation messages from JSON5 file', async () => { + i18nLoader.registerTranslationFile( + join(__dirname, './__fixtures__/test_plugin_2/translations/fr.json') + ); + + expect(await i18nLoader.getTranslationsByLocale('fr')).toEqual({ + locale: 'fr', + test: 'test', + }); + }); + }); + + 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([ + join(__dirname, './__fixtures__/test_plugin_1/translations/en.json'), + join(__dirname, './__fixtures__/test_plugin_1/translations/en-US.json'), + join(__dirname, './__fixtures__/test_plugin_2/translations/en.json'), + join(__dirname, './__fixtures__/test_plugin_2/translations/ru.json'), + ]); + + 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', + }, + ['en-US']: { + locale: 'en-US', + ['a.b.c']: 'bar', + ['d.e.f']: 'foo', + }, + ru: { + locale: 'ru', + test: 'test', + }, + }); + }); + + test('should return empty object if there are no registered locales', async () => { + expect(await i18nLoader.getAllTranslations()).toEqual({}); + }); + }); + + describe('getAllTranslationsFromPaths', () => { + test('should return translation messages for all passed paths to translation files', async () => { + expect( + await i18nLoader.getAllTranslationsFromPaths([ + join(__dirname, './__fixtures__/test_plugin_1/translations/en.json'), + join(__dirname, './__fixtures__/test_plugin_1/translations/en-US.json'), + join(__dirname, './__fixtures__/test_plugin_2/translations/en.json'), + join(__dirname, './__fixtures__/test_plugin_2/translations/ru.json'), + ]) + ).toEqual({ + en: { + locale: 'en', + ['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', + }, + ru: { + locale: 'ru', + test: 'test', + }, + }); + }); + + test('should return empty object if there are no translation files', async () => { + expect(await i18nLoader.getAllTranslationsFromPaths()).toEqual({}); + }); + }); +}); diff --git a/packages/kbn-i18n/src/react/__snapshots__/provider.test.js.snap b/packages/kbn-i18n/src/react/__snapshots__/provider.test.js.snap new file mode 100644 index 0000000000000..750d2d48b045a --- /dev/null +++ b/packages/kbn-i18n/src/react/__snapshots__/provider.test.js.snap @@ -0,0 +1,139 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`I18nProvider provides with context 1`] = ` +Object { + "defaultFormats": 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", + }, + }, + }, + "defaultLocale": "en", + "formatDate": [Function], + "formatHTMLMessage": [Function], + "formatMessage": [Function], + "formatNumber": [Function], + "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", + }, + }, + }, + "formatters": Object { + "getDateTimeFormat": [Function], + "getMessageFormat": [Function], + "getNumberFormat": [Function], + "getPluralFormat": [Function], + "getRelativeFormat": [Function], + }, + "locale": "en", + "messages": Object {}, + "now": [Function], + "textComponent": "span", +} +`; + +exports[`I18nProvider renders children 1`] = `