From c1943c7b58676ed9fa15ea5efb281dd2328b99b2 Mon Sep 17 00:00:00 2001 From: LeanidShutau Date: Fri, 1 Jun 2018 18:28:23 +0300 Subject: [PATCH 01/24] Implement a build tool for locale files verification --- package.json | 3 + scripts/check_locale_files.js | 21 +++ src/dev/i18n/check_locale_files.js | 202 +++++++++++++++++++++++++++++ src/dev/i18n/utils.js | 25 ++++ src/dev/run_check_locale_files.js | 25 ++++ yarn.lock | 88 +++++++++++++ 6 files changed, 364 insertions(+) create mode 100644 scripts/check_locale_files.js create mode 100644 src/dev/i18n/check_locale_files.js create mode 100644 src/dev/i18n/utils.js create mode 100644 src/dev/run_check_locale_files.js diff --git a/package.json b/package.json index ae6a38a620fad..0fcce878c12cc 100644 --- a/package.json +++ b/package.json @@ -219,6 +219,9 @@ "yauzl": "2.7.0" }, "devDependencies": { + "@babel/parser": "^7.0.0-beta.49", + "@babel/traverse": "^7.0.0-beta.49", + "@babel/types": "^7.0.0-beta.49", "@elastic/eslint-config-kibana": "link:packages/eslint-config-kibana", "@elastic/eslint-plugin-kibana-custom": "link:packages/eslint-plugin-kibana-custom", "@kbn/es": "link:packages/kbn-es", diff --git a/scripts/check_locale_files.js b/scripts/check_locale_files.js new file mode 100644 index 0000000000000..f5e0dffd47efe --- /dev/null +++ b/scripts/check_locale_files.js @@ -0,0 +1,21 @@ +/* + * 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. + */ + +require('../src/setup_node_env'); +require('../src/dev/run_check_locale_files'); diff --git a/src/dev/i18n/check_locale_files.js b/src/dev/i18n/check_locale_files.js new file mode 100644 index 0000000000000..33a0118d81e66 --- /dev/null +++ b/src/dev/i18n/check_locale_files.js @@ -0,0 +1,202 @@ +/* + * 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 path from 'path'; +import JSON5 from 'json5'; +import { parse } from '@babel/parser'; +import traverse from '@babel/traverse'; +import { + isIdentifier, + isObjectExpression, + isStringLiteral, +} from '@babel/types'; + +import { globAsync, readFileAsync } from './utils'; + +function plainify(object) { + const result = {}; + + for (const [key, value] of Object.entries(object)) { + if (typeof value === 'object' && value !== null) { + for (const [nestedKey, nestedValue] of Object.entries(plainify(value))) { + result[`${key}.${nestedKey}`] = nestedValue; + } + } else { + result[key] = value; + } + } + + return result; +} + +function arraysDiff(left = [], right = []) { + const leftDiff = left.filter(value => right.includes(value)); + const rightDiff = right.filter(value => left.includes(value)); + return [leftDiff, rightDiff]; +} + +function readKeySubTree(node) { + if (isStringLiteral(node)) { + return node.value; + } + if (isIdentifier(node)) { + return node.name; + } +} + +function getDuplicates(node, parentPath) { + const keys = []; + const duplicates = []; + + for (const property of node.properties) { + const key = readKeySubTree(property.key); + const nodePath = `${parentPath}.${key}`; + + if (!duplicates.includes(nodePath)) { + if (!keys.includes(nodePath)) { + keys.push(nodePath); + } else { + duplicates.push(nodePath); + } + } + + if (isObjectExpression(property.value)) { + duplicates.push(...getDuplicates(property.value, `${parentPath}.${key}`)); + } + } + + return duplicates; +} + +function verifyJSON(json, fileName) { + const jsonAST = parse(`+${json}`); + let namespace = ''; + + traverse(jsonAST, { + enter(path) { + if (isObjectExpression(path.node)) { + if (path.node.properties.length !== 1) { + throw new Error( + `Locale file ${fileName} should be a JSON with a single-key object` + ); + } + if (!isObjectExpression(path.node.properties[0].value)) { + throw new Error(`Invalid locale file: ${fileName}`); + } + + namespace = readKeySubTree(path.node.properties[0].key); + const duplicates = getDuplicates( + path.node.properties[0].value, + namespace + ); + + if (duplicates.length !== 0) { + throw new Error( + `There are translation id duplicates in locale file ${fileName}: +${duplicates.join(', ')}` + ); + } + + path.stop(); + } + }, + }); + + return namespace; +} + +async function checkFile(localePath) { + let errorMessage = ''; + + const defaultMessagesBuffer = await readFileAsync( + path.resolve(path.dirname(localePath), 'defaultMessages.json') + ); + const defaultMessagesIds = Object.keys( + JSON.parse(defaultMessagesBuffer.toString()) + ); + + const localeBuffer = await readFileAsync(localePath); + + const namespace = verifyJSON(localeBuffer.toString(), localePath); + + const translations = JSON5.parse(localeBuffer.toString()); + const translationsIds = Object.keys(plainify(translations)); + + const [unusedTranslations, missingTranslations] = arraysDiff( + translationsIds, + defaultMessagesIds + ); + + if (unusedTranslations.length > 0) { + errorMessage += `\nThere are unused translations in locale file ${localePath}: +${unusedTranslations.join(', ')}`; + } + + if (missingTranslations.length > 0) { + errorMessage += `\nThere are missing translations in locale file ${localePath}: +${missingTranslations.join(', ')}`; + } + + if (errorMessage) { + throw new Error(errorMessage); + } + + return namespace; +} + +export async function checkLocaleFiles(pluginsPaths) { + const pluginsMapByLocale = new Map(); + + for (const pluginPath of pluginsPaths) { + const globOptions = { + ignore: [ + './translations/defaultMessages.json', + './translations/messagesCache.json', + ], + cwd: path.resolve(pluginPath), + }; + + const localeEntries = await globAsync('./translations/*.json', globOptions); + + for (const entry of localeEntries) { + const locale = path.basename(entry); + + if (pluginsMapByLocale.has(locale)) { + pluginsMapByLocale.get(locale).push(pluginPath); + } else { + pluginsMapByLocale.set(locale, [pluginPath]); + } + } + } + + for (const locale of pluginsMapByLocale.keys()) { + const namespaces = []; + for (const pluginPath of pluginsMapByLocale.get(locale)) { + const namespace = await checkFile( + path.resolve(pluginPath, 'translations', locale) + ); + if (namespaces.includes(namespace)) { + throw new Error( + `Error in ${pluginPath} plugin ${locale} locale file\nLocale file namespace should be unique for each plugin` + ); + } + namespaces.push(namespace); + } + } +} diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js new file mode 100644 index 0000000000000..e7b3ae278b889 --- /dev/null +++ b/src/dev/i18n/utils.js @@ -0,0 +1,25 @@ +/* + * 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 fs from 'fs'; +import glob from 'glob'; +import { promisify } from 'util'; + +export const readFileAsync = promisify(fs.readFile); +export const globAsync = promisify(glob); diff --git a/src/dev/run_check_locale_files.js b/src/dev/run_check_locale_files.js new file mode 100644 index 0000000000000..91e4c021fa69a --- /dev/null +++ b/src/dev/run_check_locale_files.js @@ -0,0 +1,25 @@ +/* + * 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 { run } from './run'; +import { checkLocaleFiles } from './i18n/check_locale_files'; + +run(async () => { + await checkLocaleFiles(process.argv.slice(2)); +}); diff --git a/yarn.lock b/yarn.lock index e1bc758956954..358399cb8f0f1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10,12 +10,28 @@ esutils "^2.0.2" js-tokens "^3.0.0" +"@babel/code-frame@7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.49.tgz#becd805482734440c9d137e46d77340e64d7f51b" + dependencies: + "@babel/highlight" "7.0.0-beta.49" + "@babel/code-frame@^7.0.0-beta.35": version "7.0.0-beta.42" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.0.0-beta.42.tgz#a9c83233fa7cd06b39dc77adbb908616ff4f1962" dependencies: "@babel/highlight" "7.0.0-beta.42" +"@babel/generator@7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.0.0-beta.49.tgz#e9cffda913996accec793bbc25ab91bc19d0bf7a" + dependencies: + "@babel/types" "7.0.0-beta.49" + jsesc "^2.5.1" + lodash "^4.17.5" + source-map "^0.5.0" + trim-right "^1.0.1" + "@babel/helper-function-name@7.0.0-beta.31": version "7.0.0-beta.31" resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.31.tgz#afe63ad799209989348b1109b44feb66aa245f57" @@ -25,12 +41,32 @@ "@babel/traverse" "7.0.0-beta.31" "@babel/types" "7.0.0-beta.31" +"@babel/helper-function-name@7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/helper-function-name/-/helper-function-name-7.0.0-beta.49.tgz#a25c1119b9f035278670126e0225c03041c8de32" + dependencies: + "@babel/helper-get-function-arity" "7.0.0-beta.49" + "@babel/template" "7.0.0-beta.49" + "@babel/types" "7.0.0-beta.49" + "@babel/helper-get-function-arity@7.0.0-beta.31": version "7.0.0-beta.31" resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.31.tgz#1176d79252741218e0aec872ada07efb2b37a493" dependencies: "@babel/types" "7.0.0-beta.31" +"@babel/helper-get-function-arity@7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/helper-get-function-arity/-/helper-get-function-arity-7.0.0-beta.49.tgz#cf5023f32d2ad92d087374939cec0951bcb51441" + dependencies: + "@babel/types" "7.0.0-beta.49" + +"@babel/helper-split-export-declaration@7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.0.0-beta.49.tgz#40d78eda0968d011b1c52866e5746cfb23e57548" + dependencies: + "@babel/types" "7.0.0-beta.49" + "@babel/highlight@7.0.0-beta.42": version "7.0.0-beta.42" resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.42.tgz#a502a1c0d6f99b2b0e81d468a1b0c0e81e3f3623" @@ -39,6 +75,18 @@ esutils "^2.0.2" js-tokens "^3.0.0" +"@babel/highlight@7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/highlight/-/highlight-7.0.0-beta.49.tgz#96bdc6b43e13482012ba6691b1018492d39622cc" + dependencies: + chalk "^2.0.0" + esutils "^2.0.2" + js-tokens "^3.0.0" + +"@babel/parser@7.0.0-beta.49", "@babel/parser@^7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.0.0-beta.49.tgz#944d0c5ba2812bb159edbd226743afd265179bdc" + "@babel/template@7.0.0-beta.31": version "7.0.0-beta.31" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.31.tgz#577bb29389f6c497c3e7d014617e7d6713f68bda" @@ -48,6 +96,15 @@ babylon "7.0.0-beta.31" lodash "^4.2.0" +"@babel/template@7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.49.tgz#e38abe8217cb9793f461a5306d7ad745d83e1d27" + dependencies: + "@babel/code-frame" "7.0.0-beta.49" + "@babel/parser" "7.0.0-beta.49" + "@babel/types" "7.0.0-beta.49" + lodash "^4.17.5" + "@babel/traverse@7.0.0-beta.31": version "7.0.0-beta.31" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.31.tgz#db399499ad74aefda014f0c10321ab255134b1df" @@ -61,6 +118,21 @@ invariant "^2.2.0" lodash "^4.2.0" +"@babel/traverse@^7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.0.0-beta.49.tgz#4f2a73682a18334ed6625d100a8d27319f7c2d68" + dependencies: + "@babel/code-frame" "7.0.0-beta.49" + "@babel/generator" "7.0.0-beta.49" + "@babel/helper-function-name" "7.0.0-beta.49" + "@babel/helper-split-export-declaration" "7.0.0-beta.49" + "@babel/parser" "7.0.0-beta.49" + "@babel/types" "7.0.0-beta.49" + debug "^3.1.0" + globals "^11.1.0" + invariant "^2.2.0" + lodash "^4.17.5" + "@babel/types@7.0.0-beta.31": version "7.0.0-beta.31" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.31.tgz#42c9c86784f674c173fb21882ca9643334029de4" @@ -69,6 +141,14 @@ lodash "^4.2.0" to-fast-properties "^2.0.0" +"@babel/types@7.0.0-beta.49", "@babel/types@^7.0.0-beta.49": + version "7.0.0-beta.49" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.0.0-beta.49.tgz#b7e3b1c3f4d4cfe11bdf8c89f1efd5e1617b87a6" + dependencies: + esutils "^2.0.2" + lodash "^4.17.5" + to-fast-properties "^2.0.0" + "@elastic/eslint-config-kibana@link:packages/eslint-config-kibana": version "0.0.0" uid "" @@ -5447,6 +5527,10 @@ globals@^11.0.1: version "11.3.0" resolved "https://registry.yarnpkg.com/globals/-/globals-11.3.0.tgz#e04fdb7b9796d8adac9c8f64c14837b2313378b0" +globals@^11.1.0: + version "11.5.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.5.0.tgz#6bc840de6771173b191f13d3a9c94d441ee92642" + globals@^9.18.0: version "9.18.0" resolved "https://registry.yarnpkg.com/globals/-/globals-9.18.0.tgz#aa3896b3e69b487f17e31ed2143d69a8e30c2d8a" @@ -7413,6 +7497,10 @@ jsesc@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-1.3.0.tgz#46c3fec8c1892b12b0833db9bc7622176dbab34b" +jsesc@^2.5.1: + version "2.5.1" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.1.tgz#e421a2a8e20d6b0819df28908f782526b96dd1fe" + jsesc@~0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" From e4ae11fa4a5dccfffabc66925b9e2dbd03a01f55 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Thu, 14 Jun 2018 13:02:34 +0300 Subject: [PATCH 02/24] Refactor locale files verification tool --- src/dev/i18n/check_locale_files.js | 102 +---------------------------- src/dev/i18n/utils.js | 22 +++++++ src/dev/i18n/verify_locale_json.js | 96 +++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 100 deletions(-) create mode 100644 src/dev/i18n/verify_locale_json.js diff --git a/src/dev/i18n/check_locale_files.js b/src/dev/i18n/check_locale_files.js index 33a0118d81e66..401665b5f59c6 100644 --- a/src/dev/i18n/check_locale_files.js +++ b/src/dev/i18n/check_locale_files.js @@ -19,107 +19,9 @@ import path from 'path'; import JSON5 from 'json5'; -import { parse } from '@babel/parser'; -import traverse from '@babel/traverse'; -import { - isIdentifier, - isObjectExpression, - isStringLiteral, -} from '@babel/types'; - -import { globAsync, readFileAsync } from './utils'; - -function plainify(object) { - const result = {}; - - for (const [key, value] of Object.entries(object)) { - if (typeof value === 'object' && value !== null) { - for (const [nestedKey, nestedValue] of Object.entries(plainify(value))) { - result[`${key}.${nestedKey}`] = nestedValue; - } - } else { - result[key] = value; - } - } - - return result; -} - -function arraysDiff(left = [], right = []) { - const leftDiff = left.filter(value => right.includes(value)); - const rightDiff = right.filter(value => left.includes(value)); - return [leftDiff, rightDiff]; -} -function readKeySubTree(node) { - if (isStringLiteral(node)) { - return node.value; - } - if (isIdentifier(node)) { - return node.name; - } -} - -function getDuplicates(node, parentPath) { - const keys = []; - const duplicates = []; - - for (const property of node.properties) { - const key = readKeySubTree(property.key); - const nodePath = `${parentPath}.${key}`; - - if (!duplicates.includes(nodePath)) { - if (!keys.includes(nodePath)) { - keys.push(nodePath); - } else { - duplicates.push(nodePath); - } - } - - if (isObjectExpression(property.value)) { - duplicates.push(...getDuplicates(property.value, `${parentPath}.${key}`)); - } - } - - return duplicates; -} - -function verifyJSON(json, fileName) { - const jsonAST = parse(`+${json}`); - let namespace = ''; - - traverse(jsonAST, { - enter(path) { - if (isObjectExpression(path.node)) { - if (path.node.properties.length !== 1) { - throw new Error( - `Locale file ${fileName} should be a JSON with a single-key object` - ); - } - if (!isObjectExpression(path.node.properties[0].value)) { - throw new Error(`Invalid locale file: ${fileName}`); - } - - namespace = readKeySubTree(path.node.properties[0].key); - const duplicates = getDuplicates( - path.node.properties[0].value, - namespace - ); - - if (duplicates.length !== 0) { - throw new Error( - `There are translation id duplicates in locale file ${fileName}: -${duplicates.join(', ')}` - ); - } - - path.stop(); - } - }, - }); - - return namespace; -} +import { arraysDiff, globAsync, plainify, readFileAsync } from './utils'; +import { verifyJSON } from './verify_locale_json'; async function checkFile(localePath) { let errorMessage = ''; diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index e7b3ae278b889..98ec8429bb28a 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -23,3 +23,25 @@ import { promisify } from 'util'; export const readFileAsync = promisify(fs.readFile); export const globAsync = promisify(glob); + +export function plainify(object) { + const result = {}; + + for (const [key, value] of Object.entries(object)) { + if (typeof value === 'object' && value !== null) { + for (const [nestedKey, nestedValue] of Object.entries(plainify(value))) { + result[`${key}.${nestedKey}`] = nestedValue; + } + } else { + result[key] = value; + } + } + + return result; +} + +export function arraysDiff(left = [], right = []) { + const leftDiff = left.filter(value => right.includes(value)); + const rightDiff = right.filter(value => left.includes(value)); + return [leftDiff, rightDiff]; +} diff --git a/src/dev/i18n/verify_locale_json.js b/src/dev/i18n/verify_locale_json.js new file mode 100644 index 0000000000000..b22a046e6e00c --- /dev/null +++ b/src/dev/i18n/verify_locale_json.js @@ -0,0 +1,96 @@ +/* + * 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 { parse } from '@babel/parser'; +import traverse from '@babel/traverse'; +import { + isIdentifier, + isObjectExpression, + isStringLiteral, +} from '@babel/types'; + +function readKeySubTree(node) { + if (isStringLiteral(node)) { + return node.value; + } + if (isIdentifier(node)) { + return node.name; + } +} + +function getDuplicates(node, parentPath) { + const keys = []; + const duplicates = []; + + for (const property of node.properties) { + const key = readKeySubTree(property.key); + const nodePath = `${parentPath}.${key}`; + + if (!duplicates.includes(nodePath)) { + if (!keys.includes(nodePath)) { + keys.push(nodePath); + } else { + duplicates.push(nodePath); + } + } + + if (isObjectExpression(property.value)) { + duplicates.push(...getDuplicates(property.value, `${parentPath}.${key}`)); + } + } + + return duplicates; +} + +export function verifyJSON(json, fileName) { + const jsonAST = parse(`+${json}`); + let namespace = ''; + + traverse(jsonAST, { + enter(path) { + if (isObjectExpression(path.node)) { + if (path.node.properties.length !== 1) { + throw new Error( + `Locale file ${fileName} should be a JSON with a single-key object` + ); + } + if (!isObjectExpression(path.node.properties[0].value)) { + throw new Error(`Invalid locale file: ${fileName}`); + } + + namespace = readKeySubTree(path.node.properties[0].key); + const duplicates = getDuplicates( + path.node.properties[0].value, + namespace + ); + + if (duplicates.length !== 0) { + throw new Error( + `There are translation id duplicates in locale file ${fileName}: +${duplicates.join(', ')}` + ); + } + + path.stop(); + } + }, + }); + + return namespace; +} From 7b0eafc3c7098ed55b95907069172ec7773d3ba5 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Thu, 14 Jun 2018 13:07:54 +0300 Subject: [PATCH 03/24] Fix default messages structure --- src/dev/i18n/check_locale_files.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dev/i18n/check_locale_files.js b/src/dev/i18n/check_locale_files.js index 401665b5f59c6..5616751e592ee 100644 --- a/src/dev/i18n/check_locale_files.js +++ b/src/dev/i18n/check_locale_files.js @@ -30,7 +30,7 @@ async function checkFile(localePath) { path.resolve(path.dirname(localePath), 'defaultMessages.json') ); const defaultMessagesIds = Object.keys( - JSON.parse(defaultMessagesBuffer.toString()) + plainify(JSON.parse(defaultMessagesBuffer.toString())) ); const localeBuffer = await readFileAsync(localePath); From d17eb99d9a667b1a3030462670f1698ab29a04fc Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Tue, 26 Jun 2018 16:50:09 +0300 Subject: [PATCH 04/24] Return to plain structured JSON files --- src/dev/i18n/check_locale_files.js | 6 +++--- src/dev/i18n/utils.js | 16 ---------------- 2 files changed, 3 insertions(+), 19 deletions(-) diff --git a/src/dev/i18n/check_locale_files.js b/src/dev/i18n/check_locale_files.js index 5616751e592ee..52c2eab8dba36 100644 --- a/src/dev/i18n/check_locale_files.js +++ b/src/dev/i18n/check_locale_files.js @@ -20,7 +20,7 @@ import path from 'path'; import JSON5 from 'json5'; -import { arraysDiff, globAsync, plainify, readFileAsync } from './utils'; +import { arraysDiff, globAsync, readFileAsync } from './utils'; import { verifyJSON } from './verify_locale_json'; async function checkFile(localePath) { @@ -30,7 +30,7 @@ async function checkFile(localePath) { path.resolve(path.dirname(localePath), 'defaultMessages.json') ); const defaultMessagesIds = Object.keys( - plainify(JSON.parse(defaultMessagesBuffer.toString())) + JSON.parse(defaultMessagesBuffer.toString()) ); const localeBuffer = await readFileAsync(localePath); @@ -38,7 +38,7 @@ async function checkFile(localePath) { const namespace = verifyJSON(localeBuffer.toString(), localePath); const translations = JSON5.parse(localeBuffer.toString()); - const translationsIds = Object.keys(plainify(translations)); + const translationsIds = Object.keys(translations); const [unusedTranslations, missingTranslations] = arraysDiff( translationsIds, diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 98ec8429bb28a..30d5e5b4753ff 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -24,22 +24,6 @@ import { promisify } from 'util'; export const readFileAsync = promisify(fs.readFile); export const globAsync = promisify(glob); -export function plainify(object) { - const result = {}; - - for (const [key, value] of Object.entries(object)) { - if (typeof value === 'object' && value !== null) { - for (const [nestedKey, nestedValue] of Object.entries(plainify(value))) { - result[`${key}.${nestedKey}`] = nestedValue; - } - } else { - result[key] = value; - } - } - - return result; -} - export function arraysDiff(left = [], right = []) { const leftDiff = left.filter(value => right.includes(value)); const rightDiff = right.filter(value => left.includes(value)); From 9a34c13318c61f2ee6060ddd86fdc1c05ce3e279 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Mon, 9 Jul 2018 15:20:02 +0300 Subject: [PATCH 05/24] Update locale files checking tool --- package.json | 1 + src/dev/i18n/check_locale_files.js | 20 ++++--- src/dev/i18n/utils.js | 42 +++++++++++++- src/dev/i18n/verify_locale_json.js | 89 ++++++++++-------------------- 4 files changed, 80 insertions(+), 72 deletions(-) diff --git a/package.json b/package.json index edeb1484aadb9..917a493ae9063 100644 --- a/package.json +++ b/package.json @@ -293,6 +293,7 @@ "jest-raw-loader": "^1.0.1", "jimp": "0.2.28", "jsdom": "9.9.1", + "json5": "^1.0.1", "karma": "1.7.0", "karma-chrome-launcher": "2.1.1", "karma-coverage": "1.1.1", diff --git a/src/dev/i18n/check_locale_files.js b/src/dev/i18n/check_locale_files.js index 52c2eab8dba36..3189d6bd2bd49 100644 --- a/src/dev/i18n/check_locale_files.js +++ b/src/dev/i18n/check_locale_files.js @@ -27,15 +27,20 @@ async function checkFile(localePath) { let errorMessage = ''; const defaultMessagesBuffer = await readFileAsync( - path.resolve(path.dirname(localePath), 'defaultMessages.json') + path.resolve(path.dirname(localePath), 'en.json') ); const defaultMessagesIds = Object.keys( - JSON.parse(defaultMessagesBuffer.toString()) + JSON5.parse(defaultMessagesBuffer.toString()) ); const localeBuffer = await readFileAsync(localePath); - const namespace = verifyJSON(localeBuffer.toString(), localePath); + let namespace; + try { + namespace = verifyJSON(localeBuffer.toString(), localePath); + } catch (error) { + throw new Error(`Error in ${localePath}\n${error.message || error}`); + } const translations = JSON5.parse(localeBuffer.toString()); const translationsIds = Object.keys(translations); @@ -46,12 +51,12 @@ async function checkFile(localePath) { ); if (unusedTranslations.length > 0) { - errorMessage += `\nThere are unused translations in locale file ${localePath}: + errorMessage += `\nUnused translations in locale file ${localePath}: ${unusedTranslations.join(', ')}`; } if (missingTranslations.length > 0) { - errorMessage += `\nThere are missing translations in locale file ${localePath}: + errorMessage += `\nMissing translations in locale file ${localePath}: ${missingTranslations.join(', ')}`; } @@ -67,10 +72,7 @@ export async function checkLocaleFiles(pluginsPaths) { for (const pluginPath of pluginsPaths) { const globOptions = { - ignore: [ - './translations/defaultMessages.json', - './translations/messagesCache.json', - ], + ignore: ['./translations/en.json', './translations/messagesCache.json'], cwd: path.resolve(pluginPath), }; diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 30d5e5b4753ff..3aa06c9ec82a3 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -20,12 +20,50 @@ import fs from 'fs'; import glob from 'glob'; import { promisify } from 'util'; +import { isNode } from '@babel/types'; export const readFileAsync = promisify(fs.readFile); export const globAsync = promisify(glob); export function arraysDiff(left = [], right = []) { - const leftDiff = left.filter(value => right.includes(value)); - const rightDiff = right.filter(value => left.includes(value)); + const leftDiff = left.filter(value => !right.includes(value)); + const rightDiff = right.filter(value => !left.includes(value)); return [leftDiff, rightDiff]; } + +/** + * Workaround of @babel/traverse typescript bug: https://github.com/babel/babel/issues/8262 + */ +export function* traverseNodes(nodes, extractMessagesFromNode) { + for (const node of nodes) { + let stop = false; + let message; + + if (isNode(node)) { + message = extractMessagesFromNode({ + node, + stop() { + stop = true; + }, + }); + } + + if (message) { + yield message; + } + + if (stop) { + break; + } + + if (node && typeof node === 'object') { + const values = Object.values(node).filter( + value => value && typeof value === 'object' + ); + + if (values.length > 0) { + yield* traverseNodes(values, extractMessagesFromNode); + } + } + } +} diff --git a/src/dev/i18n/verify_locale_json.js b/src/dev/i18n/verify_locale_json.js index b22a046e6e00c..020c5aa713acd 100644 --- a/src/dev/i18n/verify_locale_json.js +++ b/src/dev/i18n/verify_locale_json.js @@ -18,79 +18,46 @@ */ import { parse } from '@babel/parser'; -import traverse from '@babel/traverse'; -import { - isIdentifier, - isObjectExpression, - isStringLiteral, -} from '@babel/types'; +import { isObjectExpression } from '@babel/types'; -function readKeySubTree(node) { - if (isStringLiteral(node)) { - return node.value; - } - if (isIdentifier(node)) { - return node.name; - } -} - -function getDuplicates(node, parentPath) { - const keys = []; - const duplicates = []; +import { traverseNodes } from './utils'; - for (const property of node.properties) { - const key = readKeySubTree(property.key); - const nodePath = `${parentPath}.${key}`; +export function verifyJSON(json) { + const jsonAST = parse(`+${json}`); + let namespace = ''; - if (!duplicates.includes(nodePath)) { - if (!keys.includes(nodePath)) { - keys.push(nodePath); - } else { - duplicates.push(nodePath); - } + traverseNodes(jsonAST.program.body, ({ node, stop }) => { + if (!isObjectExpression(node)) { + return; } - if (isObjectExpression(property.value)) { - duplicates.push(...getDuplicates(property.value, `${parentPath}.${key}`)); + if (!node.properties.some(prop => prop.key.name === 'formats')) { + throw 'Locale file should contain formats object.'; } - } - - return duplicates; -} - -export function verifyJSON(json, fileName) { - const jsonAST = parse(`+${json}`); - let namespace = ''; - traverse(jsonAST, { - enter(path) { - if (isObjectExpression(path.node)) { - if (path.node.properties.length !== 1) { - throw new Error( - `Locale file ${fileName} should be a JSON with a single-key object` - ); + for (const property of node.properties) { + if (property.key.name !== 'formats') { + const messageNamespace = property.key.value.split('.')[0]; + if (!namespace) { + namespace = messageNamespace; } - if (!isObjectExpression(path.node.properties[0].value)) { - throw new Error(`Invalid locale file: ${fileName}`); - } - - namespace = readKeySubTree(path.node.properties[0].key); - const duplicates = getDuplicates( - path.node.properties[0].value, - namespace - ); - if (duplicates.length !== 0) { - throw new Error( - `There are translation id duplicates in locale file ${fileName}: -${duplicates.join(', ')}` - ); + if (namespace !== messageNamespace) { + throw 'All messages should start with the same namespace.'; } + } + } - path.stop(); + const idsSet = new Set(); + for (const id of node.properties.map(prop => prop.key.value)) { + if (idsSet.has(id)) { + throw `Ids duplicate: ${id}`; } - }, - }); + idsSet.add(id); + } + + stop(); + }).next(); return namespace; } From 24314b100bd847ba3851c4af9862ee4d3740234d Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Fri, 20 Jul 2018 11:48:16 +0300 Subject: [PATCH 06/24] Add unit tests --- .../test_plugin_1/translations/en.json | 60 +++++++++++++++ .../test_plugin_1/translations/valid.json | 60 +++++++++++++++ .../test_plugin_2/translations/en.json | 60 +++++++++++++++ .../test_plugin_2/translations/valid.json | 60 +++++++++++++++ .../test_plugin_3/translations/en.json | 60 +++++++++++++++ .../test_plugin_3/translations/missing.json | 59 +++++++++++++++ .../test_plugin_3/translations/unused.json | 61 +++++++++++++++ .../test_plugin_4/translations/en.json | 60 +++++++++++++++ .../test_plugin_4/translations/valid.json | 60 +++++++++++++++ .../test_plugin_5/translations/en.json | 60 +++++++++++++++ .../test_plugin_5/translations/valid.json | 60 +++++++++++++++ src/dev/i18n/check_locale_files.js | 15 +--- src/dev/i18n/check_locale_files.test.js | 74 +++++++++++++++++++ src/dev/i18n/extract_default_translations.js | 6 +- src/dev/i18n/verify_locale_json.js | 12 +-- 15 files changed, 748 insertions(+), 19 deletions(-) create mode 100644 src/dev/i18n/__fixtures__/test_plugin_1/translations/en.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_1/translations/valid.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_2/translations/en.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_2/translations/valid.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_3/translations/en.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_3/translations/missing.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_3/translations/unused.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_4/translations/en.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_4/translations/valid.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_5/translations/en.json create mode 100644 src/dev/i18n/__fixtures__/test_plugin_5/translations/valid.json create mode 100644 src/dev/i18n/check_locale_files.test.js diff --git a/src/dev/i18n/__fixtures__/test_plugin_1/translations/en.json b/src/dev/i18n/__fixtures__/test_plugin_1/translations/en.json new file mode 100644 index 0000000000000..1bd44013a5c88 --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_1/translations/en.json @@ -0,0 +1,60 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'test_plugin_1.id_1': 'Message text 1', + 'test_plugin_1.id_2': 'Message text 2', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_1/translations/valid.json b/src/dev/i18n/__fixtures__/test_plugin_1/translations/valid.json new file mode 100644 index 0000000000000..a8bc7b7bff344 --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_1/translations/valid.json @@ -0,0 +1,60 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'test_plugin_1.id_1': 'Translated text 1', + 'test_plugin_1.id_2': 'Translated text 2', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_2/translations/en.json b/src/dev/i18n/__fixtures__/test_plugin_2/translations/en.json new file mode 100644 index 0000000000000..6c6130c91bd99 --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_2/translations/en.json @@ -0,0 +1,60 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'test_plugin_2.id_1': 'Message text 1', + 'test_plugin_2.id_2': 'Message text 2', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_2/translations/valid.json b/src/dev/i18n/__fixtures__/test_plugin_2/translations/valid.json new file mode 100644 index 0000000000000..1110bbcd2ae5c --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_2/translations/valid.json @@ -0,0 +1,60 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'test_plugin_2.id_1': 'Translated text 1', + 'test_plugin_2.id_2': 'Translated text 2', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_3/translations/en.json b/src/dev/i18n/__fixtures__/test_plugin_3/translations/en.json new file mode 100644 index 0000000000000..f42068bd5d392 --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_3/translations/en.json @@ -0,0 +1,60 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'test_plugin_3.id_1': 'Message text 1', + 'test_plugin_3.id_2': 'Message text 2', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_3/translations/missing.json b/src/dev/i18n/__fixtures__/test_plugin_3/translations/missing.json new file mode 100644 index 0000000000000..3da67f939a03d --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_3/translations/missing.json @@ -0,0 +1,59 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'test_plugin_3.id_1': 'Message text 1', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_3/translations/unused.json b/src/dev/i18n/__fixtures__/test_plugin_3/translations/unused.json new file mode 100644 index 0000000000000..fa67268fdc6e9 --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_3/translations/unused.json @@ -0,0 +1,61 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'test_plugin_3.id_1': 'Message text 1', + 'test_plugin_3.id_2': 'Message text 2', + 'test_plugin_3.id_3': 'Message text 3', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_4/translations/en.json b/src/dev/i18n/__fixtures__/test_plugin_4/translations/en.json new file mode 100644 index 0000000000000..306db3232d0d9 --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_4/translations/en.json @@ -0,0 +1,60 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'duplicated_namespace.id_1': 'Message text 1', + 'duplicated_namespace.id_2': 'Message text 2', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_4/translations/valid.json b/src/dev/i18n/__fixtures__/test_plugin_4/translations/valid.json new file mode 100644 index 0000000000000..90d7752222365 --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_4/translations/valid.json @@ -0,0 +1,60 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'duplicated_namespace.id_1': 'Translated text 1', + 'duplicated_namespace.id_2': 'Translated text 2', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_5/translations/en.json b/src/dev/i18n/__fixtures__/test_plugin_5/translations/en.json new file mode 100644 index 0000000000000..31b87e59109cf --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_5/translations/en.json @@ -0,0 +1,60 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'duplicated_namespace.id_3': 'Message text 3', + 'duplicated_namespace.id_4': 'Message text 4', +} diff --git a/src/dev/i18n/__fixtures__/test_plugin_5/translations/valid.json b/src/dev/i18n/__fixtures__/test_plugin_5/translations/valid.json new file mode 100644 index 0000000000000..7fffdf03efe22 --- /dev/null +++ b/src/dev/i18n/__fixtures__/test_plugin_5/translations/valid.json @@ -0,0 +1,60 @@ +{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'duplicated_namespace.id_3': 'Translated text 3', + 'duplicated_namespace.id_4': 'Translated text 4', +} diff --git a/src/dev/i18n/check_locale_files.js b/src/dev/i18n/check_locale_files.js index 3189d6bd2bd49..1417334d7e4a0 100644 --- a/src/dev/i18n/check_locale_files.js +++ b/src/dev/i18n/check_locale_files.js @@ -23,15 +23,13 @@ import JSON5 from 'json5'; import { arraysDiff, globAsync, readFileAsync } from './utils'; import { verifyJSON } from './verify_locale_json'; -async function checkFile(localePath) { +export async function checkFile(localePath) { let errorMessage = ''; const defaultMessagesBuffer = await readFileAsync( path.resolve(path.dirname(localePath), 'en.json') ); - const defaultMessagesIds = Object.keys( - JSON5.parse(defaultMessagesBuffer.toString()) - ); + const defaultMessagesIds = Object.keys(JSON5.parse(defaultMessagesBuffer.toString())); const localeBuffer = await readFileAsync(localePath); @@ -45,10 +43,7 @@ async function checkFile(localePath) { const translations = JSON5.parse(localeBuffer.toString()); const translationsIds = Object.keys(translations); - const [unusedTranslations, missingTranslations] = arraysDiff( - translationsIds, - defaultMessagesIds - ); + const [unusedTranslations, missingTranslations] = arraysDiff(translationsIds, defaultMessagesIds); if (unusedTranslations.length > 0) { errorMessage += `\nUnused translations in locale file ${localePath}: @@ -92,9 +87,7 @@ export async function checkLocaleFiles(pluginsPaths) { for (const locale of pluginsMapByLocale.keys()) { const namespaces = []; for (const pluginPath of pluginsMapByLocale.get(locale)) { - const namespace = await checkFile( - path.resolve(pluginPath, 'translations', locale) - ); + const namespace = await checkFile(path.resolve(pluginPath, 'translations', locale)); if (namespaces.includes(namespace)) { throw new Error( `Error in ${pluginPath} plugin ${locale} locale file\nLocale file namespace should be unique for each plugin` diff --git a/src/dev/i18n/check_locale_files.test.js b/src/dev/i18n/check_locale_files.test.js new file mode 100644 index 0000000000000..145977b962c09 --- /dev/null +++ b/src/dev/i18n/check_locale_files.test.js @@ -0,0 +1,74 @@ +/* + * 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 { resolve, join } from 'path'; + +import { checkFile, checkLocaleFiles } from './check_locale_files'; + +const pluginsPaths = [ + resolve(__dirname, '__fixtures__', 'test_plugin_1'), + resolve(__dirname, '__fixtures__', 'test_plugin_2'), + resolve(__dirname, '__fixtures__', 'test_plugin_3'), + resolve(__dirname, '__fixtures__', 'test_plugin_4'), + resolve(__dirname, '__fixtures__', 'test_plugin_5'), +]; + +describe('dev/i18n/check_locale_files', () => { + describe('checkFile', () => { + it('returns namespace of a valid JSON file', async () => { + const localePath1 = join(pluginsPaths[0], 'translations', 'valid.json'); + const localePath2 = join(pluginsPaths[1], 'translations', 'valid.json'); + + expect(await checkFile(localePath1)).toBe('test_plugin_1'); + expect(await checkFile(localePath2)).toBe('test_plugin_2'); + }); + + it('throws an error for unused id and missing id', async () => { + const localeWithMissingMessage = join(pluginsPaths[2], 'translations', 'missing.json'); + const localeWithUnusedMessage = join(pluginsPaths[2], 'translations', 'unused.json'); + + expect(checkFile(localeWithMissingMessage)).rejects.toEqual( + new Error( + `\nMissing translations in locale file ${localeWithMissingMessage}:\ntest_plugin_3.id_2` + ) + ); + + expect(checkFile(localeWithUnusedMessage)).rejects.toEqual( + new Error( + `\nUnused translations in locale file ${localeWithUnusedMessage}:\ntest_plugin_3.id_3` + ) + ); + }); + }); + + describe('checkLocaleFiles', () => { + it('validates locale files in multiple plugins', async () => { + expect(await checkLocaleFiles([pluginsPaths[0], pluginsPaths[1]])).toBe(undefined); + }); + + it('throws an error for namespaces collision', async () => { + expect(checkLocaleFiles([pluginsPaths[3], pluginsPaths[4]])).rejects.toEqual( + new Error( + `Error in ${pluginsPaths[4]} plugin valid.json locale file +Locale file namespace should be unique for each plugin` + ) + ); + }); + }); +}); diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index d63c7b88092dd..57059ce2e400b 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -18,7 +18,7 @@ */ import { resolve } from 'path'; -import { formats } from '@kbn/i18n'; +import { i18n } from '@kbn/i18n'; import JSON5 from 'json5'; import { extractHtmlMessages } from './extract_html_messages'; @@ -93,7 +93,9 @@ export async function extractDefaultTranslations(inputPath) { ); // .slice(0, -1): remove closing curly brace from json to append messages - let jsonBuffer = Buffer.from(JSON5.stringify({ formats }, { quote: `'`, space: 2 }).slice(0, -1)); + let jsonBuffer = Buffer.from( + JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) + ); const defaultMessages = [...defaultMessagesMap].sort(([key1], [key2]) => { return key1 < key2 ? -1 : 1; diff --git a/src/dev/i18n/verify_locale_json.js b/src/dev/i18n/verify_locale_json.js index 020c5aa713acd..a008b0c005a70 100644 --- a/src/dev/i18n/verify_locale_json.js +++ b/src/dev/i18n/verify_locale_json.js @@ -26,9 +26,9 @@ export function verifyJSON(json) { const jsonAST = parse(`+${json}`); let namespace = ''; - traverseNodes(jsonAST.program.body, ({ node, stop }) => { + for (const node of traverseNodes(jsonAST.program.body)) { if (!isObjectExpression(node)) { - return; + continue; } if (!node.properties.some(prop => prop.key.name === 'formats')) { @@ -43,7 +43,7 @@ export function verifyJSON(json) { } if (namespace !== messageNamespace) { - throw 'All messages should start with the same namespace.'; + throw 'All messages ids should start with the same namespace.'; } } } @@ -51,13 +51,13 @@ export function verifyJSON(json) { const idsSet = new Set(); for (const id of node.properties.map(prop => prop.key.value)) { if (idsSet.has(id)) { - throw `Ids duplicate: ${id}`; + throw `Ids collision: ${id}`; } idsSet.add(id); } - stop(); - }).next(); + break; + } return namespace; } From 7ba5de5a370be084d8aaadb71efcb9983dec2ce2 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Fri, 20 Jul 2018 15:22:42 +0300 Subject: [PATCH 07/24] Fix tests --- .../test_plugin_1/translations/en.json | 0 .../test_plugin_1/translations/valid.json | 0 .../test_plugin_2/translations/en.json | 0 .../test_plugin_2/translations/valid.json | 0 .../test_plugin_3/translations/en.json | 0 .../test_plugin_3/translations/missing.json | 0 .../test_plugin_3/translations/unused.json | 0 .../test_plugin_4/translations/en.json | 0 .../test_plugin_4/translations/valid.json | 0 .../test_plugin_5/translations/en.json | 0 .../test_plugin_5/translations/valid.json | 0 .../test_plugin/test_file.html | 0 src/dev/i18n/check_locale_files.test.js | 22 ++++++++++--------- .../i18n/extract_default_translations.test.js | 15 ++++++++----- 14 files changed, 22 insertions(+), 15 deletions(-) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_1/translations/en.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_1/translations/valid.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_2/translations/en.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_2/translations/valid.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_3/translations/en.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_3/translations/missing.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_3/translations/unused.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_4/translations/en.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_4/translations/valid.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_5/translations/en.json (100%) rename src/dev/i18n/__fixtures__/{ => check_locale_files}/test_plugin_5/translations/valid.json (100%) rename src/dev/i18n/__fixtures__/{ => extract_default_translations}/test_plugin/test_file.html (100%) diff --git a/src/dev/i18n/__fixtures__/test_plugin_1/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/en.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_1/translations/en.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/en.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_1/translations/valid.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/valid.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_1/translations/valid.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/valid.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_2/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/en.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_2/translations/en.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/en.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_2/translations/valid.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/valid.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_2/translations/valid.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/valid.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_3/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/en.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_3/translations/en.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/en.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_3/translations/missing.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/missing.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_3/translations/missing.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/missing.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_3/translations/unused.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/unused.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_3/translations/unused.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/unused.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_4/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/en.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_4/translations/en.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/en.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_4/translations/valid.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/valid.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_4/translations/valid.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/valid.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_5/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/en.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_5/translations/en.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/en.json diff --git a/src/dev/i18n/__fixtures__/test_plugin_5/translations/valid.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/valid.json similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin_5/translations/valid.json rename to src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/valid.json diff --git a/src/dev/i18n/__fixtures__/test_plugin/test_file.html b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin/test_file.html similarity index 100% rename from src/dev/i18n/__fixtures__/test_plugin/test_file.html rename to src/dev/i18n/__fixtures__/extract_default_translations/test_plugin/test_file.html diff --git a/src/dev/i18n/check_locale_files.test.js b/src/dev/i18n/check_locale_files.test.js index 145977b962c09..f351f4b0ea474 100644 --- a/src/dev/i18n/check_locale_files.test.js +++ b/src/dev/i18n/check_locale_files.test.js @@ -17,31 +17,33 @@ * under the License. */ -import { resolve, join } from 'path'; +import path from 'path'; import { checkFile, checkLocaleFiles } from './check_locale_files'; +const testsFixturesRoot = path.resolve(__dirname, '__fixtures__', 'check_locale_files'); + const pluginsPaths = [ - resolve(__dirname, '__fixtures__', 'test_plugin_1'), - resolve(__dirname, '__fixtures__', 'test_plugin_2'), - resolve(__dirname, '__fixtures__', 'test_plugin_3'), - resolve(__dirname, '__fixtures__', 'test_plugin_4'), - resolve(__dirname, '__fixtures__', 'test_plugin_5'), + path.join(testsFixturesRoot, 'test_plugin_1'), + path.join(testsFixturesRoot, 'test_plugin_2'), + path.join(testsFixturesRoot, 'test_plugin_3'), + path.join(testsFixturesRoot, 'test_plugin_4'), + path.join(testsFixturesRoot, 'test_plugin_5'), ]; describe('dev/i18n/check_locale_files', () => { describe('checkFile', () => { it('returns namespace of a valid JSON file', async () => { - const localePath1 = join(pluginsPaths[0], 'translations', 'valid.json'); - const localePath2 = join(pluginsPaths[1], 'translations', 'valid.json'); + const localePath1 = path.join(pluginsPaths[0], 'translations', 'valid.json'); + const localePath2 = path.join(pluginsPaths[1], 'translations', 'valid.json'); expect(await checkFile(localePath1)).toBe('test_plugin_1'); expect(await checkFile(localePath2)).toBe('test_plugin_2'); }); it('throws an error for unused id and missing id', async () => { - const localeWithMissingMessage = join(pluginsPaths[2], 'translations', 'missing.json'); - const localeWithUnusedMessage = join(pluginsPaths[2], 'translations', 'unused.json'); + const localeWithMissingMessage = path.join(pluginsPaths[2], 'translations', 'missing.json'); + const localeWithUnusedMessage = path.join(pluginsPaths[2], 'translations', 'unused.json'); expect(checkFile(localeWithMissingMessage)).rejects.toEqual( new Error( diff --git a/src/dev/i18n/extract_default_translations.test.js b/src/dev/i18n/extract_default_translations.test.js index eb78987e5e75d..d01e8c6a8bb04 100644 --- a/src/dev/i18n/extract_default_translations.test.js +++ b/src/dev/i18n/extract_default_translations.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { resolve } from 'path'; +import path from 'path'; import fs from 'fs'; import { promisify } from 'util'; @@ -27,18 +27,23 @@ const readFileAsync = promisify(fs.readFile); const removeDirAsync = promisify(fs.rmdir); const unlinkAsync = promisify(fs.unlink); -const PLUGIN_PATH = resolve(__dirname, '__fixtures__', 'test_plugin'); +const PLUGIN_PATH = path.resolve( + __dirname, + '__fixtures__', + 'extract_default_translations', + 'test_plugin' +); describe('dev/i18n/extract_default_translations', () => { it('injects default formats into en.json', async () => { await extractDefaultTranslations(PLUGIN_PATH); const extractedJSONBuffer = await readFileAsync( - resolve(PLUGIN_PATH, 'translations', 'en.json') + path.resolve(PLUGIN_PATH, 'translations', 'en.json') ); - await unlinkAsync(resolve(PLUGIN_PATH, 'translations', 'en.json')); - await removeDirAsync(resolve(PLUGIN_PATH, 'translations')); + await unlinkAsync(path.resolve(PLUGIN_PATH, 'translations', 'en.json')); + await removeDirAsync(path.resolve(PLUGIN_PATH, 'translations')); expect(extractedJSONBuffer.toString()).toMatchSnapshot(); }); From 4b720176265ddbe7f6e7d66a11f0de0ae66223cb Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Thu, 26 Jul 2018 15:16:11 +0300 Subject: [PATCH 08/24] Resolve comments --- .../check_locale_files.test.js.snap | 18 +++++++++++++++ src/dev/i18n/check_locale_files.js | 14 ++++++----- src/dev/i18n/check_locale_files.test.js | 23 ++++--------------- .../i18n/extract_default_translations.test.js | 11 +++++---- src/dev/i18n/utils.js | 6 ++--- src/dev/i18n/verify_locale_json.js | 22 +++++++----------- src/dev/run_check_locale_files.js | 4 +--- 7 files changed, 48 insertions(+), 50 deletions(-) create mode 100644 src/dev/i18n/__snapshots__/check_locale_files.test.js.snap diff --git a/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap b/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap new file mode 100644 index 0000000000000..53d7bcac6a049 --- /dev/null +++ b/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/check_locale_files checkFile throws an error for unused id and missing id 1`] = ` +" +Missing translations in locale file __fixtures__\\\\check_locale_files\\\\test_plugin_3\\\\translations\\\\missing.json: +test_plugin_3.id_2" +`; + +exports[`dev/i18n/check_locale_files checkFile throws an error for unused id and missing id 2`] = ` +" +Unused translations in locale file __fixtures__\\\\check_locale_files\\\\test_plugin_3\\\\translations\\\\unused.json: +test_plugin_3.id_3" +`; + +exports[`dev/i18n/check_locale_files checkLocaleFiles throws an error for namespaces collision 1`] = ` +"Error in __fixtures__\\\\check_locale_files\\\\test_plugin_5 plugin valid.json locale file +Locale file namespace should be unique for each plugin" +`; diff --git a/src/dev/i18n/check_locale_files.js b/src/dev/i18n/check_locale_files.js index 1417334d7e4a0..b52b27acbcfa3 100644 --- a/src/dev/i18n/check_locale_files.js +++ b/src/dev/i18n/check_locale_files.js @@ -20,7 +20,7 @@ import path from 'path'; import JSON5 from 'json5'; -import { arraysDiff, globAsync, readFileAsync } from './utils'; +import { difference, globAsync, readFileAsync } from './utils'; import { verifyJSON } from './verify_locale_json'; export async function checkFile(localePath) { @@ -37,21 +37,22 @@ export async function checkFile(localePath) { try { namespace = verifyJSON(localeBuffer.toString(), localePath); } catch (error) { - throw new Error(`Error in ${localePath}\n${error.message || error}`); + throw new Error(`Error in ${path.relative(__dirname, localePath)}\n${error.message || error}`); } const translations = JSON5.parse(localeBuffer.toString()); const translationsIds = Object.keys(translations); - const [unusedTranslations, missingTranslations] = arraysDiff(translationsIds, defaultMessagesIds); + const unusedTranslations = difference(translationsIds, defaultMessagesIds); + const missingTranslations = difference(defaultMessagesIds, translationsIds); if (unusedTranslations.length > 0) { - errorMessage += `\nUnused translations in locale file ${localePath}: + errorMessage += `\nUnused translations in locale file ${path.relative(__dirname, localePath)}: ${unusedTranslations.join(', ')}`; } if (missingTranslations.length > 0) { - errorMessage += `\nMissing translations in locale file ${localePath}: + errorMessage += `\nMissing translations in locale file ${path.relative(__dirname, localePath)}: ${missingTranslations.join(', ')}`; } @@ -90,7 +91,8 @@ export async function checkLocaleFiles(pluginsPaths) { const namespace = await checkFile(path.resolve(pluginPath, 'translations', locale)); if (namespaces.includes(namespace)) { throw new Error( - `Error in ${pluginPath} plugin ${locale} locale file\nLocale file namespace should be unique for each plugin` + `Error in ${path.relative(__dirname, pluginPath)} plugin ${locale} locale file +Locale file namespace should be unique for each plugin` ); } namespaces.push(namespace); diff --git a/src/dev/i18n/check_locale_files.test.js b/src/dev/i18n/check_locale_files.test.js index f351f4b0ea474..3a470f078758e 100644 --- a/src/dev/i18n/check_locale_files.test.js +++ b/src/dev/i18n/check_locale_files.test.js @@ -44,18 +44,8 @@ describe('dev/i18n/check_locale_files', () => { it('throws an error for unused id and missing id', async () => { const localeWithMissingMessage = path.join(pluginsPaths[2], 'translations', 'missing.json'); const localeWithUnusedMessage = path.join(pluginsPaths[2], 'translations', 'unused.json'); - - expect(checkFile(localeWithMissingMessage)).rejects.toEqual( - new Error( - `\nMissing translations in locale file ${localeWithMissingMessage}:\ntest_plugin_3.id_2` - ) - ); - - expect(checkFile(localeWithUnusedMessage)).rejects.toEqual( - new Error( - `\nUnused translations in locale file ${localeWithUnusedMessage}:\ntest_plugin_3.id_3` - ) - ); + await expect(checkFile(localeWithMissingMessage)).rejects.toThrowErrorMatchingSnapshot(); + await expect(checkFile(localeWithUnusedMessage)).rejects.toThrowErrorMatchingSnapshot(); }); }); @@ -65,12 +55,9 @@ describe('dev/i18n/check_locale_files', () => { }); it('throws an error for namespaces collision', async () => { - expect(checkLocaleFiles([pluginsPaths[3], pluginsPaths[4]])).rejects.toEqual( - new Error( - `Error in ${pluginsPaths[4]} plugin valid.json locale file -Locale file namespace should be unique for each plugin` - ) - ); + await expect( + checkLocaleFiles([pluginsPaths[3], pluginsPaths[4]]) + ).rejects.toThrowErrorMatchingSnapshot(); }); }); }); diff --git a/src/dev/i18n/extract_default_translations.test.js b/src/dev/i18n/extract_default_translations.test.js index d01e8c6a8bb04..51e7ecf038406 100644 --- a/src/dev/i18n/extract_default_translations.test.js +++ b/src/dev/i18n/extract_default_translations.test.js @@ -34,16 +34,17 @@ const PLUGIN_PATH = path.resolve( 'test_plugin' ); +const pluginTranslationsPath = path.resolve(PLUGIN_PATH, 'translations'); +const pluginTranslationsEnPath = path.resolve(pluginTranslationsPath, 'en.json'); + describe('dev/i18n/extract_default_translations', () => { it('injects default formats into en.json', async () => { await extractDefaultTranslations(PLUGIN_PATH); - const extractedJSONBuffer = await readFileAsync( - path.resolve(PLUGIN_PATH, 'translations', 'en.json') - ); + const extractedJSONBuffer = await readFileAsync(pluginTranslationsEnPath); - await unlinkAsync(path.resolve(PLUGIN_PATH, 'translations', 'en.json')); - await removeDirAsync(path.resolve(PLUGIN_PATH, 'translations')); + await unlinkAsync(pluginTranslationsEnPath); + await removeDirAsync(pluginTranslationsPath); expect(extractedJSONBuffer.toString()).toMatchSnapshot(); }); diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 61546782f4960..5cc285809d177 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -38,10 +38,8 @@ export const globAsync = promisify(glob); export const makeDirAsync = promisify(fs.mkdir); export const accessAsync = promisify(fs.access); -export function arraysDiff(left = [], right = []) { - const leftDiff = left.filter(value => !right.includes(value)); - const rightDiff = right.filter(value => !left.includes(value)); - return [leftDiff, rightDiff]; +export function difference(left = [], right = []) { + return left.filter(value => !right.includes(value)); } export function isPropertyWithKey(property, identifierName) { diff --git a/src/dev/i18n/verify_locale_json.js b/src/dev/i18n/verify_locale_json.js index a008b0c005a70..74641159d0b4d 100644 --- a/src/dev/i18n/verify_locale_json.js +++ b/src/dev/i18n/verify_locale_json.js @@ -24,7 +24,6 @@ import { traverseNodes } from './utils'; export function verifyJSON(json) { const jsonAST = parse(`+${json}`); - let namespace = ''; for (const node of traverseNodes(jsonAST.program.body)) { if (!isObjectExpression(node)) { @@ -35,19 +34,16 @@ export function verifyJSON(json) { throw 'Locale file should contain formats object.'; } - for (const property of node.properties) { - if (property.key.name !== 'formats') { - const messageNamespace = property.key.value.split('.')[0]; - if (!namespace) { - namespace = messageNamespace; - } + const nameProperties = node.properties.filter(property => property.key.name !== 'formats'); + const namespaces = nameProperties.map(property => property.key.value.split('.')[0]); + const uniqueNamespaces = new Set(namespaces); - if (namespace !== messageNamespace) { - throw 'All messages ids should start with the same namespace.'; - } - } + if (uniqueNamespaces.size !== 1) { + throw 'All messages ids should start with the same namespace.'; } + const namespace = uniqueNamespaces.values().next().value; + const idsSet = new Set(); for (const id of node.properties.map(prop => prop.key.value)) { if (idsSet.has(id)) { @@ -56,8 +52,6 @@ export function verifyJSON(json) { idsSet.add(id); } - break; + return namespace; } - - return namespace; } diff --git a/src/dev/run_check_locale_files.js b/src/dev/run_check_locale_files.js index 91e4c021fa69a..541cf56ee161b 100644 --- a/src/dev/run_check_locale_files.js +++ b/src/dev/run_check_locale_files.js @@ -20,6 +20,4 @@ import { run } from './run'; import { checkLocaleFiles } from './i18n/check_locale_files'; -run(async () => { - await checkLocaleFiles(process.argv.slice(2)); -}); +run(() => checkLocaleFiles(process.argv.slice(2))); From 6da77970501f59109d51dba3ca33406a481ce99a Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Thu, 26 Jul 2018 19:27:00 +0300 Subject: [PATCH 09/24] Normalize paths for crossplatform snapshot testing --- package.json | 1 + .../i18n/__snapshots__/check_locale_files.test.js.snap | 6 +++--- src/dev/i18n/check_locale_files.js | 9 +++++---- yarn.lock | 4 ++++ 4 files changed, 13 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index d5214a2471481..8a3e25cb59121 100644 --- a/package.json +++ b/package.json @@ -331,6 +331,7 @@ "ncp": "2.0.0", "nock": "8.0.0", "node-sass": "^4.9.0", + "normalize-path": "^3.0.0", "pixelmatch": "4.0.2", "prettier": "^1.12.1", "proxyquire": "1.7.11", diff --git a/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap b/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap index 53d7bcac6a049..3febd1b1b71d3 100644 --- a/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap +++ b/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap @@ -2,17 +2,17 @@ exports[`dev/i18n/check_locale_files checkFile throws an error for unused id and missing id 1`] = ` " -Missing translations in locale file __fixtures__\\\\check_locale_files\\\\test_plugin_3\\\\translations\\\\missing.json: +Missing translations in locale file __fixtures__/check_locale_files/test_plugin_3/translations/missing.json: test_plugin_3.id_2" `; exports[`dev/i18n/check_locale_files checkFile throws an error for unused id and missing id 2`] = ` " -Unused translations in locale file __fixtures__\\\\check_locale_files\\\\test_plugin_3\\\\translations\\\\unused.json: +Unused translations in locale file __fixtures__/check_locale_files/test_plugin_3/translations/unused.json: test_plugin_3.id_3" `; exports[`dev/i18n/check_locale_files checkLocaleFiles throws an error for namespaces collision 1`] = ` -"Error in __fixtures__\\\\check_locale_files\\\\test_plugin_5 plugin valid.json locale file +"Error in __fixtures__/check_locale_files/test_plugin_5 plugin valid.json locale file Locale file namespace should be unique for each plugin" `; diff --git a/src/dev/i18n/check_locale_files.js b/src/dev/i18n/check_locale_files.js index b52b27acbcfa3..71417a5eda8ed 100644 --- a/src/dev/i18n/check_locale_files.js +++ b/src/dev/i18n/check_locale_files.js @@ -19,6 +19,7 @@ import path from 'path'; import JSON5 from 'json5'; +import normalize from 'normalize-path'; import { difference, globAsync, readFileAsync } from './utils'; import { verifyJSON } from './verify_locale_json'; @@ -37,7 +38,7 @@ export async function checkFile(localePath) { try { namespace = verifyJSON(localeBuffer.toString(), localePath); } catch (error) { - throw new Error(`Error in ${path.relative(__dirname, localePath)}\n${error.message || error}`); + throw new Error(`Error in ${normalize(path.relative(__dirname, localePath))}\n${error.message || error}`); } const translations = JSON5.parse(localeBuffer.toString()); @@ -47,12 +48,12 @@ export async function checkFile(localePath) { const missingTranslations = difference(defaultMessagesIds, translationsIds); if (unusedTranslations.length > 0) { - errorMessage += `\nUnused translations in locale file ${path.relative(__dirname, localePath)}: + errorMessage += `\nUnused translations in locale file ${normalize(path.relative(__dirname, localePath))}: ${unusedTranslations.join(', ')}`; } if (missingTranslations.length > 0) { - errorMessage += `\nMissing translations in locale file ${path.relative(__dirname, localePath)}: + errorMessage += `\nMissing translations in locale file ${normalize(path.relative(__dirname, localePath))}: ${missingTranslations.join(', ')}`; } @@ -91,7 +92,7 @@ export async function checkLocaleFiles(pluginsPaths) { const namespace = await checkFile(path.resolve(pluginPath, 'translations', locale)); if (namespaces.includes(namespace)) { throw new Error( - `Error in ${path.relative(__dirname, pluginPath)} plugin ${locale} locale file + `Error in ${normalize(path.relative(__dirname, pluginPath))} plugin ${locale} locale file Locale file namespace should be unique for each plugin` ); } diff --git a/yarn.lock b/yarn.lock index b5c25bd93d1e5..1b567a6dd5e35 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9574,6 +9574,10 @@ normalize-path@^2.0.0, normalize-path@^2.0.1, normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" +normalize-path@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + normalize-range@^0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/normalize-range/-/normalize-range-0.1.2.tgz#2d10c06bdfd312ea9777695a4d28439456b75942" From 06b3bad1d630037dbe5a23a986b0fbc44ec6510c Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Mon, 13 Aug 2018 15:07:25 +0300 Subject: [PATCH 10/24] Add locale files integration --- ...ale_files.js => integrate_locale_files.js} | 2 +- .../test_plugin_1/translations/en.json | 60 -------- .../test_plugin_1/translations/valid.json | 60 -------- .../test_plugin_2/translations/en.json | 60 -------- .../test_plugin_3/translations/en.json | 60 -------- .../test_plugin_3/translations/missing.json | 59 -------- .../test_plugin_3/translations/unused.json | 61 -------- .../test_plugin_4/translations/en.json | 60 -------- .../test_plugin_4/translations/valid.json | 60 -------- .../test_plugin_5/translations/en.json | 60 -------- .../test_plugin_5/translations/valid.json | 60 -------- .../test_plugin_1/index.js | 23 +++ .../test_plugin_2/index.js} | 5 +- .../translations/fr.json} | 5 +- .../check_locale_files.test.js.snap | 18 --- .../integrate_locale_files.test.js.snap | 138 ++++++++++++++++++ src/dev/i18n/check_locale_files.js | 102 ------------- src/dev/i18n/check_locale_files.test.js | 63 -------- src/dev/i18n/integrate_locale_files.js | 108 ++++++++++++++ src/dev/i18n/integrate_locale_files.test.js | 95 ++++++++++++ src/dev/i18n/verify_locale_json.js | 13 +- src/dev/run_integrate_locale_files.js | 36 +++++ 22 files changed, 408 insertions(+), 800 deletions(-) rename scripts/{check_locale_files.js => integrate_locale_files.js} (94%) delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/en.json delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/valid.json delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/en.json delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/en.json delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/missing.json delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/unused.json delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/en.json delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/valid.json delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/en.json delete mode 100644 src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/valid.json create mode 100644 src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/index.js rename src/dev/{run_check_locale_files.js => i18n/__fixtures__/integrate_locale_files/test_plugin_2/index.js} (84%) rename src/dev/i18n/__fixtures__/{check_locale_files/test_plugin_2/translations/valid.json => integrate_locale_files/translations/fr.json} (88%) delete mode 100644 src/dev/i18n/__snapshots__/check_locale_files.test.js.snap create mode 100644 src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap delete mode 100644 src/dev/i18n/check_locale_files.js delete mode 100644 src/dev/i18n/check_locale_files.test.js create mode 100644 src/dev/i18n/integrate_locale_files.js create mode 100644 src/dev/i18n/integrate_locale_files.test.js create mode 100644 src/dev/run_integrate_locale_files.js diff --git a/scripts/check_locale_files.js b/scripts/integrate_locale_files.js similarity index 94% rename from scripts/check_locale_files.js rename to scripts/integrate_locale_files.js index f5e0dffd47efe..8f33c0bb34cb7 100644 --- a/scripts/check_locale_files.js +++ b/scripts/integrate_locale_files.js @@ -18,4 +18,4 @@ */ require('../src/setup_node_env'); -require('../src/dev/run_check_locale_files'); +require('../src/dev/run_integrate_locale_files'); diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/en.json deleted file mode 100644 index 1bd44013a5c88..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/en.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'test_plugin_1.id_1': 'Message text 1', - 'test_plugin_1.id_2': 'Message text 2', -} diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/valid.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/valid.json deleted file mode 100644 index a8bc7b7bff344..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_1/translations/valid.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'test_plugin_1.id_1': 'Translated text 1', - 'test_plugin_1.id_2': 'Translated text 2', -} diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/en.json deleted file mode 100644 index 6c6130c91bd99..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/en.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'test_plugin_2.id_1': 'Message text 1', - 'test_plugin_2.id_2': 'Message text 2', -} diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/en.json deleted file mode 100644 index f42068bd5d392..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/en.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'test_plugin_3.id_1': 'Message text 1', - 'test_plugin_3.id_2': 'Message text 2', -} diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/missing.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/missing.json deleted file mode 100644 index 3da67f939a03d..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/missing.json +++ /dev/null @@ -1,59 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'test_plugin_3.id_1': 'Message text 1', -} diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/unused.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/unused.json deleted file mode 100644 index fa67268fdc6e9..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_3/translations/unused.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'test_plugin_3.id_1': 'Message text 1', - 'test_plugin_3.id_2': 'Message text 2', - 'test_plugin_3.id_3': 'Message text 3', -} diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/en.json deleted file mode 100644 index 306db3232d0d9..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/en.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'duplicated_namespace.id_1': 'Message text 1', - 'duplicated_namespace.id_2': 'Message text 2', -} diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/valid.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/valid.json deleted file mode 100644 index 90d7752222365..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_4/translations/valid.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'duplicated_namespace.id_1': 'Translated text 1', - 'duplicated_namespace.id_2': 'Translated text 2', -} diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/en.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/en.json deleted file mode 100644 index 31b87e59109cf..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/en.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'duplicated_namespace.id_3': 'Message text 3', - 'duplicated_namespace.id_4': 'Message text 4', -} diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/valid.json b/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/valid.json deleted file mode 100644 index 7fffdf03efe22..0000000000000 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_5/translations/valid.json +++ /dev/null @@ -1,60 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'duplicated_namespace.id_3': 'Translated text 3', - 'duplicated_namespace.id_4': 'Translated text 4', -} diff --git a/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/index.js b/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/index.js new file mode 100644 index 0000000000000..34ef6a75b6ba7 --- /dev/null +++ b/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/index.js @@ -0,0 +1,23 @@ +/* + * 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 { i18n } from '@kbn/i18n'; + +i18n.translate('plugin-1.message-id-1', { defaultMessage: 'Message text 1' }); +i18n.translate('plugin-1.message-id-2', { defaultMessage: 'Message text 2' }); diff --git a/src/dev/run_check_locale_files.js b/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/index.js similarity index 84% rename from src/dev/run_check_locale_files.js rename to src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/index.js index 541cf56ee161b..dd3ca99c8e445 100644 --- a/src/dev/run_check_locale_files.js +++ b/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/index.js @@ -17,7 +17,6 @@ * under the License. */ -import { run } from './run'; -import { checkLocaleFiles } from './i18n/check_locale_files'; +import { i18n } from '@kbn/i18n'; -run(() => checkLocaleFiles(process.argv.slice(2))); +i18n.translate('plugin-2.message-id', { defaultMessage: 'Message text' }); diff --git a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/valid.json b/src/dev/i18n/__fixtures__/integrate_locale_files/translations/fr.json similarity index 88% rename from src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/valid.json rename to src/dev/i18n/__fixtures__/integrate_locale_files/translations/fr.json index 1110bbcd2ae5c..8f797cd2fbd16 100644 --- a/src/dev/i18n/__fixtures__/check_locale_files/test_plugin_2/translations/valid.json +++ b/src/dev/i18n/__fixtures__/integrate_locale_files/translations/fr.json @@ -55,6 +55,7 @@ }, }, }, - 'test_plugin_2.id_1': 'Translated text 1', - 'test_plugin_2.id_2': 'Translated text 2', + 'plugin-1.message-id-1': 'Translated text 1', + 'plugin-1.message-id-2': 'Translated text 2', + 'plugin-2.message-id': 'Translated text', } diff --git a/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap b/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap deleted file mode 100644 index 3febd1b1b71d3..0000000000000 --- a/src/dev/i18n/__snapshots__/check_locale_files.test.js.snap +++ /dev/null @@ -1,18 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/check_locale_files checkFile throws an error for unused id and missing id 1`] = ` -" -Missing translations in locale file __fixtures__/check_locale_files/test_plugin_3/translations/missing.json: -test_plugin_3.id_2" -`; - -exports[`dev/i18n/check_locale_files checkFile throws an error for unused id and missing id 2`] = ` -" -Unused translations in locale file __fixtures__/check_locale_files/test_plugin_3/translations/unused.json: -test_plugin_3.id_3" -`; - -exports[`dev/i18n/check_locale_files checkLocaleFiles throws an error for namespaces collision 1`] = ` -"Error in __fixtures__/check_locale_files/test_plugin_5 plugin valid.json locale file -Locale file namespace should be unique for each plugin" -`; diff --git a/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap new file mode 100644 index 0000000000000..533c7d92211a0 --- /dev/null +++ b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap @@ -0,0 +1,138 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/check_locale_files integrateLocaleFiles splits locale file by plugins and moves it to plugins folders 1`] = ` +"{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'plugin-1.message-id-1': 'Translated text 1', + 'plugin-1.message-id-2': 'Translated text 2', +}" +`; + +exports[`dev/i18n/check_locale_files integrateLocaleFiles splits locale file by plugins and moves it to plugins folders 2`] = ` +"{ + formats: { + number: { + currency: { + style: 'currency', + }, + percent: { + style: 'percent', + }, + }, + date: { + short: { + month: 'numeric', + day: 'numeric', + year: '2-digit', + }, + medium: { + month: 'short', + day: 'numeric', + year: 'numeric', + }, + long: { + month: 'long', + day: 'numeric', + year: 'numeric', + }, + full: { + weekday: 'long', + month: 'long', + day: 'numeric', + year: 'numeric', + }, + }, + time: { + short: { + hour: 'numeric', + minute: 'numeric', + }, + medium: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + }, + long: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + full: { + hour: 'numeric', + minute: 'numeric', + second: 'numeric', + timeZoneName: 'short', + }, + }, + }, + 'plugin-2.message-id': 'Translated text', +}" +`; + +exports[`dev/i18n/check_locale_files verifyMessages throws an error for unused id and missing id 1`] = ` +" +Missing translations: +plugin-1.message-id-2" +`; + +exports[`dev/i18n/check_locale_files verifyMessages throws an error for unused id and missing id 2`] = ` +" +Unused translations: +plugin-1.message-id-3" +`; diff --git a/src/dev/i18n/check_locale_files.js b/src/dev/i18n/check_locale_files.js deleted file mode 100644 index 71417a5eda8ed..0000000000000 --- a/src/dev/i18n/check_locale_files.js +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import path from 'path'; -import JSON5 from 'json5'; -import normalize from 'normalize-path'; - -import { difference, globAsync, readFileAsync } from './utils'; -import { verifyJSON } from './verify_locale_json'; - -export async function checkFile(localePath) { - let errorMessage = ''; - - const defaultMessagesBuffer = await readFileAsync( - path.resolve(path.dirname(localePath), 'en.json') - ); - const defaultMessagesIds = Object.keys(JSON5.parse(defaultMessagesBuffer.toString())); - - const localeBuffer = await readFileAsync(localePath); - - let namespace; - try { - namespace = verifyJSON(localeBuffer.toString(), localePath); - } catch (error) { - throw new Error(`Error in ${normalize(path.relative(__dirname, localePath))}\n${error.message || error}`); - } - - const translations = JSON5.parse(localeBuffer.toString()); - const translationsIds = Object.keys(translations); - - const unusedTranslations = difference(translationsIds, defaultMessagesIds); - const missingTranslations = difference(defaultMessagesIds, translationsIds); - - if (unusedTranslations.length > 0) { - errorMessage += `\nUnused translations in locale file ${normalize(path.relative(__dirname, localePath))}: -${unusedTranslations.join(', ')}`; - } - - if (missingTranslations.length > 0) { - errorMessage += `\nMissing translations in locale file ${normalize(path.relative(__dirname, localePath))}: -${missingTranslations.join(', ')}`; - } - - if (errorMessage) { - throw new Error(errorMessage); - } - - return namespace; -} - -export async function checkLocaleFiles(pluginsPaths) { - const pluginsMapByLocale = new Map(); - - for (const pluginPath of pluginsPaths) { - const globOptions = { - ignore: ['./translations/en.json', './translations/messagesCache.json'], - cwd: path.resolve(pluginPath), - }; - - const localeEntries = await globAsync('./translations/*.json', globOptions); - - for (const entry of localeEntries) { - const locale = path.basename(entry); - - if (pluginsMapByLocale.has(locale)) { - pluginsMapByLocale.get(locale).push(pluginPath); - } else { - pluginsMapByLocale.set(locale, [pluginPath]); - } - } - } - - for (const locale of pluginsMapByLocale.keys()) { - const namespaces = []; - for (const pluginPath of pluginsMapByLocale.get(locale)) { - const namespace = await checkFile(path.resolve(pluginPath, 'translations', locale)); - if (namespaces.includes(namespace)) { - throw new Error( - `Error in ${normalize(path.relative(__dirname, pluginPath))} plugin ${locale} locale file -Locale file namespace should be unique for each plugin` - ); - } - namespaces.push(namespace); - } - } -} diff --git a/src/dev/i18n/check_locale_files.test.js b/src/dev/i18n/check_locale_files.test.js deleted file mode 100644 index 3a470f078758e..0000000000000 --- a/src/dev/i18n/check_locale_files.test.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import path from 'path'; - -import { checkFile, checkLocaleFiles } from './check_locale_files'; - -const testsFixturesRoot = path.resolve(__dirname, '__fixtures__', 'check_locale_files'); - -const pluginsPaths = [ - path.join(testsFixturesRoot, 'test_plugin_1'), - path.join(testsFixturesRoot, 'test_plugin_2'), - path.join(testsFixturesRoot, 'test_plugin_3'), - path.join(testsFixturesRoot, 'test_plugin_4'), - path.join(testsFixturesRoot, 'test_plugin_5'), -]; - -describe('dev/i18n/check_locale_files', () => { - describe('checkFile', () => { - it('returns namespace of a valid JSON file', async () => { - const localePath1 = path.join(pluginsPaths[0], 'translations', 'valid.json'); - const localePath2 = path.join(pluginsPaths[1], 'translations', 'valid.json'); - - expect(await checkFile(localePath1)).toBe('test_plugin_1'); - expect(await checkFile(localePath2)).toBe('test_plugin_2'); - }); - - it('throws an error for unused id and missing id', async () => { - const localeWithMissingMessage = path.join(pluginsPaths[2], 'translations', 'missing.json'); - const localeWithUnusedMessage = path.join(pluginsPaths[2], 'translations', 'unused.json'); - await expect(checkFile(localeWithMissingMessage)).rejects.toThrowErrorMatchingSnapshot(); - await expect(checkFile(localeWithUnusedMessage)).rejects.toThrowErrorMatchingSnapshot(); - }); - }); - - describe('checkLocaleFiles', () => { - it('validates locale files in multiple plugins', async () => { - expect(await checkLocaleFiles([pluginsPaths[0], pluginsPaths[1]])).toBe(undefined); - }); - - it('throws an error for namespaces collision', async () => { - await expect( - checkLocaleFiles([pluginsPaths[3], pluginsPaths[4]]) - ).rejects.toThrowErrorMatchingSnapshot(); - }); - }); -}); diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js new file mode 100644 index 0000000000000..b35b7483141d2 --- /dev/null +++ b/src/dev/i18n/integrate_locale_files.js @@ -0,0 +1,108 @@ +/* + * 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 path from 'path'; +import JSON5 from 'json5'; +import normalize from 'normalize-path'; + +import { + difference, + globAsync, + readFileAsync, + writeFileAsync, + accessAsync, + makeDirAsync, +} from './utils'; +import { verifyJSON } from './verify_locale_json'; +import config from '../../../.localizationrc.json'; + +export function verifyMessages(localeMessages, defaultMessagesMap) { + let errorMessage = ''; + + const defaultMessagesIds = [...defaultMessagesMap.keys()]; + const localeMessagesIds = Object.keys(localeMessages).filter(id => id !== 'formats'); + + const unusedTranslations = difference(localeMessagesIds, defaultMessagesIds); + const missingTranslations = difference(defaultMessagesIds, localeMessagesIds); + + if (unusedTranslations.length > 0) { + errorMessage += `\nUnused translations:\n${unusedTranslations.join(', ')}`; + } + + if (missingTranslations.length > 0) { + errorMessage += `\nMissing translations:\n${missingTranslations.join(', ')}`; + } + + if (errorMessage) { + throw new Error(errorMessage); + } +} + +export async function integrateLocaleFiles(localesPath, defaultMessagesMap) { + const globOptions = { + cwd: path.resolve(localesPath), + }; + + const localeEntries = await globAsync('./*.json', globOptions); + + for (const entry of localeEntries.map(entry => path.resolve(localesPath, entry))) { + const localeJSON = (await readFileAsync(entry)).toString(); + const localeMessages = JSON5.parse(localeJSON); + + try { + await verifyJSON(localeJSON); + await verifyMessages(localeMessages, defaultMessagesMap); + } catch (error) { + throw new Error(`Error in ${normalize(path.relative(__dirname, entry))}: +${error.message || error}`); + } + + const fileName = path.basename(entry); + const messagesByPluginMap = new Map(); + const messagesEntries = Object.entries(localeMessages).filter(([id]) => id !== 'formats'); + + for (const [messageId, messageValue] of messagesEntries) { + const [namespace] = messageId.split('.'); + + if (messagesByPluginMap.has(namespace)) { + messagesByPluginMap.get(namespace)[messageId] = messageValue; + } else { + messagesByPluginMap.set(namespace, { + formats: localeMessages.formats, + [messageId]: messageValue, + }); + } + } + + for (const [namespace, messages] of messagesByPluginMap) { + const pluginPath = config.paths[namespace]; + + try { + await accessAsync(path.resolve(pluginPath, 'translations')); + } catch (_) { + await makeDirAsync(path.resolve(pluginPath, 'translations')); + } + + await writeFileAsync( + path.resolve(pluginPath, 'translations', fileName), + JSON5.stringify(messages, { space: 2 }) + ); + } + } +} diff --git a/src/dev/i18n/integrate_locale_files.test.js b/src/dev/i18n/integrate_locale_files.test.js new file mode 100644 index 0000000000000..0ef1fc87228df --- /dev/null +++ b/src/dev/i18n/integrate_locale_files.test.js @@ -0,0 +1,95 @@ +/* + * 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 path from 'path'; +import fs from 'fs'; + +import { verifyMessages, integrateLocaleFiles } from './integrate_locale_files'; + +const testsFixturesRoot = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files'); +const localesPath = path.join(testsFixturesRoot, 'translations'); + +const defaultMessagesMap = new Map([ + ['plugin-1.message-id-1', 'Message text 1'], + ['plugin-1.message-id-2', 'Message text 2'], + ['plugin-2.message-id', 'Message text'], +]); + +jest.mock('../../../.localizationrc.json', () => ({ + paths: { + 'plugin-1': 'src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1', + 'plugin-2': 'src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2', + }, + exclude: [], +})); + +describe('dev/i18n/check_locale_files', () => { + describe('verifyMessages', () => { + it('validates locale messages object', () => { + const localeMessages = { + formats: {}, + 'plugin-1.message-id-1': 'Translated text 1', + 'plugin-1.message-id-2': 'Translated text 2', + 'plugin-2.message-id': 'Translated text', + }; + + expect(() => verifyMessages(localeMessages, defaultMessagesMap)).not.toThrow(); + }); + + it('throws an error for unused id and missing id', async () => { + const localeMessagesWithMissingMessage = { + formats: {}, + 'plugin-1.message-id-1': 'Translated text 1', + 'plugin-2.message-id': 'Translated text', + }; + + const localeMessagesWithUnusedMessage = { + formats: {}, + 'plugin-1.message-id-1': 'Translated text 1', + 'plugin-1.message-id-2': 'Translated text 2', + 'plugin-1.message-id-3': 'Translated text 3', + 'plugin-2.message-id': 'Translated text', + }; + + await expect(() => + verifyMessages(localeMessagesWithMissingMessage, defaultMessagesMap) + ).toThrowErrorMatchingSnapshot(); + await expect(() => + verifyMessages(localeMessagesWithUnusedMessage, defaultMessagesMap) + ).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('integrateLocaleFiles', () => { + it('splits locale file by plugins and moves it to plugins folders', async () => { + await integrateLocaleFiles(localesPath, defaultMessagesMap); + + [ + './src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/translations/fr.json', + './src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/translations/fr.json', + ].map(integratedLocalePath => { + const integratedLocaleJSONBuffer = fs.readFileSync(integratedLocalePath); + fs.unlinkSync(integratedLocalePath); + fs.rmdirSync(path.dirname(integratedLocalePath)); + + expect(integratedLocaleJSONBuffer.toString()).toMatchSnapshot(); + }); + }); + }); +}); diff --git a/src/dev/i18n/verify_locale_json.js b/src/dev/i18n/verify_locale_json.js index 74641159d0b4d..a7d8dc5a35005 100644 --- a/src/dev/i18n/verify_locale_json.js +++ b/src/dev/i18n/verify_locale_json.js @@ -34,24 +34,15 @@ export function verifyJSON(json) { throw 'Locale file should contain formats object.'; } - const nameProperties = node.properties.filter(property => property.key.name !== 'formats'); - const namespaces = nameProperties.map(property => property.key.value.split('.')[0]); - const uniqueNamespaces = new Set(namespaces); - - if (uniqueNamespaces.size !== 1) { - throw 'All messages ids should start with the same namespace.'; - } - - const namespace = uniqueNamespaces.values().next().value; - const idsSet = new Set(); for (const id of node.properties.map(prop => prop.key.value)) { if (idsSet.has(id)) { throw `Ids collision: ${id}`; } + idsSet.add(id); } - return namespace; + break; } } diff --git a/src/dev/run_integrate_locale_files.js b/src/dev/run_integrate_locale_files.js new file mode 100644 index 0000000000000..fc589c1b4e4fe --- /dev/null +++ b/src/dev/run_integrate_locale_files.js @@ -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 yargs from 'yargs'; + +import { run } from './run'; +import { integrateLocaleFiles } from './i18n/integrate_locale_files'; +import { getDefaultMessagesMap } from './i18n/extract_default_translations'; + +run(async () => { + const { argv } = yargs.option('path', { + demandOption: true, + describe: 'Path to locale files directory', + type: 'string', + }); + + const defaultMessagesMap = await getDefaultMessagesMap(['.']); + + await integrateLocaleFiles(argv.path, defaultMessagesMap); +}); From 9507818800d7727d578f2daeeafb3aa483f4a63c Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Wed, 22 Aug 2018 17:41:54 +0300 Subject: [PATCH 11/24] Update integration tool --- .../integrate_locale_files/fr.json | 61 +++++ .../test_plugin_1/index.js | 23 -- .../test_plugin_2/index.js | 22 -- .../translations/fr.json | 61 ----- .../integrate_locale_files.test.js.snap | 232 +++++++++--------- src/dev/i18n/extract_default_translations.js | 15 +- src/dev/i18n/integrate_locale_files.js | 44 ++-- src/dev/i18n/integrate_locale_files.test.js | 28 +-- src/dev/i18n/utils.js | 6 + src/dev/i18n/verify_locale_json.js | 48 ---- 10 files changed, 223 insertions(+), 317 deletions(-) create mode 100644 src/dev/i18n/__fixtures__/integrate_locale_files/fr.json delete mode 100644 src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/index.js delete mode 100644 src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/index.js delete mode 100644 src/dev/i18n/__fixtures__/integrate_locale_files/translations/fr.json delete mode 100644 src/dev/i18n/verify_locale_json.js diff --git a/src/dev/i18n/__fixtures__/integrate_locale_files/fr.json b/src/dev/i18n/__fixtures__/integrate_locale_files/fr.json new file mode 100644 index 0000000000000..fe86b218f282c --- /dev/null +++ b/src/dev/i18n/__fixtures__/integrate_locale_files/fr.json @@ -0,0 +1,61 @@ +{ + "formats": { + "number": { + "currency": { + "style": "currency" + }, + "percent": { + "style": "percent" + } + }, + "date": { + "short": { + "month": "numeric", + "day": "numeric", + "year": "2-digit" + }, + "medium": { + "month": "short", + "day": "numeric", + "year": "numeric" + }, + "long": { + "month": "long", + "day": "numeric", + "year": "numeric" + }, + "full": { + "weekday": "long", + "month": "long", + "day": "numeric", + "year": "numeric" + } + }, + "time": { + "short": { + "hour": "numeric", + "minute": "numeric" + }, + "medium": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric" + }, + "long": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + }, + "full": { + "hour": "numeric", + "minute": "numeric", + "second": "numeric", + "timeZoneName": "short" + } + } + }, + "plugin-1.message-id-1": "Translated text 1", + "plugin-1.message-id-2": "Translated text 2", + "plugin-2.message-id": "Translated text" +} diff --git a/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/index.js b/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/index.js deleted file mode 100644 index 34ef6a75b6ba7..0000000000000 --- a/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/index.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@kbn/i18n'; - -i18n.translate('plugin-1.message-id-1', { defaultMessage: 'Message text 1' }); -i18n.translate('plugin-1.message-id-2', { defaultMessage: 'Message text 2' }); diff --git a/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/index.js b/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/index.js deleted file mode 100644 index dd3ca99c8e445..0000000000000 --- a/src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/index.js +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { i18n } from '@kbn/i18n'; - -i18n.translate('plugin-2.message-id', { defaultMessage: 'Message text' }); diff --git a/src/dev/i18n/__fixtures__/integrate_locale_files/translations/fr.json b/src/dev/i18n/__fixtures__/integrate_locale_files/translations/fr.json deleted file mode 100644 index 8f797cd2fbd16..0000000000000 --- a/src/dev/i18n/__fixtures__/integrate_locale_files/translations/fr.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, - }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - }, - }, - 'plugin-1.message-id-1': 'Translated text 1', - 'plugin-1.message-id-2': 'Translated text 2', - 'plugin-2.message-id': 'Translated text', -} diff --git a/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap index 533c7d92211a0..ce2c8fbca856f 100644 --- a/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap +++ b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap @@ -1,128 +1,134 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`dev/i18n/check_locale_files integrateLocaleFiles splits locale file by plugins and moves it to plugins folders 1`] = ` -"{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, +Array [ + "src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/translations/fr.json", + "{ + \\"formats\\": { + \\"number\\": { + \\"currency\\": { + \\"style\\": \\"currency\\" + }, + \\"percent\\": { + \\"style\\": \\"percent\\" + } }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, + \\"date\\": { + \\"short\\": { + \\"month\\": \\"numeric\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"2-digit\\" + }, + \\"medium\\": { + \\"month\\": \\"short\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + }, + \\"long\\": { + \\"month\\": \\"long\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + }, + \\"full\\": { + \\"weekday\\": \\"long\\", + \\"month\\": \\"long\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + } }, + \\"time\\": { + \\"short\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\" + }, + \\"medium\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\" + }, + \\"long\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\", + \\"timeZoneName\\": \\"short\\" + }, + \\"full\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\", + \\"timeZoneName\\": \\"short\\" + } + } }, - 'plugin-1.message-id-1': 'Translated text 1', - 'plugin-1.message-id-2': 'Translated text 2', -}" + \\"plugin-1.message-id-1\\": \\"Translated text 1\\", + \\"plugin-1.message-id-2\\": \\"Translated text 2\\" +}", +] `; exports[`dev/i18n/check_locale_files integrateLocaleFiles splits locale file by plugins and moves it to plugins folders 2`] = ` -"{ - formats: { - number: { - currency: { - style: 'currency', - }, - percent: { - style: 'percent', - }, +Array [ + "src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/translations/fr.json", + "{ + \\"formats\\": { + \\"number\\": { + \\"currency\\": { + \\"style\\": \\"currency\\" + }, + \\"percent\\": { + \\"style\\": \\"percent\\" + } }, - date: { - short: { - month: 'numeric', - day: 'numeric', - year: '2-digit', - }, - medium: { - month: 'short', - day: 'numeric', - year: 'numeric', - }, - long: { - month: 'long', - day: 'numeric', - year: 'numeric', - }, - full: { - weekday: 'long', - month: 'long', - day: 'numeric', - year: 'numeric', - }, - }, - time: { - short: { - hour: 'numeric', - minute: 'numeric', - }, - medium: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - }, - long: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, - full: { - hour: 'numeric', - minute: 'numeric', - second: 'numeric', - timeZoneName: 'short', - }, + \\"date\\": { + \\"short\\": { + \\"month\\": \\"numeric\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"2-digit\\" + }, + \\"medium\\": { + \\"month\\": \\"short\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + }, + \\"long\\": { + \\"month\\": \\"long\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + }, + \\"full\\": { + \\"weekday\\": \\"long\\", + \\"month\\": \\"long\\", + \\"day\\": \\"numeric\\", + \\"year\\": \\"numeric\\" + } }, + \\"time\\": { + \\"short\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\" + }, + \\"medium\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\" + }, + \\"long\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\", + \\"timeZoneName\\": \\"short\\" + }, + \\"full\\": { + \\"hour\\": \\"numeric\\", + \\"minute\\": \\"numeric\\", + \\"second\\": \\"numeric\\", + \\"timeZoneName\\": \\"short\\" + } + } }, - 'plugin-2.message-id': 'Translated text', -}" + \\"plugin-2.message-id\\": \\"Translated text\\" +}", +] `; exports[`dev/i18n/check_locale_files verifyMessages throws an error for unused id and missing id 1`] = ` diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 645c43ed319d1..ee86f63b8f8b8 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -20,13 +20,12 @@ import path from 'path'; import { i18n } from '@kbn/i18n'; import JSON5 from 'json5'; -import normalize from 'normalize-path'; import { extractHtmlMessages } from './extract_html_messages'; import { extractCodeMessages } from './extract_code_messages'; import { extractPugMessages } from './extract_pug_messages'; import { extractHandlebarsMessages } from './extract_handlebars_messages'; -import { globAsync, readFileAsync, writeFileAsync } from './utils'; +import { globAsync, normalizePath, readFileAsync, writeFileAsync } from './utils'; import { paths, exclude } from '../../../.i18nrc.json'; const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; @@ -42,10 +41,6 @@ function addMessageToMap(targetMap, key, value) { targetMap.set(key, value); } -function normalizePath(inputPath) { - return normalize(path.relative('.', inputPath)); -} - function filterPaths(inputPaths) { const availablePaths = Object.values(paths); const pathsForExtraction = new Set(); @@ -177,13 +172,19 @@ function serializeToJson(defaultMessages) { return JSON.stringify(resultJsonObject, undefined, 2); } -export async function extractDefaultTranslations({ paths, output, outputFormat }) { +export async function getDefaultMessagesMap(paths) { const defaultMessagesMap = new Map(); for (const inputPath of filterPaths(paths)) { await extractMessagesFromPathToMap(inputPath, defaultMessagesMap); } + return defaultMessagesMap; +} + +export async function extractDefaultTranslations({ paths, output, outputFormat }) { + const defaultMessagesMap = await getDefaultMessagesMap(paths); + // messages shouldn't be extracted to a file if output is not supplied if (!output || !defaultMessagesMap.size) { return; diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index b35b7483141d2..109e88d25dd5c 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -18,19 +18,9 @@ */ import path from 'path'; -import JSON5 from 'json5'; -import normalize from 'normalize-path'; - -import { - difference, - globAsync, - readFileAsync, - writeFileAsync, - accessAsync, - makeDirAsync, -} from './utils'; -import { verifyJSON } from './verify_locale_json'; -import config from '../../../.localizationrc.json'; + +import { difference, globAsync, normalizePath, readFileAsync, writeFileAsync } from './utils'; +import { paths } from '../../../.i18nrc.json'; export function verifyMessages(localeMessages, defaultMessagesMap) { let errorMessage = ''; @@ -62,14 +52,16 @@ export async function integrateLocaleFiles(localesPath, defaultMessagesMap) { const localeEntries = await globAsync('./*.json', globOptions); for (const entry of localeEntries.map(entry => path.resolve(localesPath, entry))) { - const localeJSON = (await readFileAsync(entry)).toString(); - const localeMessages = JSON5.parse(localeJSON); + const localeMessages = JSON.parse((await readFileAsync(entry)).toString()); + + if (!localeMessages.formats) { + throw 'Locale file should contain formats object.'; + } try { - await verifyJSON(localeJSON); - await verifyMessages(localeMessages, defaultMessagesMap); + verifyMessages(localeMessages, defaultMessagesMap); } catch (error) { - throw new Error(`Error in ${normalize(path.relative(__dirname, entry))}: + throw new Error(`Error in ${normalizePath(entry)}: ${error.message || error}`); } @@ -78,7 +70,11 @@ ${error.message || error}`); const messagesEntries = Object.entries(localeMessages).filter(([id]) => id !== 'formats'); for (const [messageId, messageValue] of messagesEntries) { - const [namespace] = messageId.split('.'); + const namespace = Object.keys(paths).find(key => messageId.startsWith(`${key}.`)); + + if (!namespace) { + throw new Error(`Unknown namespace in id ${messageId} in ${normalizePath(entry)}.`); + } if (messagesByPluginMap.has(namespace)) { messagesByPluginMap.get(namespace)[messageId] = messageValue; @@ -91,17 +87,11 @@ ${error.message || error}`); } for (const [namespace, messages] of messagesByPluginMap) { - const pluginPath = config.paths[namespace]; - - try { - await accessAsync(path.resolve(pluginPath, 'translations')); - } catch (_) { - await makeDirAsync(path.resolve(pluginPath, 'translations')); - } + const pluginPath = paths[namespace]; await writeFileAsync( path.resolve(pluginPath, 'translations', fileName), - JSON5.stringify(messages, { space: 2 }) + JSON.stringify(messages, undefined, 2) ); } } diff --git a/src/dev/i18n/integrate_locale_files.test.js b/src/dev/i18n/integrate_locale_files.test.js index 0ef1fc87228df..aaf2c6855bb57 100644 --- a/src/dev/i18n/integrate_locale_files.test.js +++ b/src/dev/i18n/integrate_locale_files.test.js @@ -18,12 +18,11 @@ */ import path from 'path'; -import fs from 'fs'; import { verifyMessages, integrateLocaleFiles } from './integrate_locale_files'; +import { normalizePath } from './utils'; -const testsFixturesRoot = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files'); -const localesPath = path.join(testsFixturesRoot, 'translations'); +const localesPath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files'); const defaultMessagesMap = new Map([ ['plugin-1.message-id-1', 'Message text 1'], @@ -31,7 +30,7 @@ const defaultMessagesMap = new Map([ ['plugin-2.message-id', 'Message text'], ]); -jest.mock('../../../.localizationrc.json', () => ({ +jest.mock('../../../.i18nrc.json', () => ({ paths: { 'plugin-1': 'src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1', 'plugin-2': 'src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2', @@ -39,6 +38,9 @@ jest.mock('../../../.localizationrc.json', () => ({ exclude: [], })); +const utils = require('./utils'); +utils.writeFileAsync = jest.fn(); + describe('dev/i18n/check_locale_files', () => { describe('verifyMessages', () => { it('validates locale messages object', () => { @@ -52,7 +54,7 @@ describe('dev/i18n/check_locale_files', () => { expect(() => verifyMessages(localeMessages, defaultMessagesMap)).not.toThrow(); }); - it('throws an error for unused id and missing id', async () => { + it('throws an error for unused id and missing id', () => { const localeMessagesWithMissingMessage = { formats: {}, 'plugin-1.message-id-1': 'Translated text 1', @@ -67,10 +69,10 @@ describe('dev/i18n/check_locale_files', () => { 'plugin-2.message-id': 'Translated text', }; - await expect(() => + expect(() => verifyMessages(localeMessagesWithMissingMessage, defaultMessagesMap) ).toThrowErrorMatchingSnapshot(); - await expect(() => + expect(() => verifyMessages(localeMessagesWithUnusedMessage, defaultMessagesMap) ).toThrowErrorMatchingSnapshot(); }); @@ -80,16 +82,10 @@ describe('dev/i18n/check_locale_files', () => { it('splits locale file by plugins and moves it to plugins folders', async () => { await integrateLocaleFiles(localesPath, defaultMessagesMap); - [ - './src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/translations/fr.json', - './src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/translations/fr.json', - ].map(integratedLocalePath => { - const integratedLocaleJSONBuffer = fs.readFileSync(integratedLocalePath); - fs.unlinkSync(integratedLocalePath); - fs.rmdirSync(path.dirname(integratedLocalePath)); + const [[path1, json1], [path2, json2]] = utils.writeFileAsync.mock.calls; - expect(integratedLocaleJSONBuffer.toString()).toMatchSnapshot(); - }); + expect([normalizePath(path1), json1]).toMatchSnapshot(); + expect([normalizePath(path2), json2]).toMatchSnapshot(); }); }); }); diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 6165e2ddc5c74..83eebba3b8b9b 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -27,6 +27,8 @@ import { import fs from 'fs'; import glob from 'glob'; import { promisify } from 'util'; +import normalize from 'normalize-path'; +import path from 'path'; const ESCAPE_LINE_BREAK_REGEX = /(? !right.includes(value)); } diff --git a/src/dev/i18n/verify_locale_json.js b/src/dev/i18n/verify_locale_json.js deleted file mode 100644 index a7d8dc5a35005..0000000000000 --- a/src/dev/i18n/verify_locale_json.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to Elasticsearch B.V. under one or more contributor - * license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright - * ownership. Elasticsearch B.V. licenses this file to you under - * the Apache License, Version 2.0 (the "License"); you may - * not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ - -import { parse } from '@babel/parser'; -import { isObjectExpression } from '@babel/types'; - -import { traverseNodes } from './utils'; - -export function verifyJSON(json) { - const jsonAST = parse(`+${json}`); - - for (const node of traverseNodes(jsonAST.program.body)) { - if (!isObjectExpression(node)) { - continue; - } - - if (!node.properties.some(prop => prop.key.name === 'formats')) { - throw 'Locale file should contain formats object.'; - } - - const idsSet = new Set(); - for (const id of node.properties.map(prop => prop.key.value)) { - if (idsSet.has(id)) { - throw `Ids collision: ${id}`; - } - - idsSet.add(id); - } - - break; - } -} From 38b8e270d270f51ec2a02485782c18fcec457310 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Mon, 27 Aug 2018 15:11:33 +0300 Subject: [PATCH 12/24] Resolve comments --- .../test_plugin/test_file.html | 1 - .../integrate_locale_files.test.js.snap | 24 +++-- src/dev/i18n/integrate_locale_files.js | 68 +++++++------- src/dev/i18n/integrate_locale_files.test.js | 89 ++++++++++++------- src/dev/run_integrate_locale_files.js | 17 ++-- 5 files changed, 112 insertions(+), 87 deletions(-) delete mode 100644 src/dev/i18n/__fixtures__/extract_default_translations/test_plugin/test_file.html diff --git a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin/test_file.html b/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin/test_file.html deleted file mode 100644 index b102216c7d12e..0000000000000 --- a/src/dev/i18n/__fixtures__/extract_default_translations/test_plugin/test_file.html +++ /dev/null @@ -1 +0,0 @@ -

{{ 'test-plugin.message-id' | i18n: { defaultMessage: 'Message text' } }}

diff --git a/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap index ce2c8fbca856f..99ab69b542ad8 100644 --- a/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap +++ b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap @@ -1,6 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`dev/i18n/check_locale_files integrateLocaleFiles splits locale file by plugins and moves it to plugins folders 1`] = ` +exports[`dev/i18n/integrate_locale_files integrateLocaleFiles splits locale file by plugins and writes them into the right folders 1`] = ` Array [ "src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/translations/fr.json", "{ @@ -66,7 +66,7 @@ Array [ ] `; -exports[`dev/i18n/check_locale_files integrateLocaleFiles splits locale file by plugins and moves it to plugins folders 2`] = ` +exports[`dev/i18n/integrate_locale_files integrateLocaleFiles splits locale file by plugins and writes them into the right folders 2`] = ` Array [ "src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/translations/fr.json", "{ @@ -131,14 +131,22 @@ Array [ ] `; -exports[`dev/i18n/check_locale_files verifyMessages throws an error for unused id and missing id 1`] = ` -" +exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 1`] = ` +"Error in translations/fr.json: Missing translations: -plugin-1.message-id-2" +plugin-1.message-id-2." `; -exports[`dev/i18n/check_locale_files verifyMessages throws an error for unused id and missing id 2`] = ` -" +exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 2`] = ` +"Error in translations/fr.json: Unused translations: -plugin-1.message-id-3" +plugin-1.message-id-3." +`; + +exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 3`] = ` +"Error in translations/fr.json: +Unused translations: +plugin-2.message +Missing translations: +plugin-2.message-id." `; diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index 109e88d25dd5c..5eb8c93e52875 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -21,15 +21,16 @@ import path from 'path'; import { difference, globAsync, normalizePath, readFileAsync, writeFileAsync } from './utils'; import { paths } from '../../../.i18nrc.json'; +import { getDefaultMessagesMap } from './extract_default_translations'; -export function verifyMessages(localeMessages, defaultMessagesMap) { +export function verifyMessages(localizedMessagesMap, defaultMessagesMap, filePath) { let errorMessage = ''; const defaultMessagesIds = [...defaultMessagesMap.keys()]; - const localeMessagesIds = Object.keys(localeMessages).filter(id => id !== 'formats'); + const localizedMessagesIds = [...localizedMessagesMap.keys()]; - const unusedTranslations = difference(localeMessagesIds, defaultMessagesIds); - const missingTranslations = difference(defaultMessagesIds, localeMessagesIds); + const unusedTranslations = difference(localizedMessagesIds, defaultMessagesIds); + const missingTranslations = difference(defaultMessagesIds, localizedMessagesIds); if (unusedTranslations.length > 0) { errorMessage += `\nUnused translations:\n${unusedTranslations.join(', ')}`; @@ -40,57 +41,58 @@ export function verifyMessages(localeMessages, defaultMessagesMap) { } if (errorMessage) { - throw new Error(errorMessage); + throw new Error(`Error in ${filePath}:${errorMessage}.`); } } -export async function integrateLocaleFiles(localesPath, defaultMessagesMap) { +export async function integrateLocaleFiles(localesPath) { + const defaultMessagesMap = await getDefaultMessagesMap(['.']); const globOptions = { cwd: path.resolve(localesPath), }; - const localeEntries = await globAsync('./*.json', globOptions); + const filePaths = (await globAsync('./*.json', globOptions)).map(filePath => + path.resolve(localesPath, filePath) + ); - for (const entry of localeEntries.map(entry => path.resolve(localesPath, entry))) { - const localeMessages = JSON.parse((await readFileAsync(entry)).toString()); + for (const filePath of filePaths) { + const normalizedFilePath = normalizePath(filePath); + const localizedMessages = JSON.parse((await readFileAsync(filePath)).toString()); - if (!localeMessages.formats) { - throw 'Locale file should contain formats object.'; + if (!localizedMessages.formats) { + throw new Error( + `Error in ${normalizedFilePath}:\nLocale file should contain formats object.` + ); } - try { - verifyMessages(localeMessages, defaultMessagesMap); - } catch (error) { - throw new Error(`Error in ${normalizePath(entry)}: -${error.message || error}`); - } + const localizedMessagesMap = new Map( + Object.entries(localizedMessages).filter(([key]) => key !== 'formats') + ); + + verifyMessages(localizedMessagesMap, defaultMessagesMap, normalizedFilePath); - const fileName = path.basename(entry); - const messagesByPluginMap = new Map(); - const messagesEntries = Object.entries(localeMessages).filter(([id]) => id !== 'formats'); + const localizedMessagesByNamespace = new Map(); + const knownPaths = Object.keys(paths); - for (const [messageId, messageValue] of messagesEntries) { - const namespace = Object.keys(paths).find(key => messageId.startsWith(`${key}.`)); + for (const [messageId, messageValue] of localizedMessagesMap) { + const namespace = knownPaths.find(key => messageId.startsWith(`${key}.`)); if (!namespace) { - throw new Error(`Unknown namespace in id ${messageId} in ${normalizePath(entry)}.`); + throw new Error(`Error in ${normalizedFilePath}:\nUnknown namespace in id ${messageId}.`); } - if (messagesByPluginMap.has(namespace)) { - messagesByPluginMap.get(namespace)[messageId] = messageValue; - } else { - messagesByPluginMap.set(namespace, { - formats: localeMessages.formats, - [messageId]: messageValue, + if (!localizedMessagesByNamespace.has(namespace)) { + localizedMessagesByNamespace.set(namespace, { + formats: localizedMessages.formats, }); } - } - for (const [namespace, messages] of messagesByPluginMap) { - const pluginPath = paths[namespace]; + localizedMessagesByNamespace.get(namespace)[messageId] = messageValue; + } + for (const [namespace, messages] of localizedMessagesByNamespace) { await writeFileAsync( - path.resolve(pluginPath, 'translations', fileName), + path.resolve(paths[namespace], 'translations', path.basename(filePath)), JSON.stringify(messages, undefined, 2) ); } diff --git a/src/dev/i18n/integrate_locale_files.test.js b/src/dev/i18n/integrate_locale_files.test.js index aaf2c6855bb57..3649ab94aede4 100644 --- a/src/dev/i18n/integrate_locale_files.test.js +++ b/src/dev/i18n/integrate_locale_files.test.js @@ -24,11 +24,18 @@ import { normalizePath } from './utils'; const localesPath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files'); -const defaultMessagesMap = new Map([ - ['plugin-1.message-id-1', 'Message text 1'], - ['plugin-1.message-id-2', 'Message text 2'], - ['plugin-2.message-id', 'Message text'], -]); +jest.mock('./extract_default_translations.js', () => ({ + getDefaultMessagesMap: () => { + return new Map([ + ['plugin-1.message-id-1', 'Message text 1'], + ['plugin-1.message-id-2', 'Message text 2'], + ['plugin-2.message-id', 'Message text'], + ]); + }, +})); + +const { getDefaultMessagesMap } = require('./extract_default_translations.js'); +const defaultMessagesMap = getDefaultMessagesMap(); jest.mock('../../../.i18nrc.json', () => ({ paths: { @@ -41,46 +48,62 @@ jest.mock('../../../.i18nrc.json', () => ({ const utils = require('./utils'); utils.writeFileAsync = jest.fn(); -describe('dev/i18n/check_locale_files', () => { +describe('dev/i18n/integrate_locale_files', () => { describe('verifyMessages', () => { - it('validates locale messages object', () => { - const localeMessages = { - formats: {}, - 'plugin-1.message-id-1': 'Translated text 1', - 'plugin-1.message-id-2': 'Translated text 2', - 'plugin-2.message-id': 'Translated text', - }; - - expect(() => verifyMessages(localeMessages, defaultMessagesMap)).not.toThrow(); + test('validates locale messages object', () => { + const localizedMessagesMap = new Map([ + ['plugin-1.message-id-1', 'Translated text 1'], + ['plugin-1.message-id-2', 'Translated text 2'], + ['plugin-2.message-id', 'Translated text'], + ]); + + expect(() => + verifyMessages(localizedMessagesMap, defaultMessagesMap, 'translations/fr.json') + ).not.toThrow(); }); - it('throws an error for unused id and missing id', () => { - const localeMessagesWithMissingMessage = { - formats: {}, - 'plugin-1.message-id-1': 'Translated text 1', - 'plugin-2.message-id': 'Translated text', - }; - - const localeMessagesWithUnusedMessage = { - formats: {}, - 'plugin-1.message-id-1': 'Translated text 1', - 'plugin-1.message-id-2': 'Translated text 2', - 'plugin-1.message-id-3': 'Translated text 3', - 'plugin-2.message-id': 'Translated text', - }; + test('throws an error for unused id and missing id', () => { + const localizedMessagesMapWithMissingMessage = new Map([ + ['plugin-1.message-id-1', 'Translated text 1'], + ['plugin-2.message-id', 'Translated text'], + ]); + const localizedMessagesMapWithUnusedMessage = new Map([ + ['plugin-1.message-id-1', 'Translated text 1'], + ['plugin-1.message-id-2', 'Translated text 2'], + ['plugin-1.message-id-3', 'Translated text 3'], + ['plugin-2.message-id', 'Translated text'], + ]); + + const localizedMessagesMapWithIdTypo = new Map([ + ['plugin-1.message-id-1', 'Message text 1'], + ['plugin-1.message-id-2', 'Message text 2'], + ['plugin-2.message', 'Message text'], + ]); + + expect(() => + verifyMessages( + localizedMessagesMapWithMissingMessage, + defaultMessagesMap, + 'translations/fr.json' + ) + ).toThrowErrorMatchingSnapshot(); expect(() => - verifyMessages(localeMessagesWithMissingMessage, defaultMessagesMap) + verifyMessages( + localizedMessagesMapWithUnusedMessage, + defaultMessagesMap, + 'translations/fr.json' + ) ).toThrowErrorMatchingSnapshot(); expect(() => - verifyMessages(localeMessagesWithUnusedMessage, defaultMessagesMap) + verifyMessages(localizedMessagesMapWithIdTypo, defaultMessagesMap, 'translations/fr.json') ).toThrowErrorMatchingSnapshot(); }); }); describe('integrateLocaleFiles', () => { - it('splits locale file by plugins and moves it to plugins folders', async () => { - await integrateLocaleFiles(localesPath, defaultMessagesMap); + test('splits locale file by plugins and writes them into the right folders', async () => { + await integrateLocaleFiles(localesPath); const [[path1, json1], [path2, json2]] = utils.writeFileAsync.mock.calls; diff --git a/src/dev/run_integrate_locale_files.js b/src/dev/run_integrate_locale_files.js index fc589c1b4e4fe..7e55aff7b14b0 100644 --- a/src/dev/run_integrate_locale_files.js +++ b/src/dev/run_integrate_locale_files.js @@ -17,20 +17,13 @@ * under the License. */ -import yargs from 'yargs'; - import { run } from './run'; import { integrateLocaleFiles } from './i18n/integrate_locale_files'; -import { getDefaultMessagesMap } from './i18n/extract_default_translations'; - -run(async () => { - const { argv } = yargs.option('path', { - demandOption: true, - describe: 'Path to locale files directory', - type: 'string', - }); - const defaultMessagesMap = await getDefaultMessagesMap(['.']); +run(async ({ flags: { path } }) => { + if (!path) { + throw new Error(`--path option isn't provided.`); + } - await integrateLocaleFiles(argv.path, defaultMessagesMap); + await integrateLocaleFiles(path); }); From 10c9022e13da9396a6b3c82e1a5d29a485043837 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Wed, 29 Aug 2018 12:57:12 +0300 Subject: [PATCH 13/24] Add mkdir for translations folders --- .../__snapshots__/integrate_locale_files.test.js.snap | 7 +++++++ src/dev/i18n/integrate_locale_files.js | 11 +++++++++-- src/dev/i18n/integrate_locale_files.test.js | 3 +++ src/dev/i18n/utils.js | 2 ++ 4 files changed, 21 insertions(+), 2 deletions(-) diff --git a/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap index 99ab69b542ad8..2789e1238920f 100644 --- a/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap +++ b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap @@ -131,6 +131,13 @@ Array [ ] `; +exports[`dev/i18n/integrate_locale_files integrateLocaleFiles splits locale file by plugins and writes them into the right folders 3`] = ` +Array [ + "src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1/translations", + "src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_2/translations", +] +`; + exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 1`] = ` "Error in translations/fr.json: Missing translations: diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index 5eb8c93e52875..c7e56824eec9e 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -19,7 +19,7 @@ import path from 'path'; -import { difference, globAsync, normalizePath, readFileAsync, writeFileAsync } from './utils'; +import { difference, globAsync, normalizePath, readFileAsync, writeFileAsync, accessAsync, makeDirAsync } from './utils'; import { paths } from '../../../.i18nrc.json'; import { getDefaultMessagesMap } from './extract_default_translations'; @@ -91,8 +91,15 @@ export async function integrateLocaleFiles(localesPath) { } for (const [namespace, messages] of localizedMessagesByNamespace) { + const destPath = paths[namespace]; + try { + await accessAsync(path.resolve(destPath, 'translations')); + } catch (_) { + await makeDirAsync(path.resolve(destPath, 'translations')); + } + await writeFileAsync( - path.resolve(paths[namespace], 'translations', path.basename(filePath)), + path.resolve(destPath, 'translations', path.basename(filePath)), JSON.stringify(messages, undefined, 2) ); } diff --git a/src/dev/i18n/integrate_locale_files.test.js b/src/dev/i18n/integrate_locale_files.test.js index 3649ab94aede4..bb28dedfaf2ff 100644 --- a/src/dev/i18n/integrate_locale_files.test.js +++ b/src/dev/i18n/integrate_locale_files.test.js @@ -47,6 +47,7 @@ jest.mock('../../../.i18nrc.json', () => ({ const utils = require('./utils'); utils.writeFileAsync = jest.fn(); +utils.makeDirAsync = jest.fn(); describe('dev/i18n/integrate_locale_files', () => { describe('verifyMessages', () => { @@ -106,9 +107,11 @@ describe('dev/i18n/integrate_locale_files', () => { await integrateLocaleFiles(localesPath); const [[path1, json1], [path2, json2]] = utils.writeFileAsync.mock.calls; + const [[dirPath1], [dirPath2]] = utils.makeDirAsync.mock.calls; expect([normalizePath(path1), json1]).toMatchSnapshot(); expect([normalizePath(path2), json2]).toMatchSnapshot(); + expect([normalizePath(dirPath1), normalizePath(dirPath2)]).toMatchSnapshot(); }); }); }); diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 83eebba3b8b9b..c3398a548e2e3 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -36,6 +36,8 @@ const LINE_BREAK_REGEX = /\n/g; export const readFileAsync = promisify(fs.readFile); export const writeFileAsync = promisify(fs.writeFile); +export const makeDirAsync = promisify(fs.mkdir); +export const accessAsync = promisify(fs.access); export const globAsync = promisify(glob); export function normalizePath(inputPath) { From 83ef98450a445ef19941bb79cd6d65b7c6b667c8 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Fri, 7 Sep 2018 17:02:25 +0300 Subject: [PATCH 14/24] Fix wrong variable name --- src/dev/i18n/integrate_locale_files.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index c7e56824eec9e..567558c467b14 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -19,7 +19,15 @@ import path from 'path'; -import { difference, globAsync, normalizePath, readFileAsync, writeFileAsync, accessAsync, makeDirAsync } from './utils'; +import { + difference, + globAsync, + normalizePath, + readFileAsync, + writeFileAsync, + accessAsync, + makeDirAsync, +} from './utils'; import { paths } from '../../../.i18nrc.json'; import { getDefaultMessagesMap } from './extract_default_translations'; @@ -72,10 +80,10 @@ export async function integrateLocaleFiles(localesPath) { verifyMessages(localizedMessagesMap, defaultMessagesMap, normalizedFilePath); const localizedMessagesByNamespace = new Map(); - const knownPaths = Object.keys(paths); + const knownNamespaces = Object.keys(paths); for (const [messageId, messageValue] of localizedMessagesMap) { - const namespace = knownPaths.find(key => messageId.startsWith(`${key}.`)); + const namespace = knownNamespaces.find(key => messageId.startsWith(`${key}.`)); if (!namespace) { throw new Error(`Error in ${normalizedFilePath}:\nUnknown namespace in id ${messageId}.`); From 4acebd6c20161086612e1254f27dd7a540e8697e Mon Sep 17 00:00:00 2001 From: maryia-lapata Date: Wed, 12 Sep 2018 18:20:48 +0300 Subject: [PATCH 15/24] Rename script --- scripts/{integrate_locale_files.js => i18n_integrate.js} | 2 +- .../{run_integrate_locale_files.js => run_i18n_integrate.js} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename scripts/{integrate_locale_files.js => i18n_integrate.js} (94%) rename src/dev/{run_integrate_locale_files.js => run_i18n_integrate.js} (100%) diff --git a/scripts/integrate_locale_files.js b/scripts/i18n_integrate.js similarity index 94% rename from scripts/integrate_locale_files.js rename to scripts/i18n_integrate.js index 8f33c0bb34cb7..9fbdf424682b0 100644 --- a/scripts/integrate_locale_files.js +++ b/scripts/i18n_integrate.js @@ -18,4 +18,4 @@ */ require('../src/setup_node_env'); -require('../src/dev/run_integrate_locale_files'); +require('../src/dev/run_i18n_integrate'); diff --git a/src/dev/run_integrate_locale_files.js b/src/dev/run_i18n_integrate.js similarity index 100% rename from src/dev/run_integrate_locale_files.js rename to src/dev/run_i18n_integrate.js From e1804c48267ee6d25d65c89fdbf3dba498800177 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Thu, 27 Sep 2018 13:14:46 +0300 Subject: [PATCH 16/24] Resolve comments --- src/dev/i18n/extract_default_translations.js | 7 +- src/dev/i18n/integrate_locale_files.js | 91 ++++++++------------ src/dev/i18n/serializers/json.js | 6 +- src/dev/i18n/serializers/json5.js | 8 +- src/dev/i18n/utils.js | 6 -- src/dev/run_i18n_integrate.js | 18 ++-- 6 files changed, 60 insertions(+), 76 deletions(-) diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 379593cf42d0a..235d80825ad1f 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -19,6 +19,7 @@ import path from 'path'; import chalk from 'chalk'; +import normalize from 'normalize-path'; import { extractHtmlMessages, @@ -26,11 +27,15 @@ import { extractPugMessages, extractHandlebarsMessages, } from './extractors'; -import { globAsync, normalizePath, readFileAsync } from './utils'; +import { globAsync, readFileAsync } from './utils'; import { paths, exclude } from '../../../.i18nrc.json'; import { createFailError, isFailError } from '../run'; +function normalizePath(inputPath) { + return normalize(path.relative('.', inputPath)); +} + function addMessageToMap(targetMap, key, value) { const existingValue = targetMap.get(key); diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index 567558c467b14..6c1822c6b21d2 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -19,19 +19,13 @@ import path from 'path'; -import { - difference, - globAsync, - normalizePath, - readFileAsync, - writeFileAsync, - accessAsync, - makeDirAsync, -} from './utils'; +import { difference, readFileAsync, writeFileAsync, accessAsync, makeDirAsync } from './utils'; import { paths } from '../../../.i18nrc.json'; import { getDefaultMessagesMap } from './extract_default_translations'; +import { createFailError } from '../run'; +import { serializeToJson } from './serializers/json'; -export function verifyMessages(localizedMessagesMap, defaultMessagesMap, filePath) { +export function verifyMessages(localizedMessagesMap, defaultMessagesMap) { let errorMessage = ''; const defaultMessagesIds = [...defaultMessagesMap.keys()]; @@ -49,67 +43,52 @@ export function verifyMessages(localizedMessagesMap, defaultMessagesMap, filePat } if (errorMessage) { - throw new Error(`Error in ${filePath}:${errorMessage}.`); + throw createFailError(`${errorMessage}`); } } -export async function integrateLocaleFiles(localesPath) { +export async function integrateLocaleFiles(filePath, log) { const defaultMessagesMap = await getDefaultMessagesMap(['.']); - const globOptions = { - cwd: path.resolve(localesPath), - }; + const localizedMessages = JSON.parse((await readFileAsync(filePath)).toString()); - const filePaths = (await globAsync('./*.json', globOptions)).map(filePath => - path.resolve(localesPath, filePath) - ); + if (!localizedMessages.formats) { + throw createFailError(`Locale file should contain formats object.`); + } - for (const filePath of filePaths) { - const normalizedFilePath = normalizePath(filePath); - const localizedMessages = JSON.parse((await readFileAsync(filePath)).toString()); + const localizedMessagesMap = new Map( + Object.entries(localizedMessages).filter(([key]) => key !== 'formats') + ); - if (!localizedMessages.formats) { - throw new Error( - `Error in ${normalizedFilePath}:\nLocale file should contain formats object.` - ); - } + verifyMessages(localizedMessagesMap, defaultMessagesMap); - const localizedMessagesMap = new Map( - Object.entries(localizedMessages).filter(([key]) => key !== 'formats') - ); + const localizedMessagesByNamespace = new Map(); + const knownNamespaces = Object.keys(paths); - verifyMessages(localizedMessagesMap, defaultMessagesMap, normalizedFilePath); + for (const [messageId, messageValue] of localizedMessagesMap) { + const namespace = knownNamespaces.find(key => messageId.startsWith(`${key}.`)); - const localizedMessagesByNamespace = new Map(); - const knownNamespaces = Object.keys(paths); + if (!namespace) { + throw createFailError(`Unknown namespace in id ${messageId}.`); + } - for (const [messageId, messageValue] of localizedMessagesMap) { - const namespace = knownNamespaces.find(key => messageId.startsWith(`${key}.`)); + if (!localizedMessagesByNamespace.has(namespace)) { + localizedMessagesByNamespace.set(namespace, {}); + } - if (!namespace) { - throw new Error(`Error in ${normalizedFilePath}:\nUnknown namespace in id ${messageId}.`); - } + localizedMessagesByNamespace.get(namespace)[messageId] = { message: messageValue }; + } - if (!localizedMessagesByNamespace.has(namespace)) { - localizedMessagesByNamespace.set(namespace, { - formats: localizedMessages.formats, - }); - } + for (const [namespace, messages] of localizedMessagesByNamespace) { + const destPath = paths[namespace]; - localizedMessagesByNamespace.get(namespace)[messageId] = messageValue; + try { + await accessAsync(path.resolve(destPath, 'translations')); + } catch (_) { + await makeDirAsync(path.resolve(destPath, 'translations')); } - for (const [namespace, messages] of localizedMessagesByNamespace) { - const destPath = paths[namespace]; - try { - await accessAsync(path.resolve(destPath, 'translations')); - } catch (_) { - await makeDirAsync(path.resolve(destPath, 'translations')); - } - - await writeFileAsync( - path.resolve(destPath, 'translations', path.basename(filePath)), - JSON.stringify(messages, undefined, 2) - ); - } + const writePath = path.resolve(destPath, 'translations', path.basename(filePath)); + await writeFileAsync(writePath, serializeToJson(messages, localizedMessages.formats)); + log.success(`Translations have been integrated to ${path.relative('./', writePath)}`); } } diff --git a/src/dev/i18n/serializers/json.js b/src/dev/i18n/serializers/json.js index 8e615af1e81d3..b7cb927a1ad30 100644 --- a/src/dev/i18n/serializers/json.js +++ b/src/dev/i18n/serializers/json.js @@ -19,10 +19,10 @@ import { i18n } from '@kbn/i18n'; -export function serializeToJson(defaultMessages) { - const resultJsonObject = { formats: i18n.formats }; +export function serializeToJson(messages, formats = i18n.formats) { + const resultJsonObject = { formats }; - for (const [mapKey, mapValue] of defaultMessages) { + for (const [mapKey, mapValue] of Array.isArray(messages) ? messages : Object.entries(messages)) { if (mapValue.context) { resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; } else { diff --git a/src/dev/i18n/serializers/json5.js b/src/dev/i18n/serializers/json5.js index 0156053d5f43b..aa034f74e7f8d 100644 --- a/src/dev/i18n/serializers/json5.js +++ b/src/dev/i18n/serializers/json5.js @@ -22,13 +22,11 @@ import { i18n } from '@kbn/i18n'; const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; -export function serializeToJson5(defaultMessages) { +export function serializeToJson5(messages, formats = i18n.formats) { // .slice(0, -1): remove closing curly brace from json to append messages - let jsonBuffer = Buffer.from( - JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) - ); + let jsonBuffer = Buffer.from(JSON5.stringify({ formats }, { quote: `'`, space: 2 }).slice(0, -1)); - for (const [mapKey, mapValue] of defaultMessages) { + for (const [mapKey, mapValue] of Array.isArray(messages) ? messages : Object.entries(messages)) { const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); const formattedContext = mapValue.context ? mapValue.context.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 56236b4be4d15..e2918cdcbd403 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -27,8 +27,6 @@ import { import fs from 'fs'; import glob from 'glob'; import { promisify } from 'util'; -import normalize from 'normalize-path'; -import path from 'path'; import chalk from 'chalk'; const ESCAPE_LINE_BREAK_REGEX = /(? !right.includes(value)); } diff --git a/src/dev/run_i18n_integrate.js b/src/dev/run_i18n_integrate.js index 7e55aff7b14b0..5834613ab8f6a 100644 --- a/src/dev/run_i18n_integrate.js +++ b/src/dev/run_i18n_integrate.js @@ -17,13 +17,21 @@ * under the License. */ -import { run } from './run'; +import chalk from 'chalk'; + +import { createFailError, run } from './run'; import { integrateLocaleFiles } from './i18n/integrate_locale_files'; -run(async ({ flags: { path } }) => { - if (!path) { - throw new Error(`--path option isn't provided.`); +run(async ({ flags: { path }, log }) => { + if (!path || typeof path === 'boolean') { + throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} --path option isn't provided.`); + } + + if (Array.isArray(path)) { + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} --path value should be a single string` + ); } - await integrateLocaleFiles(path); + await integrateLocaleFiles(path, log); }); From e16abcde5b6fe0662b0498cceccddeb6d400bfee Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Fri, 19 Oct 2018 13:12:42 +0300 Subject: [PATCH 17/24] Fix tests --- .../integrate_locale_files.test.js.snap | 12 ++++++------ src/dev/i18n/__snapshots__/utils.test.js.snap | 2 ++ src/dev/i18n/extract_default_translations.js | 7 +------ src/dev/i18n/integrate_locale_files.js | 11 +++++++++-- src/dev/i18n/integrate_locale_files.test.js | 5 +++-- src/dev/i18n/serializers/json.test.js | 2 +- src/dev/i18n/serializers/json5.test.js | 2 +- src/dev/i18n/utils.js | 6 ++++++ src/dev/i18n/utils.test.js | 5 +++++ 9 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap index 2789e1238920f..157524e955cdd 100644 --- a/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap +++ b/src/dev/i18n/__snapshots__/integrate_locale_files.test.js.snap @@ -139,21 +139,21 @@ Array [ `; exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 1`] = ` -"Error in translations/fr.json: +" Missing translations: -plugin-1.message-id-2." +plugin-1.message-id-2" `; exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 2`] = ` -"Error in translations/fr.json: +" Unused translations: -plugin-1.message-id-3." +plugin-1.message-id-3" `; exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 3`] = ` -"Error in translations/fr.json: +" Unused translations: plugin-2.message Missing translations: -plugin-2.message-id." +plugin-2.message-id" `; diff --git a/src/dev/i18n/__snapshots__/utils.test.js.snap b/src/dev/i18n/__snapshots__/utils.test.js.snap index 8f92d9b34efd6..8645942cf122d 100644 --- a/src/dev/i18n/__snapshots__/utils.test.js.snap +++ b/src/dev/i18n/__snapshots__/utils.test.js.snap @@ -9,6 +9,8 @@ exports[`i18n utils should create verbose parser error message 1`] = ` " `; +exports[`i18n utils should normalizePath 1`] = `"src/dev/i18n"`; + exports[`i18n utils should not escape linebreaks 1`] = ` "Text with diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 235d80825ad1f..116e4a609bf46 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -19,7 +19,6 @@ import path from 'path'; import chalk from 'chalk'; -import normalize from 'normalize-path'; import { extractHtmlMessages, @@ -27,15 +26,11 @@ import { extractPugMessages, extractHandlebarsMessages, } from './extractors'; -import { globAsync, readFileAsync } from './utils'; +import { globAsync, readFileAsync, normalizePath } from './utils'; import { paths, exclude } from '../../../.i18nrc.json'; import { createFailError, isFailError } from '../run'; -function normalizePath(inputPath) { - return normalize(path.relative('.', inputPath)); -} - function addMessageToMap(targetMap, key, value) { const existingValue = targetMap.get(key); diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index 6c1822c6b21d2..ee667bfe3317b 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -19,7 +19,14 @@ import path from 'path'; -import { difference, readFileAsync, writeFileAsync, accessAsync, makeDirAsync } from './utils'; +import { + difference, + readFileAsync, + writeFileAsync, + accessAsync, + makeDirAsync, + normalizePath, +} from './utils'; import { paths } from '../../../.i18nrc.json'; import { getDefaultMessagesMap } from './extract_default_translations'; import { createFailError } from '../run'; @@ -89,6 +96,6 @@ export async function integrateLocaleFiles(filePath, log) { const writePath = path.resolve(destPath, 'translations', path.basename(filePath)); await writeFileAsync(writePath, serializeToJson(messages, localizedMessages.formats)); - log.success(`Translations have been integrated to ${path.relative('./', writePath)}`); + log.success(`Translations have been integrated to ${normalizePath(writePath)}`); } } diff --git a/src/dev/i18n/integrate_locale_files.test.js b/src/dev/i18n/integrate_locale_files.test.js index bb28dedfaf2ff..efd052460d7c3 100644 --- a/src/dev/i18n/integrate_locale_files.test.js +++ b/src/dev/i18n/integrate_locale_files.test.js @@ -22,7 +22,7 @@ import path from 'path'; import { verifyMessages, integrateLocaleFiles } from './integrate_locale_files'; import { normalizePath } from './utils'; -const localesPath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files'); +const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json'); jest.mock('./extract_default_translations.js', () => ({ getDefaultMessagesMap: () => { @@ -104,7 +104,8 @@ describe('dev/i18n/integrate_locale_files', () => { describe('integrateLocaleFiles', () => { test('splits locale file by plugins and writes them into the right folders', async () => { - await integrateLocaleFiles(localesPath); + const success = jest.fn(); + await integrateLocaleFiles(localePath, { success }); const [[path1, json1], [path2, json2]] = utils.writeFileAsync.mock.calls; const [[dirPath1], [dirPath2]] = utils.makeDirAsync.mock.calls; diff --git a/src/dev/i18n/serializers/json.test.js b/src/dev/i18n/serializers/json.test.js index 9486a999fe7db..fb66175d56c8d 100644 --- a/src/dev/i18n/serializers/json.test.js +++ b/src/dev/i18n/serializers/json.test.js @@ -32,6 +32,6 @@ describe('dev/i18n/serializers/json', () => { ], ]); - expect(serializeToJson(messages)).toMatchSnapshot(); + expect(serializeToJson([...messages])).toMatchSnapshot(); }); }); diff --git a/src/dev/i18n/serializers/json5.test.js b/src/dev/i18n/serializers/json5.test.js index 90be880bd32a3..0db11443409ac 100644 --- a/src/dev/i18n/serializers/json5.test.js +++ b/src/dev/i18n/serializers/json5.test.js @@ -37,6 +37,6 @@ describe('dev/i18n/serializers/json5', () => { ], ]); - expect(serializeToJson5(messages).toString()).toMatchSnapshot(); + expect(serializeToJson5([...messages]).toString()).toMatchSnapshot(); }); }); diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index e2918cdcbd403..56236b4be4d15 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -27,6 +27,8 @@ import { import fs from 'fs'; import glob from 'glob'; import { promisify } from 'util'; +import normalize from 'normalize-path'; +import path from 'path'; import chalk from 'chalk'; const ESCAPE_LINE_BREAK_REGEX = /(? !right.includes(value)); } diff --git a/src/dev/i18n/utils.test.js b/src/dev/i18n/utils.test.js index 1654310071b1c..faeea265f17e9 100644 --- a/src/dev/i18n/utils.test.js +++ b/src/dev/i18n/utils.test.js @@ -26,6 +26,7 @@ import { traverseNodes, formatJSString, createParserErrorMessage, + normalizePath, } from './utils'; const i18nTranslateSources = ['i18n', 'i18n.translate'].map( @@ -104,4 +105,8 @@ describe('i18n utils', () => { expect(createParserErrorMessage(content, error)).toMatchSnapshot(); } }); + + test('should normalizePath', () => { + expect(normalizePath(__dirname)).toMatchSnapshot(); + }); }); From 139b6ba77f7b9e19688bf1a642bc816db08854db Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Fri, 2 Nov 2018 11:53:01 +0300 Subject: [PATCH 18/24] Fix locale file format bug --- src/dev/i18n/__fixtures__/integrate_locale_files/fr.json | 8 +++++--- src/dev/i18n/integrate_locale_files.js | 4 +--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/dev/i18n/__fixtures__/integrate_locale_files/fr.json b/src/dev/i18n/__fixtures__/integrate_locale_files/fr.json index fe86b218f282c..72312c447961e 100644 --- a/src/dev/i18n/__fixtures__/integrate_locale_files/fr.json +++ b/src/dev/i18n/__fixtures__/integrate_locale_files/fr.json @@ -55,7 +55,9 @@ } } }, - "plugin-1.message-id-1": "Translated text 1", - "plugin-1.message-id-2": "Translated text 2", - "plugin-2.message-id": "Translated text" + "messages": { + "plugin-1.message-id-1": "Translated text 1", + "plugin-1.message-id-2": "Translated text 2", + "plugin-2.message-id": "Translated text" + } } diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index ee667bfe3317b..64892d7a1af2c 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -62,9 +62,7 @@ export async function integrateLocaleFiles(filePath, log) { throw createFailError(`Locale file should contain formats object.`); } - const localizedMessagesMap = new Map( - Object.entries(localizedMessages).filter(([key]) => key !== 'formats') - ); + const localizedMessagesMap = new Map(Object.entries(localizedMessages.messages)); verifyMessages(localizedMessagesMap, defaultMessagesMap); From c8f2643756aeda8b20cf1449b064c7f8a008b238 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Wed, 19 Dec 2018 22:05:16 +0300 Subject: [PATCH 19/24] Resolve comments --- src/dev/i18n/integrate_locale_files.js | 53 ++++++++++++--------- src/dev/i18n/integrate_locale_files.test.js | 29 +++++------ src/dev/i18n/serializers/json.js | 2 +- src/dev/i18n/serializers/json.test.js | 6 +-- src/dev/i18n/serializers/json5.js | 2 +- src/dev/i18n/serializers/json5.test.js | 6 +-- src/dev/run_i18n_integrate.js | 2 +- 7 files changed, 52 insertions(+), 48 deletions(-) diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index 64892d7a1af2c..f90c00575e6ba 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -39,33 +39,21 @@ export function verifyMessages(localizedMessagesMap, defaultMessagesMap) { const localizedMessagesIds = [...localizedMessagesMap.keys()]; const unusedTranslations = difference(localizedMessagesIds, defaultMessagesIds); - const missingTranslations = difference(defaultMessagesIds, localizedMessagesIds); - if (unusedTranslations.length > 0) { errorMessage += `\nUnused translations:\n${unusedTranslations.join(', ')}`; } + const missingTranslations = difference(defaultMessagesIds, localizedMessagesIds); if (missingTranslations.length > 0) { errorMessage += `\nMissing translations:\n${missingTranslations.join(', ')}`; } if (errorMessage) { - throw createFailError(`${errorMessage}`); + throw createFailError(errorMessage); } } -export async function integrateLocaleFiles(filePath, log) { - const defaultMessagesMap = await getDefaultMessagesMap(['.']); - const localizedMessages = JSON.parse((await readFileAsync(filePath)).toString()); - - if (!localizedMessages.formats) { - throw createFailError(`Locale file should contain formats object.`); - } - - const localizedMessagesMap = new Map(Object.entries(localizedMessages.messages)); - - verifyMessages(localizedMessagesMap, defaultMessagesMap); - +function groupMessagesByNamespace(localizedMessagesMap) { const localizedMessagesByNamespace = new Map(); const knownNamespaces = Object.keys(paths); @@ -77,23 +65,44 @@ export async function integrateLocaleFiles(filePath, log) { } if (!localizedMessagesByNamespace.has(namespace)) { - localizedMessagesByNamespace.set(namespace, {}); + localizedMessagesByNamespace.set(namespace, []); } - localizedMessagesByNamespace.get(namespace)[messageId] = { message: messageValue }; + localizedMessagesByNamespace.get(namespace).push([messageId, { message: messageValue }]); } + return localizedMessagesByNamespace; +} + +async function writeMessages(localizedMessagesByNamespace, fileName, formats, log) { for (const [namespace, messages] of localizedMessagesByNamespace) { - const destPath = paths[namespace]; + const destPath = path.resolve(paths[namespace], 'translations'); try { - await accessAsync(path.resolve(destPath, 'translations')); + await accessAsync(destPath); } catch (_) { - await makeDirAsync(path.resolve(destPath, 'translations')); + await makeDirAsync(destPath); } - const writePath = path.resolve(destPath, 'translations', path.basename(filePath)); - await writeFileAsync(writePath, serializeToJson(messages, localizedMessages.formats)); + const writePath = path.resolve(destPath, fileName); + await writeFileAsync(writePath, serializeToJson(messages, formats)); log.success(`Translations have been integrated to ${normalizePath(writePath)}`); } } + +export async function integrateLocaleFiles(filePath, log) { + const defaultMessagesMap = await getDefaultMessagesMap(['.']); + const localizedMessages = JSON.parse((await readFileAsync(filePath)).toString()); + + if (!localizedMessages.formats) { + throw createFailError(`Locale file should contain formats object.`); + } + + const localizedMessagesMap = new Map(Object.entries(localizedMessages.messages)); + verifyMessages(localizedMessagesMap, defaultMessagesMap); + + // use basename of filePath to write the same locale name as the source file has + const fileName = path.basename(filePath); + const localizedMessagesByNamespace = groupMessagesByNamespace(localizedMessagesMap); + await writeMessages(localizedMessagesByNamespace, fileName, localizedMessages.formats, log); +} diff --git a/src/dev/i18n/integrate_locale_files.test.js b/src/dev/i18n/integrate_locale_files.test.js index efd052460d7c3..5d0872431efcb 100644 --- a/src/dev/i18n/integrate_locale_files.test.js +++ b/src/dev/i18n/integrate_locale_files.test.js @@ -24,19 +24,16 @@ import { normalizePath } from './utils'; const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json'); +const mockDefaultMessagesMap = new Map([ + ['plugin-1.message-id-1', 'Message text 1'], + ['plugin-1.message-id-2', 'Message text 2'], + ['plugin-2.message-id', 'Message text'], +]); + jest.mock('./extract_default_translations.js', () => ({ - getDefaultMessagesMap: () => { - return new Map([ - ['plugin-1.message-id-1', 'Message text 1'], - ['plugin-1.message-id-2', 'Message text 2'], - ['plugin-2.message-id', 'Message text'], - ]); - }, + getDefaultMessagesMap: () => mockDefaultMessagesMap, })); -const { getDefaultMessagesMap } = require('./extract_default_translations.js'); -const defaultMessagesMap = getDefaultMessagesMap(); - jest.mock('../../../.i18nrc.json', () => ({ paths: { 'plugin-1': 'src/dev/i18n/__fixtures__/integrate_locale_files/test_plugin_1', @@ -51,16 +48,14 @@ utils.makeDirAsync = jest.fn(); describe('dev/i18n/integrate_locale_files', () => { describe('verifyMessages', () => { - test('validates locale messages object', () => { + test('validates localized messages', () => { const localizedMessagesMap = new Map([ ['plugin-1.message-id-1', 'Translated text 1'], ['plugin-1.message-id-2', 'Translated text 2'], ['plugin-2.message-id', 'Translated text'], ]); - expect(() => - verifyMessages(localizedMessagesMap, defaultMessagesMap, 'translations/fr.json') - ).not.toThrow(); + expect(() => verifyMessages(localizedMessagesMap, mockDefaultMessagesMap)).not.toThrow(); }); test('throws an error for unused id and missing id', () => { @@ -85,19 +80,19 @@ describe('dev/i18n/integrate_locale_files', () => { expect(() => verifyMessages( localizedMessagesMapWithMissingMessage, - defaultMessagesMap, + mockDefaultMessagesMap, 'translations/fr.json' ) ).toThrowErrorMatchingSnapshot(); expect(() => verifyMessages( localizedMessagesMapWithUnusedMessage, - defaultMessagesMap, + mockDefaultMessagesMap, 'translations/fr.json' ) ).toThrowErrorMatchingSnapshot(); expect(() => - verifyMessages(localizedMessagesMapWithIdTypo, defaultMessagesMap, 'translations/fr.json') + verifyMessages(localizedMessagesMapWithIdTypo, mockDefaultMessagesMap) ).toThrowErrorMatchingSnapshot(); }); }); diff --git a/src/dev/i18n/serializers/json.js b/src/dev/i18n/serializers/json.js index 5c12ba9f1c0de..edf51e66ac651 100644 --- a/src/dev/i18n/serializers/json.js +++ b/src/dev/i18n/serializers/json.js @@ -22,7 +22,7 @@ import { i18n } from '@kbn/i18n'; export function serializeToJson(messages, formats = i18n.formats) { const resultJsonObject = { formats, messages: {} }; - for (const [mapKey, mapValue] of Array.isArray(messages) ? messages : Object.entries(messages)) { + for (const [mapKey, mapValue] of messages) { if (mapValue.description) { resultJsonObject.messages[mapKey] = { text: mapValue.message, comment: mapValue.description }; } else { diff --git a/src/dev/i18n/serializers/json.test.js b/src/dev/i18n/serializers/json.test.js index c830ba56376fd..b910b3b9fbcc3 100644 --- a/src/dev/i18n/serializers/json.test.js +++ b/src/dev/i18n/serializers/json.test.js @@ -21,7 +21,7 @@ import { serializeToJson } from './json'; describe('dev/i18n/serializers/json', () => { test('should serialize default messages to JSON', () => { - const messages = new Map([ + const messages = [ ['plugin1.message.id-1', { message: 'Message text 1 ' }], [ 'plugin2.message.id-2', @@ -30,8 +30,8 @@ describe('dev/i18n/serializers/json', () => { description: 'Message description', }, ], - ]); + ]; - expect(serializeToJson([...messages])).toMatchSnapshot(); + expect(serializeToJson(messages)).toMatchSnapshot(); }); }); diff --git a/src/dev/i18n/serializers/json5.js b/src/dev/i18n/serializers/json5.js index 3cdfc307f5815..1af7c56d40676 100644 --- a/src/dev/i18n/serializers/json5.js +++ b/src/dev/i18n/serializers/json5.js @@ -30,7 +30,7 @@ export function serializeToJson5(messages, formats = i18n.formats) { .concat('\n') ); - for (const [mapKey, mapValue] of Array.isArray(messages) ? messages : Object.entries(messages)) { + for (const [mapKey, mapValue] of messages) { const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); const formattedDescription = mapValue.description ? mapValue.description.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') diff --git a/src/dev/i18n/serializers/json5.test.js b/src/dev/i18n/serializers/json5.test.js index 00aa0d694d63b..6c5ece278989d 100644 --- a/src/dev/i18n/serializers/json5.test.js +++ b/src/dev/i18n/serializers/json5.test.js @@ -21,7 +21,7 @@ import { serializeToJson5 } from './json5'; describe('dev/i18n/serializers/json5', () => { test('should serialize default messages to JSON5', () => { - const messages = new Map([ + const messages = [ [ 'plugin1.message.id-1', { @@ -35,8 +35,8 @@ describe('dev/i18n/serializers/json5', () => { description: 'Message description', }, ], - ]); + ]; - expect(serializeToJson5([...messages]).toString()).toMatchSnapshot(); + expect(serializeToJson5(messages).toString()).toMatchSnapshot(); }); }); diff --git a/src/dev/run_i18n_integrate.js b/src/dev/run_i18n_integrate.js index 5834613ab8f6a..e81d37d5c1bc6 100644 --- a/src/dev/run_i18n_integrate.js +++ b/src/dev/run_i18n_integrate.js @@ -29,7 +29,7 @@ run(async ({ flags: { path }, log }) => { if (Array.isArray(path)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} --path value should be a single string` + `${chalk.white.bgRed(' I18N ERROR ')} --path should be specified only once` ); } From f6ae2128bd5ddade6bec8ade9db10203c21ddd81 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Thu, 20 Dec 2018 16:26:05 +0300 Subject: [PATCH 20/24] Fix function calls in tests --- src/dev/i18n/integrate_locale_files.test.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/dev/i18n/integrate_locale_files.test.js b/src/dev/i18n/integrate_locale_files.test.js index 5d0872431efcb..04f7b607192c0 100644 --- a/src/dev/i18n/integrate_locale_files.test.js +++ b/src/dev/i18n/integrate_locale_files.test.js @@ -78,18 +78,10 @@ describe('dev/i18n/integrate_locale_files', () => { ]); expect(() => - verifyMessages( - localizedMessagesMapWithMissingMessage, - mockDefaultMessagesMap, - 'translations/fr.json' - ) + verifyMessages(localizedMessagesMapWithMissingMessage, mockDefaultMessagesMap) ).toThrowErrorMatchingSnapshot(); expect(() => - verifyMessages( - localizedMessagesMapWithUnusedMessage, - mockDefaultMessagesMap, - 'translations/fr.json' - ) + verifyMessages(localizedMessagesMapWithUnusedMessage, mockDefaultMessagesMap) ).toThrowErrorMatchingSnapshot(); expect(() => verifyMessages(localizedMessagesMapWithIdTypo, mockDefaultMessagesMap) From cd55d1b47231f5fcad435595431fdf4beb87ac29 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Thu, 20 Dec 2018 16:27:28 +0300 Subject: [PATCH 21/24] Update snapshots --- .../__snapshots__/json.test.js.snap | 20 +++++++++++++++++++ .../__snapshots__/json5.test.js.snap | 20 +++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/src/dev/i18n/serializers/__snapshots__/json.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap index 493e09e00a1bf..d31d37c4b65cd 100644 --- a/src/dev/i18n/serializers/__snapshots__/json.test.js.snap +++ b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap @@ -56,6 +56,26 @@ exports[`dev/i18n/serializers/json should serialize default messages to JSON 1`] \\"second\\": \\"numeric\\", \\"timeZoneName\\": \\"short\\" } + }, + \\"relative\\": { + \\"years\\": { + \\"units\\": \\"year\\" + }, + \\"months\\": { + \\"units\\": \\"month\\" + }, + \\"days\\": { + \\"units\\": \\"day\\" + }, + \\"hours\\": { + \\"units\\": \\"hour\\" + }, + \\"minutes\\": { + \\"units\\": \\"minute\\" + }, + \\"seconds\\": { + \\"units\\": \\"second\\" + } } }, \\"messages\\": { diff --git a/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap index f1e948a47b109..ea6e38202e2bb 100644 --- a/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap +++ b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap @@ -57,6 +57,26 @@ exports[`dev/i18n/serializers/json5 should serialize default messages to JSON5 1 timeZoneName: 'short', }, }, + relative: { + years: { + units: 'year', + }, + months: { + units: 'month', + }, + days: { + units: 'day', + }, + hours: { + units: 'hour', + }, + minutes: { + units: 'minute', + }, + seconds: { + units: 'second', + }, + }, }, messages: { 'plugin1.message.id-1': 'Message text 1', From 389699bc0e6bc2d1a0b23760577cfc65d3c79943 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Fri, 21 Dec 2018 15:25:50 +0300 Subject: [PATCH 22/24] Fix config passing bug --- src/dev/i18n/extract_default_translations.js | 6 +++--- src/dev/i18n/integrate_locale_files.js | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 68577596159c4..fd3db8658de28 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -146,11 +146,11 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap, config) ); } -export async function getDefaultMessagesMap(paths) { +export async function getDefaultMessagesMap(inputPaths, config) { const defaultMessagesMap = new Map(); - for (const inputPath of filterPaths(paths)) { - await extractMessagesFromPathToMap(inputPath, defaultMessagesMap); + for (const inputPath of filterPaths(inputPaths, config.paths)) { + await extractMessagesFromPathToMap(inputPath, defaultMessagesMap, config); } return defaultMessagesMap; diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index f90c00575e6ba..8ce7f84a5ccdf 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -27,7 +27,7 @@ import { makeDirAsync, normalizePath, } from './utils'; -import { paths } from '../../../.i18nrc.json'; +import { paths, exclude } from '../../../.i18nrc.json'; import { getDefaultMessagesMap } from './extract_default_translations'; import { createFailError } from '../run'; import { serializeToJson } from './serializers/json'; @@ -91,7 +91,7 @@ async function writeMessages(localizedMessagesByNamespace, fileName, formats, lo } export async function integrateLocaleFiles(filePath, log) { - const defaultMessagesMap = await getDefaultMessagesMap(['.']); + const defaultMessagesMap = await getDefaultMessagesMap(['.'], { paths, exclude }); const localizedMessages = JSON.parse((await readFileAsync(filePath)).toString()); if (!localizedMessages.formats) { From ab79ea5da162c7b83f0eceb81ea8c875af692a1a Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Fri, 21 Dec 2018 18:43:54 +0300 Subject: [PATCH 23/24] Remove context comments from result json files --- src/dev/i18n/integrate_locale_files.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index 8ce7f84a5ccdf..0cbe6a285be26 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -68,7 +68,9 @@ function groupMessagesByNamespace(localizedMessagesMap) { localizedMessagesByNamespace.set(namespace, []); } - localizedMessagesByNamespace.get(namespace).push([messageId, { message: messageValue }]); + localizedMessagesByNamespace + .get(namespace) + .push([messageId, { message: messageValue.text || messageValue }]); } return localizedMessagesByNamespace; From 79fcc93748075c94ce4ccbce62e3c5005fd73c9c Mon Sep 17 00:00:00 2001 From: Pavel Date: Sat, 22 Dec 2018 11:58:43 +0300 Subject: [PATCH 24/24] add missed reporter --- src/dev/i18n/extract_default_translations.js | 4 ++-- src/dev/i18n/integrate_locale_files.js | 4 +++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index d04ff45ffd789..b0e3d60a6d7c5 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -149,11 +149,11 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap, config, ); } -export async function getDefaultMessagesMap(inputPaths, config) { +export async function getDefaultMessagesMap(inputPaths, config, reporter) { const defaultMessagesMap = new Map(); for (const inputPath of filterPaths(inputPaths, config.paths)) { - await extractMessagesFromPathToMap(inputPath, defaultMessagesMap, config); + await extractMessagesFromPathToMap(inputPath, defaultMessagesMap, config, reporter); } return defaultMessagesMap; diff --git a/src/dev/i18n/integrate_locale_files.js b/src/dev/i18n/integrate_locale_files.js index 0cbe6a285be26..fa06f6399f460 100644 --- a/src/dev/i18n/integrate_locale_files.js +++ b/src/dev/i18n/integrate_locale_files.js @@ -26,6 +26,7 @@ import { accessAsync, makeDirAsync, normalizePath, + ErrorReporter, } from './utils'; import { paths, exclude } from '../../../.i18nrc.json'; import { getDefaultMessagesMap } from './extract_default_translations'; @@ -93,7 +94,8 @@ async function writeMessages(localizedMessagesByNamespace, fileName, formats, lo } export async function integrateLocaleFiles(filePath, log) { - const defaultMessagesMap = await getDefaultMessagesMap(['.'], { paths, exclude }); + const reporter = new ErrorReporter(); + const defaultMessagesMap = await getDefaultMessagesMap(['.'], { paths, exclude }, reporter); const localizedMessages = JSON.parse((await readFileAsync(filePath)).toString()); if (!localizedMessages.formats) {