i18n engine typescript migration (#22441)

* configure typescript build, add necessary dependencies, change extensions, react migration

* migrate lib files in root

* update tests snapshots, resolve core loader, helper

* fix types for core components

* fix angular components

* fix angular staff

* use Messages type

* first-upper-case letter while using classs

* use stable latest babel, fix ts issues

* optimize .babelrc

* update lock file

* Fix x-pack/yarn.lock

* fix issue with unknown babel plugin

* add babel-config.js file with babel configuration for i18n engine build process instead of .babelrc file to fix jest issue

* Resolve comments

* Fix babel config

* Fix packages incompatibility issue

* Fix tslint errors

* Fix tests

* Resolve comments

* Fix types
This commit is contained in:
Aliaksandr Yankouski 2018-10-02 01:55:15 -07:00 committed by Leanid Shutau
parent 6b3bc45b9a
commit a002ee4369
39 changed files with 1793 additions and 1271 deletions

View file

@ -210,8 +210,9 @@
"@kbn/eslint-plugin-license-header": "link:packages/kbn-eslint-plugin-license-header",
"@kbn/plugin-generator": "link:packages/kbn-plugin-generator",
"@kbn/test": "link:packages/kbn-test",
"@types/angular": "^1.6.50",
"@types/angular-mocks": "^1.7.0",
"@octokit/rest": "^15.10.0",
"@types/angular": "^1.6.45",
"@types/babel-core": "^6.25.5",
"@types/bluebird": "^3.1.1",
"@types/boom": "^7.2.0",

View file

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

View file

@ -1,4 +1,5 @@
{
"browser": "../target/web/angular",
"main": "../target/node/angular"
"main": "../target/node/angular",
"types": "./target/types/angular/index.d.ts"
}

View file

@ -0,0 +1,51 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// We can't use common Kibana presets here because of babel versions incompatibility
module.exports = {
plugins: ['@babel/plugin-proposal-class-properties', '@babel/plugin-proposal-object-rest-spread'],
presets: ['@babel/preset-react', '@babel/typescript'],
env: {
web: {
presets: [
[
'@babel/preset-env',
{
targets: {
browsers: ['last 2 versions', '> 5%', 'Safari 7'],
},
},
],
],
},
node: {
presets: [
[
'@babel/preset-env',
{
targets: {
node: 'current',
},
},
],
],
},
},
ignore: ['**/*.test.ts', '**/*.test.tsx'],
};

View file

@ -2,30 +2,40 @@
"name": "@kbn/i18n",
"browser": "./target/web/browser.js",
"main": "./target/node/index.js",
"types": "./target/types/index.d.ts",
"module": "./src/index.js",
"version": "1.0.0",
"license": "Apache-2.0",
"private": true,
"scripts": {
"build": "yarn build:web && yarn build:node",
"build:web": "cross-env BABEL_ENV=web babel src --out-dir target/web",
"build:node": "cross-env BABEL_ENV=node babel src --out-dir target/node",
"build": "yarn build:web && yarn build:node && yarn build:types",
"build:types": "tsc --emitDeclarationOnly",
"build:web": "cross-env BABEL_ENV=web babel src --config-file ./babel.config.js --out-dir target/web --extensions \".ts,.js,.tsx\"",
"build:node": "cross-env BABEL_ENV=node babel src --config-file ./babel.config.js --out-dir target/node --extensions \".ts,.js,.tsx\"",
"kbn:bootstrap": "yarn build",
"kbn:watch": "yarn build --watch"
},
"devDependencies": {
"@kbn/babel-preset": "link:../kbn-babel-preset",
"@kbn/dev-utils": "link:../kbn-dev-utils",
"babel-cli": "^6.26.0",
"cross-env": "^5.2.0"
"@babel/cli": "^7.1.0",
"@babel/core": "^7.1.0",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-object-rest-spread": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"@babel/preset-typescript": "^7.1.0",
"@types/intl-relativeformat": "^2.1.0",
"@types/json5": "^0.0.30",
"@types/react-intl": "^2.3.11",
"cross-env": "^5.2.0",
"typescript": "^3.0.3"
},
"dependencies": {
"intl-format-cache": "^2.1.0",
"intl-messageformat": "^2.2.0",
"intl-relativeformat": "^2.1.0",
"json5": "^1.0.1",
"prop-types": "^15.5.8",
"json5": "^2.0.1",
"prop-types": "^15.6.2",
"react": "^16.3.0",
"react-intl": "^2.4.0"
"react-intl": "^2.7.0"
}
}

View file

@ -1,4 +1,5 @@
{
"browser": "../target/web/react",
"main": "../target/node/react"
"main": "../target/node/react",
"types": "./target/types/react/index.d.ts"
}

View file

@ -19,27 +19,30 @@
import angular from 'angular';
import 'angular-mocks';
import { i18nDirective } from './directive';
import { i18nProvider } from './provider';
import { I18nProvider } from './provider';
angular
.module('app', [])
.provider('i18n', i18nProvider)
.provider('i18n', I18nProvider)
.directive('i18nId', i18nDirective);
describe('i18nDirective', () => {
let compile;
let scope;
let compile: angular.ICompileService;
let scope: angular.IRootScopeService;
beforeEach(angular.mock.module('app'));
beforeEach(
angular.mock.inject(($compile, $rootScope) => {
compile = $compile;
scope = $rootScope.$new();
})
angular.mock.inject(
($compile: angular.ICompileService, $rootScope: angular.IRootScopeService) => {
compile = $compile;
scope = $rootScope.$new();
}
)
);
it('inserts correct translation html content', () => {
test('inserts correct translation html content', () => {
const id = 'id';
const defaultMessage = 'default-message';
@ -56,7 +59,7 @@ describe('i18nDirective', () => {
expect(element.html()).toEqual(defaultMessage);
});
it('inserts correct translation html content with values', () => {
test('inserts correct translation html content with values', () => {
const id = 'id';
const defaultMessage = 'default-message {word}';
const compiledContent = 'default-message word';

View file

@ -17,7 +17,11 @@
* under the License.
*/
export function i18nDirective(i18n) {
import { IDirective, IRootElementService, IScope } from 'angular';
import { I18nServiceType } from './provider';
export function i18nDirective(i18n: I18nServiceType): IDirective {
return {
restrict: 'A',
scope: {
@ -25,19 +29,18 @@ export function i18nDirective(i18n) {
defaultMessage: '@i18nDefaultMessage',
values: '=i18nValues',
},
link: function($scope, $element) {
$scope.$watchGroup(['id', 'defaultMessage', 'values'], function([
id,
defaultMessage = '',
values = {},
]) {
$element.html(
i18n(id, {
values,
defaultMessage,
})
);
});
link($scope: IScope, $element: IRootElementService) {
$scope.$watchGroup(
['id', 'defaultMessage', 'values'],
([id, defaultMessage = '', values = {}]) => {
$element.html(
i18n(id, {
values,
defaultMessage,
})
);
}
);
},
};
}

View file

@ -17,23 +17,24 @@
* 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'),
}));
import angular from 'angular';
import 'angular-mocks';
import * as i18n from '../core/i18n';
import { i18nFilter as angularI18nFilter } from './filter';
import { I18nProvider, I18nServiceType } from './provider';
angular
.module('app', [])
.provider('i18n', i18nProvider)
.filter('i18n', i18nFilter);
.provider('i18n', I18nProvider)
.filter('i18n', angularI18nFilter);
describe('i18nFilter', () => {
let filter;
let filter: I18nServiceType;
beforeEach(angular.mock.module('app'));
beforeEach(
@ -45,15 +46,15 @@ describe('i18nFilter', () => {
jest.resetAllMocks();
});
it('provides wrapper around i18n engine', () => {
test('provides wrapper around i18n engine', () => {
const id = 'id';
const defaultMessage = 'default-message';
const values = {};
const result = filter(id, { defaultMessage, values });
expect(result).toEqual('translation');
expect(i18n.translate).toHaveBeenCalledTimes(1);
expect(i18n.translate).toHaveBeenCalledWith(id, { defaultMessage, values });
expect(result).toEqual('translation');
});
});

View file

@ -17,8 +17,10 @@
* under the License.
*/
export function i18nFilter(i18n) {
return function(id, { defaultMessage = '', values = {} } = {}) {
import { I18nServiceType } from './provider';
export function i18nFilter(i18n: I18nServiceType) {
return (id: string, { defaultMessage = '', values = {} } = {}) => {
return i18n(id, {
values,
defaultMessage,

View file

@ -17,6 +17,6 @@
* under the License.
*/
export { i18nProvider } from './provider';
export { I18nProvider } from './provider';
export { i18nFilter } from './filter';
export { i18nDirective } from './directive';

View file

@ -19,36 +19,37 @@
import angular from 'angular';
import 'angular-mocks';
import { i18nProvider } from './provider';
import * as i18n from '../core/i18n';
angular.module('app', []).provider('i18n', i18nProvider);
import * as i18nCore from '../core/i18n';
import { I18nProvider, I18nServiceType } from './provider';
angular.module('app', []).provider('i18n', I18nProvider);
describe('i18nProvider', () => {
let provider;
let service;
let provider: I18nProvider;
let service: I18nServiceType;
beforeEach(
angular.mock.module('app', [
'i18nProvider',
i18n => {
service = i18n;
(i18n: I18nProvider) => {
provider = i18n;
},
])
);
beforeEach(
angular.mock.inject(i18n => {
provider = i18n;
angular.mock.inject((i18n: I18nServiceType) => {
service = i18n;
})
);
it('provides wrapper around i18n engine', () => {
expect(provider).toEqual(i18n.translate);
test('provides wrapper around i18n engine', () => {
expect(service).toEqual(i18nCore.translate);
});
it('provides service wrapper around i18n engine', () => {
const serviceMethodNames = Object.keys(service);
const pluginMethodNames = Object.keys(i18n);
test('provides service wrapper around i18n engine', () => {
const serviceMethodNames = Object.keys(provider);
const pluginMethodNames = Object.keys(i18nCore);
expect([...serviceMethodNames, 'translate'].sort()).toEqual(
[...pluginMethodNames, '$get'].sort()

View file

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

View file

@ -28,7 +28,7 @@
* described in `options` section of [DateTimeFormat constructor].
* {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat}
*/
export const formats = {
export const formats: Formats = {
number: {
currency: {
style: 'currency',
@ -84,3 +84,43 @@ export const formats = {
},
},
};
interface NumberFormatOptions<TStyle extends string> extends Intl.NumberFormatOptions {
style?: TStyle;
localeMatcher?: 'lookup' | 'best fit';
currencyDisplay?: 'symbol' | 'code' | 'name';
}
export interface Formats {
number?: Partial<{
[key: string]: NumberFormatOptions<'currency' | 'percent' | 'decimal'>;
currency: NumberFormatOptions<'currency'>;
percent: NumberFormatOptions<'percent'>;
}>;
date?: Partial<{
[key: string]: DateTimeFormatOptions;
short: DateTimeFormatOptions;
medium: DateTimeFormatOptions;
long: DateTimeFormatOptions;
full: DateTimeFormatOptions;
}>;
time?: Partial<{
[key: string]: DateTimeFormatOptions;
short: DateTimeFormatOptions;
medium: DateTimeFormatOptions;
long: DateTimeFormatOptions;
full: DateTimeFormatOptions;
}>;
}
interface DateTimeFormatOptions extends Intl.DateTimeFormatOptions {
weekday?: 'narrow' | 'short' | 'long';
era?: 'narrow' | 'short' | 'long';
year?: 'numeric' | '2-digit';
month?: 'numeric' | '2-digit' | 'narrow' | 'short' | 'long';
day?: 'numeric' | '2-digit';
hour?: 'numeric' | '2-digit';
minute?: 'numeric' | '2-digit';
second?: 'numeric' | '2-digit';
timeZoneName?: 'short' | 'long';
}

View file

@ -17,7 +17,7 @@
* under the License.
*/
import { isString, isObject, hasValues, unique, mergeAll } from './helper';
import { hasValues, isObject, isString, mergeAll, unique } from './helper';
describe('I18n helper', () => {
describe('isString', () => {
@ -26,6 +26,7 @@ describe('I18n helper', () => {
});
test('should return false for string object', () => {
// tslint:disable-next-line:no-construct
expect(isString(new String('test'))).toBe(false);
});

View file

@ -17,15 +17,16 @@
* under the License.
*/
export const isString = value => typeof value === 'string';
export const isString = (value: any): value is string => typeof value === 'string';
export const isObject = value => typeof value === 'object' && value !== null;
export const isObject = (value: any): value is object =>
typeof value === 'object' && value !== null;
export const hasValues = values => Object.keys(values).length > 0;
export const hasValues = (values: any) => Object.keys(values).length > 0;
export const unique = (arr = []) => [...new Set(arr)];
export const unique = <T>(arr: T[] = []): T[] => [...new Set(arr)];
const merge = (a, b) =>
const merge = (a: any, b: any): { [k: string]: any } =>
unique([...Object.keys(a), ...Object.keys(b)]).reduce((acc, key) => {
if (isObject(a[key]) && isObject(b[key]) && !Array.isArray(a[key]) && !Array.isArray(b[key])) {
return {
@ -40,5 +41,5 @@ const merge = (a, b) =>
};
}, {});
export const mergeAll = (...sources) =>
export const mergeAll = (...sources: any[]) =>
sources.filter(isObject).reduce((acc, source) => merge(acc, source));

View file

@ -17,11 +17,13 @@
* under the License.
*/
import * as i18nModule from './i18n';
describe('I18n engine', () => {
let i18n;
let i18n: typeof i18nModule;
beforeEach(() => {
i18n = require('./i18n');
i18n = require.requireActual('./i18n');
});
afterEach(() => {
@ -181,11 +183,11 @@ describe('I18n engine', () => {
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(undefined as any)).toThrow();
expect(() => i18n.setLocale(null as any)).toThrow();
expect(() => i18n.setLocale(true as any)).toThrow();
expect(() => i18n.setLocale(5 as any)).toThrow();
expect(() => i18n.setLocale({} as any)).toThrow();
expect(() => i18n.setLocale('')).toThrow();
});
@ -214,11 +216,11 @@ describe('I18n engine', () => {
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(undefined as any)).toThrow();
expect(() => i18n.setDefaultLocale(null as any)).toThrow();
expect(() => i18n.setDefaultLocale(true as any)).toThrow();
expect(() => i18n.setDefaultLocale(5 as any)).toThrow();
expect(() => i18n.setDefaultLocale({} as any)).toThrow();
expect(() => i18n.setDefaultLocale('')).toThrow();
});
@ -265,16 +267,16 @@ describe('I18n engine', () => {
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();
expect(() => i18n.setFormats(undefined as any)).toThrow();
expect(() => i18n.setFormats(null as any)).toThrow();
expect(() => i18n.setFormats(true as any)).toThrow();
expect(() => i18n.setFormats(5 as any)).toThrow();
expect(() => i18n.setFormats('foo' as any)).toThrow();
expect(() => i18n.setFormats({} as any)).toThrow();
});
test('should merge current formats with a passed formats', () => {
expect(i18n.getFormats().date.short).not.toEqual({
expect(i18n.getFormats().date!.short).not.toEqual({
month: 'short',
day: 'numeric',
year: 'numeric',
@ -290,7 +292,7 @@ describe('I18n engine', () => {
},
});
expect(i18n.getFormats().date.short).toEqual({
expect(i18n.getFormats().date!.short).toEqual({
month: 'short',
day: 'numeric',
year: 'numeric',
@ -304,7 +306,7 @@ describe('I18n engine', () => {
},
});
expect(i18n.getFormats().date.short).toEqual({
expect(i18n.getFormats().date!.short).toEqual({
month: 'long',
day: 'numeric',
year: 'numeric',
@ -323,12 +325,23 @@ describe('I18n engine', () => {
const { formats } = require('./formats');
i18n.setFormats({
foo: 'bar',
number: {
currency: {
style: 'currency',
currency: 'EUR',
},
},
});
expect(i18n.getFormats()).toEqual({
...formats,
foo: 'bar',
number: {
...formats.number,
currency: {
style: 'currency',
currency: 'EUR',
},
},
});
});
});
@ -349,25 +362,28 @@ describe('I18n engine', () => {
locale: 'ru',
});
expect(i18n.getRegisteredLocales()).toContain('en', 'ru');
expect(i18n.getRegisteredLocales()).toContain('en');
expect(i18n.getRegisteredLocales()).toContain('ru');
expect(i18n.getRegisteredLocales().length).toBe(2);
i18n.addMessages({
locale: 'fr',
});
expect(i18n.getRegisteredLocales()).toContain('en', 'ru', 'fr');
expect(i18n.getRegisteredLocales()).toContain('en');
expect(i18n.getRegisteredLocales()).toContain('fr');
expect(i18n.getRegisteredLocales()).toContain('ru');
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(undefined as any)).toThrowErrorMatchingSnapshot();
expect(() => i18n.translate(null as any)).toThrowErrorMatchingSnapshot();
expect(() => i18n.translate(true as any)).toThrowErrorMatchingSnapshot();
expect(() => i18n.translate(5 as any)).toThrowErrorMatchingSnapshot();
expect(() => i18n.translate({} as any)).toThrowErrorMatchingSnapshot();
expect(() => i18n.translate('')).toThrowErrorMatchingSnapshot();
});
@ -764,7 +780,7 @@ describe('I18n engine', () => {
},
});
expect(i18n.getFormats().date.custom).toEqual({
expect((i18n.getFormats().date as any).custom).toEqual({
month: 'short',
day: 'numeric',
year: 'numeric',

View file

@ -17,25 +17,20 @@
* 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 memoizeIntlConstructor from 'intl-format-cache';
import IntlMessageFormat from 'intl-messageformat';
import IntlRelativeFormat from 'intl-relativeformat';
import memoizeIntlConstructor from 'intl-format-cache';
import { isString, isObject, hasValues, mergeAll } from './helper';
import { formats as EN_FORMATS } from './formats';
import { Messages, PlainMessages } from '../messages';
import { Formats, formats as EN_FORMATS } from './formats';
import { hasValues, isObject, isString, mergeAll } from './helper';
// Add all locale data to `IntlMessageFormat`.
import './locales';
import './locales.js';
const EN_LOCALE = 'en';
const LOCALE_DELIMITER = '-';
const messages = {};
const messages: Messages = {};
const getMessageFormat = memoizeIntlConstructor(IntlMessageFormat);
let defaultLocale = EN_LOCALE;
@ -47,28 +42,26 @@ IntlRelativeFormat.defaultLocale = defaultLocale;
/**
* Returns message by the given message id.
* @param {string} id - path to the message
* @returns {string} message - translated message from messages tree
* @param id - path to the message
*/
function getMessageById(id) {
function getMessageById(id: string): string {
return getMessages()[id];
}
/**
* Normalizes locale to make it consistent with IntlMessageFormat locales
* @param {string} locale
* @returns {string} normalizedLocale
* @param locale
*/
function normalizeLocale(locale) {
function normalizeLocale(locale: string) {
return locale.toLowerCase().replace('_', LOCALE_DELIMITER);
}
/**
* Provides a way to register translations with the engine
* @param {Messages} newMessages
* @param {string} [locale = messages.locale]
* @param newMessages
* @param [locale = messages.locale]
*/
export function addMessages(newMessages = {}, locale = newMessages.locale) {
export function addMessages(newMessages: PlainMessages = {}, locale = newMessages.locale) {
if (!locale || !isString(locale)) {
throw new Error('[I18n] A `locale` must be a non-empty string to add messages.');
}
@ -89,17 +82,16 @@ export function addMessages(newMessages = {}, locale = newMessages.locale) {
/**
* Returns messages for the current language
* @returns {Messages} messages
*/
export function getMessages() {
export function getMessages(): PlainMessages {
return messages[currentLocale] || {};
}
/**
* Tells the engine which language to use by given language key
* @param {string} locale
* @param locale
*/
export function setLocale(locale) {
export function setLocale(locale: string) {
if (!locale || !isString(locale)) {
throw new Error('[I18n] A `locale` must be a non-empty string.');
}
@ -109,7 +101,6 @@ export function setLocale(locale) {
/**
* Returns the current locale
* @returns {string} locale
*/
export function getLocale() {
return currentLocale;
@ -117,9 +108,9 @@ export function getLocale() {
/**
* Tells the library which language to fallback when missing translations
* @param {string} locale
* @param locale
*/
export function setDefaultLocale(locale) {
export function setDefaultLocale(locale: string) {
if (!locale || !isString(locale)) {
throw new Error('[I18n] A `locale` must be a non-empty string.');
}
@ -129,10 +120,6 @@ export function setDefaultLocale(locale) {
IntlRelativeFormat.defaultLocale = defaultLocale;
}
/**
* Returns the default locale
* @returns {string} defaultLocale
*/
export function getDefaultLocale() {
return defaultLocale;
}
@ -143,12 +130,12 @@ export function getDefaultLocale() {
* {@link https://github.com/yahoo/intl-messageformat/blob/master/src/core.js#L62}
* These are used when constructing the internal Intl.NumberFormat
* and Intl.DateTimeFormat instances.
* @param {object} newFormats
* @param {object} [newFormats.number]
* @param {object} [newFormats.date]
* @param {object} [newFormats.time]
* @param newFormats
* @param [newFormats.number]
* @param [newFormats.date]
* @param [newFormats.time]
*/
export function setFormats(newFormats) {
export function setFormats(newFormats: Formats) {
if (!isObject(newFormats) || !hasValues(newFormats)) {
throw new Error('[I18n] A `formats` must be a non-empty object.');
}
@ -158,7 +145,6 @@ export function setFormats(newFormats) {
/**
* Returns current formats
* @returns {object} formats
*/
export function getFormats() {
return formats;
@ -166,21 +152,30 @@ export function getFormats() {
/**
* Returns array of locales having translations
* @returns {string[]} locales
*/
export function getRegisteredLocales() {
return Object.keys(messages);
}
interface TranslateArguments {
values?: { [key: string]: string | number | Date };
defaultMessage?: string;
}
/**
* Translate message by id
* @param {string} id - translation id to be translated
* @param {object} [options]
* @param {object} [options.values] - values to pass into translation
* @param {string} [options.defaultMessage] - will be used unless translation was successful
* @returns {string}
* @param id - translation id to be translated
* @param [options]
* @param [options.values] - values to pass into translation
* @param [options.defaultMessage] - will be used unless translation was successful
*/
export function translate(id, { values = {}, defaultMessage = '' } = {}) {
export function translate(
id: string,
{ values = {}, defaultMessage = '' }: TranslateArguments = {
values: {},
defaultMessage: '',
}
) {
if (!id || !isString(id)) {
throw new Error('[I18n] An `id` must be a non-empty string to translate a message.');
}
@ -218,9 +213,9 @@ export function translate(id, { values = {}, defaultMessage = '' } = {}) {
/**
* Initializes the engine
* @param {Messages} newMessages
* @param newMessages
*/
export function init(newMessages) {
export function init(newMessages?: PlainMessages) {
if (!newMessages) {
return;
}

View file

@ -20,7 +20,7 @@
import { join } from 'path';
describe('I18n loader', () => {
let i18nLoader;
let i18nLoader: typeof import('./loader');
beforeEach(() => {
i18nLoader = require('./loader');
@ -33,7 +33,9 @@ describe('I18n loader', () => {
describe('registerTranslationFile', () => {
test('should throw error if path to translation file is not specified', () => {
expect(() => i18nLoader.registerTranslationFile()).toThrowErrorMatchingSnapshot();
expect(() =>
i18nLoader.registerTranslationFile(undefined as any)
).toThrowErrorMatchingSnapshot();
});
test('should throw error if path to translation file is not an absolute', () => {
@ -69,7 +71,8 @@ describe('I18n loader', () => {
join(__dirname, './__fixtures__/test_plugin_1/translations/en-US.json')
);
expect(i18nLoader.getRegisteredLocales()).toContain('en', 'en-US');
expect(i18nLoader.getRegisteredLocales()).toContain('en');
expect(i18nLoader.getRegisteredLocales()).toContain('en-US');
expect(i18nLoader.getRegisteredLocales().length).toBe(2);
});
});
@ -83,7 +86,8 @@ describe('I18n loader', () => {
join(__dirname, './__fixtures__/test_plugin_1/translations/en-US.json'),
]);
expect(i18nLoader.getRegisteredLocales()).toContain('en', 'en-US');
expect(i18nLoader.getRegisteredLocales()).toContain('en');
expect(i18nLoader.getRegisteredLocales()).toContain('en-US');
expect(i18nLoader.getRegisteredLocales().length).toBe(2);
});
});
@ -228,7 +232,7 @@ describe('I18n loader', () => {
});
test('should return empty object if there are no translation files', async () => {
expect(await i18nLoader.getAllTranslationsFromPaths()).toEqual({});
expect(await i18nLoader.getAllTranslationsFromPaths(undefined as any)).toEqual({});
});
});
});

View file

@ -17,44 +17,38 @@
* under the License.
*/
/**
@typedef Messages - messages tree, where leafs are translated strings
@type {object<string, object>}
@property {string} [locale] - locale of the messages
@property {object} [formats] - set of options to the underlying formatter
*/
import path from 'path';
import { readFile } from 'fs';
import * as JSON5 from 'json5';
import * as path from 'path';
import { promisify } from 'util';
import JSON5 from 'json5';
import { unique } from './core/helper';
import { Messages, PlainMessages } from './messages';
const asyncReadFile = promisify(readFile);
const TRANSLATION_FILE_EXTENSION = '.json';
/**
* Internal property for storing registered translations paths
* @type {Map<string, string[]>|{}} - Key is locale, value is array of registered paths
* Internal property for storing registered translations paths.
* Key is locale, value is array of registered paths
*/
const translationsRegistry = {};
const translationsRegistry: { [key: string]: string[] } = {};
/**
* Internal property for caching loaded translations files
* @type {Map<string, Messages>|{}} - Key is path to translation file, value is
* object with translation messages
* Internal property for caching loaded translations files.
* Key is path to translation file, value is object with translation messages
*/
const loadedFiles = {};
const loadedFiles: { [key: string]: PlainMessages } = {};
/**
* Returns locale by the given translation file name
* @param {string} fullFileName
* @returns {string} locale
* @param fullFileName
* @returns locale
* @example
* getLocaleFromFileName('./path/to/translation/ru.json') // => 'ru'
*/
function getLocaleFromFileName(fullFileName) {
function getLocaleFromFileName(fullFileName: string) {
if (!fullFileName) {
throw new Error('Filename is empty');
}
@ -72,19 +66,19 @@ function getLocaleFromFileName(fullFileName) {
/**
* Loads file and parses it as JSON5
* @param {string} pathToFile
* @returns {Promise<object>}
* @param pathToFile
* @returns
*/
async function loadFile(pathToFile) {
async function loadFile(pathToFile: string): Promise<PlainMessages> {
return JSON5.parse(await asyncReadFile(pathToFile, 'utf8'));
}
/**
* Loads translations files and adds them into "loadedFiles" cache
* @param {string[]} files
* @returns {Promise<void>}
* @param files
* @returns
*/
async function loadAndCacheFiles(files) {
async function loadAndCacheFiles(files: string[]) {
const translations = await Promise.all(files.map(loadFile));
files.forEach((file, index) => {
@ -94,9 +88,9 @@ async function loadAndCacheFiles(files) {
/**
* Registers translation file with i18n loader
* @param {string} translationFilePath - Absolute path to the translation file to register.
* @param translationFilePath - Absolute path to the translation file to register.
*/
export function registerTranslationFile(translationFilePath) {
export function registerTranslationFile(translationFilePath: string) {
if (!path.isAbsolute(translationFilePath)) {
throw new TypeError(
'Paths to translation files must be absolute. ' +
@ -114,15 +108,15 @@ export function registerTranslationFile(translationFilePath) {
/**
* Registers array of translation files with i18n loader
* @param {string[]} arrayOfPaths - Array of absolute paths to the translation files to register.
* @param arrayOfPaths - Array of absolute paths to the translation files to register.
*/
export function registerTranslationFiles(arrayOfPaths = []) {
export function registerTranslationFiles(arrayOfPaths: string[] = []) {
arrayOfPaths.forEach(registerTranslationFile);
}
/**
* Returns an array of locales that have been registered with i18n loader
* @returns {string[]} registeredTranslations
* @returns registeredTranslations
*/
export function getRegisteredLocales() {
return Object.keys(translationsRegistry);
@ -130,10 +124,10 @@ export function getRegisteredLocales() {
/**
* Returns translation messages by specified locale
* @param {string} locale
* @returns {Promise<Messages>} translations - translation messages
* @param locale
* @returns translation messages
*/
export async function getTranslationsByLocale(locale) {
export async function getTranslationsByLocale(locale: string): Promise<PlainMessages> {
const files = translationsRegistry[locale] || [];
const notLoadedFiles = files.filter(file => !loadedFiles[file]);
@ -154,10 +148,10 @@ export async function getTranslationsByLocale(locale) {
/**
* Returns all translations for registered locales
* @return {Promise<Map<string, Messages>>} translations - A Promise object
* @returns A Promise object
* where keys are the locale and values are objects of translation messages
*/
export async function getAllTranslations() {
export async function getAllTranslations(): Promise<{ [key: string]: Messages }> {
const locales = getRegisteredLocales();
const translations = await Promise.all(locales.map(getTranslationsByLocale));
@ -173,11 +167,11 @@ export async function getAllTranslations() {
/**
* Registers passed translations files, loads them and returns promise with
* all translation messages
* @param {string[]} paths - Array of absolute paths to the translation files
* @returns {Promise<Map<string, Messages>>} translations - A Promise object
* where keys are the locale and values are objects of translation messages
* @param paths - Array of absolute paths to the translation files
* @returns A Promise object where
* keys are the locale and values are objects of translation messages
*/
export async function getAllTranslationsFromPaths(paths) {
export async function getAllTranslationsFromPaths(paths: string[]) {
registerTranslationFiles(paths);
return await getAllTranslations();

View file

@ -17,18 +17,23 @@
* under the License.
*/
import * as i18n from '../core';
import { Formats } from './core/formats';
export function i18nProvider() {
this.addMessages = i18n.addMessages;
this.getMessages = i18n.getMessages;
this.setLocale = i18n.setLocale;
this.getLocale = i18n.getLocale;
this.setDefaultLocale = i18n.setDefaultLocale;
this.getDefaultLocale = i18n.getDefaultLocale;
this.setFormats = i18n.setFormats;
this.getFormats = i18n.getFormats;
this.getRegisteredLocales = i18n.getRegisteredLocales;
this.init = i18n.init;
this.$get = () => i18n.translate;
/**
* Messages tree, where leafs are translated strings
*/
export interface Messages {
[key: string]: PlainMessages;
}
export interface PlainMessages {
[key: string]: any;
/**
* locale of the messages
*/
locale?: string;
/**
* set of options to the underlying formatter
*/
formats?: Formats;
}

View file

@ -132,7 +132,9 @@ Object {
"locale": "en",
"messages": Object {},
"now": [Function],
"onError": [Function],
"textComponent": "span",
"timeZone": null,
}
`;

View file

@ -17,15 +17,15 @@
* under the License.
*/
import React from 'react';
import { mount, shallow } from 'enzyme';
import * as React from 'react';
import { intlShape } from 'react-intl';
import { shallow, mount } from 'enzyme';
import { I18nProvider } from './provider';
import { injectI18n } from './inject';
import { I18nProvider } from './provider';
describe('I18nProvider', () => {
it('renders children', () => {
const ChildrenMock = () => {};
test('renders children', () => {
const ChildrenMock = () => null;
const wrapper = shallow(
<I18nProvider>
@ -36,7 +36,7 @@ describe('I18nProvider', () => {
expect(wrapper.children()).toMatchSnapshot();
});
it('provides with context', () => {
test('provides with context', () => {
const childrenMock = () => <div />;
const WithIntl = injectI18n(childrenMock);

View file

@ -17,8 +17,8 @@
* under the License.
*/
import React, { PureComponent } from 'react';
import PropTypes from 'prop-types';
import * as PropTypes from 'prop-types';
import * as React from 'react';
import { IntlProvider } from 'react-intl';
import * as i18n from '../core';
@ -28,12 +28,12 @@ import * as i18n from '../core';
* of components. This component is used to setup the i18n context for a tree.
* IntlProvider should wrap react app's root component (inside each react render method).
*/
export class I18nProvider extends PureComponent {
static propTypes = {
export class I18nProvider extends React.PureComponent {
public static propTypes = {
children: PropTypes.object,
};
render() {
public render() {
const { children } = this.props;
return (

View file

@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"types/intl_format_cache.d.ts",
"types/intl_relativeformat.d.ts"
],
"exclude": [
"target"
],
"compilerOptions": {
"declaration": true,
"declarationDir": "./target/types",
}
}

View file

@ -0,0 +1,2 @@
extends:
- ../../tslint.yaml

View file

@ -0,0 +1,32 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
declare module 'intl-format-cache' {
import IntlMessageFormat from 'intl-messageformat';
interface Message {
format: (values: { [key: string]: string | number | Date }) => string;
}
function memoizeIntlConstructor(
IntlMessageFormatCtor: typeof IntlMessageFormat
): (msg: string, locale: string, formats: any) => Message;
export = memoizeIntlConstructor;
}

View file

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

File diff suppressed because it is too large Load diff

View file

@ -20,13 +20,13 @@
import { uiModules } from 'ui/modules';
import { metadata } from 'ui/metadata';
import {
i18nProvider,
I18nProvider,
i18nFilter,
i18nDirective,
} from '@kbn/i18n/angular';
uiModules.get('i18n')
.provider('i18n', i18nProvider)
.provider('i18n', I18nProvider)
.filter('i18n', i18nFilter)
.directive('i18nId', i18nDirective)
.config((i18nProvider) => {

View file

@ -922,7 +922,7 @@ babel-plugin-pegjs-inline-precompile@^0.1.0:
babel-plugin-syntax-object-rest-spread@^6.13.0:
version "6.13.0"
resolved "https://registry.yarnpkg.com/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
resolved "http://registry.npmjs.org/babel-plugin-syntax-object-rest-spread/-/babel-plugin-syntax-object-rest-spread-6.13.0.tgz#fd6536f2bce13836ffa3a5458c4903a597bb3bf5"
babel-plugin-transform-es2015-arrow-functions@^6.22.0:
version "6.22.0"
@ -3818,7 +3818,7 @@ hoist-non-react-statics@^2.3.0:
version "2.3.1"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.3.1.tgz#343db84c6018c650778898240135a1420ee22ce0"
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0:
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.0, hoist-non-react-statics@^2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
@ -4016,7 +4016,7 @@ intl-messageformat@^2.0.0, intl-messageformat@^2.1.0, intl-messageformat@^2.2.0:
dependencies:
intl-messageformat-parser "1.4.0"
intl-relativeformat@^2.0.0, intl-relativeformat@^2.1.0:
intl-relativeformat@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.1.0.tgz#010f1105802251f40ac47d0e3e1a201348a255df"
dependencies:
@ -4918,9 +4918,9 @@ json5@^0.5.0, json5@^0.5.1:
version "0.5.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-0.5.1.tgz#1eade7acc012034ad84e2396767ead9fa5495821"
json5@^1.0.1:
version "1.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-1.0.1.tgz#779fb0018604fa854eacbf6252180d83543e3dbe"
json5@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.0.1.tgz#3d6d0d1066039eb50984e66a7840e4f4b7a2c660"
dependencies:
minimist "^1.2.0"
@ -6690,7 +6690,7 @@ prop-types@^15.5.0, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8,
loose-envify "^1.3.1"
object-assign "^4.1.1"
prop-types@^15.5.7:
prop-types@^15.5.7, prop-types@^15.6.2:
version "15.6.2"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.2.tgz#05d5ca77b4453e985d60fc7ff8c859094a497102"
dependencies:
@ -6973,13 +6973,14 @@ react-input-autosize@^2.1.2, react-input-autosize@^2.2.1:
dependencies:
prop-types "^15.5.8"
react-intl@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.4.0.tgz#66c14dc9df9a73b2fbbfbd6021726e80a613eb15"
react-intl@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.7.0.tgz#be1244769ce51f71476afb9556485a46090db5fa"
dependencies:
hoist-non-react-statics "^2.5.5"
intl-format-cache "^2.0.5"
intl-messageformat "^2.1.0"
intl-relativeformat "^2.0.0"
intl-relativeformat "^2.1.0"
invariant "^2.1.1"
react-is@^16.3.1:

View file

@ -291,9 +291,15 @@
url-join "^4.0.0"
ws "^4.1.0"
"@types/angular@^1.6.45":
version "1.6.45"
resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.45.tgz#5b0b91a51d717f6fc816d59e1234d5292f33f7b9"
"@types/angular-mocks@^1.7.0":
version "1.7.0"
resolved "https://registry.yarnpkg.com/@types/angular-mocks/-/angular-mocks-1.7.0.tgz#310d999a3c47c10ecd8eef466b5861df84799429"
dependencies:
"@types/angular" "*"
"@types/angular@*", "@types/angular@^1.6.50":
version "1.6.50"
resolved "https://registry.yarnpkg.com/@types/angular/-/angular-1.6.50.tgz#8b6599088d80f68ef0cad7d3a2062248ebe72b3d"
"@types/babel-core@^6.25.5":
version "6.25.5"
@ -6454,7 +6460,7 @@ hoist-non-react-statics@^2.3.0, hoist-non-react-statics@^2.5.0:
version "2.5.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.0.tgz#d2ca2dfc19c5a91c5a6615ce8e564ef0347e2a40"
hoist-non-react-statics@^2.3.1:
hoist-non-react-statics@^2.3.1, hoist-non-react-statics@^2.5.5:
version "2.5.5"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-2.5.5.tgz#c5903cf409c0dfd908f388e619d86b9c1174cb47"
@ -6829,7 +6835,7 @@ intl-messageformat@^2.0.0, intl-messageformat@^2.1.0, intl-messageformat@^2.2.0:
dependencies:
intl-messageformat-parser "1.4.0"
intl-relativeformat@^2.0.0, intl-relativeformat@^2.1.0:
intl-relativeformat@^2.1.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/intl-relativeformat/-/intl-relativeformat-2.1.0.tgz#010f1105802251f40ac47d0e3e1a201348a255df"
dependencies:
@ -8001,6 +8007,12 @@ json5@^1.0.1:
dependencies:
minimist "^1.2.0"
json5@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/json5/-/json5-2.0.1.tgz#3d6d0d1066039eb50984e66a7840e4f4b7a2c660"
dependencies:
minimist "^1.2.0"
jsonfile@^3.0.0:
version "3.0.1"
resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-3.0.1.tgz#a5ecc6f65f53f662c4415c7675a0331d0992ec66"
@ -11233,13 +11245,14 @@ react-input-range@^1.3.0:
autobind-decorator "^1.3.4"
prop-types "^15.5.8"
react-intl@^2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.4.0.tgz#66c14dc9df9a73b2fbbfbd6021726e80a613eb15"
react-intl@^2.7.0:
version "2.7.0"
resolved "https://registry.yarnpkg.com/react-intl/-/react-intl-2.7.0.tgz#be1244769ce51f71476afb9556485a46090db5fa"
dependencies:
hoist-non-react-statics "^2.5.5"
intl-format-cache "^2.0.5"
intl-messageformat "^2.1.0"
intl-relativeformat "^2.0.0"
intl-relativeformat "^2.1.0"
invariant "^2.1.1"
react-is@^16.3.1: