From 0f9eb7a2ba729b8485cacaabe4af8552e7121e6c Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Wed, 22 Aug 2018 18:22:02 +0300 Subject: [PATCH 1/4] Integrate main i18n tool to build process --- src/dev/run_extract_default_translations.js | 16 ++-- src/ui/ui_render/bootstrap/template.js.hbs | 2 +- src/ui/ui_render/views/ui_app.pug | 2 +- tasks/utils/i18n_verify_keys.js | 99 --------------------- tasks/verify_translations.js | 86 +++++------------- 5 files changed, 34 insertions(+), 171 deletions(-) delete mode 100644 tasks/utils/i18n_verify_keys.js diff --git a/src/dev/run_extract_default_translations.js b/src/dev/run_extract_default_translations.js index 9d5f0011d6f387..65648113dda6a1 100644 --- a/src/dev/run_extract_default_translations.js +++ b/src/dev/run_extract_default_translations.js @@ -17,13 +17,19 @@ * under the License. */ +import chalk from 'chalk'; import { run } from './run'; import { extractDefaultTranslations } from './i18n/extract_default_translations'; run(async ({ flags: { path, output, 'output-format': outputFormat } }) => { - await extractDefaultTranslations({ - paths: Array.isArray(path) ? path : [path || './'], - output, - outputFormat, - }); + try { + await extractDefaultTranslations({ + paths: Array.isArray(path) ? path : [path || './'], + output, + outputFormat, + }); + } catch (e) { + console.error(`${chalk.white.bgRed(' I18N ERROR ')} ${e.message || e}`); + process.exit(1); + } }); diff --git a/src/ui/ui_render/bootstrap/template.js.hbs b/src/ui/ui_render/bootstrap/template.js.hbs index 7ab440dac49087..ca4501a23eff10 100644 --- a/src/ui/ui_render/bootstrap/template.js.hbs +++ b/src/ui/ui_render/bootstrap/template.js.hbs @@ -33,7 +33,7 @@ window.onload = function () { err.style['text-align'] = 'center'; err.style['background'] = '#F44336'; err.style['padding'] = '25px'; - err.innerText = '{{i18n 'UI-WELCOME_ERROR' '{"defaultMessage": "Kibana did not load properly. Check the server output for more information."}'}}'; + err.innerText = '{{i18n 'common.ui.welcomeError' '{"defaultMessage": "Kibana did not load properly. Check the server output for more information."}'}}'; document.body.innerHTML = err.outerHTML; } diff --git a/src/ui/ui_render/views/ui_app.pug b/src/ui/ui_render/views/ui_app.pug index 9f2c7fd3ec0fa5..2fc5710a86c8b9 100644 --- a/src/ui/ui_render/views/ui_app.pug +++ b/src/ui/ui_render/views/ui_app.pug @@ -108,6 +108,6 @@ block content .kibanaWelcomeLogoCircle .kibanaWelcomeLogo .kibanaWelcomeText - | #{i18n('UI-WELCOME_MESSAGE', { defaultMessage: 'Loading Kibana' })} + | #{i18n('common.ui.welcomeMessage', { defaultMessage: 'Loading Kibana' })} script(src=bootstrapScriptUrl) diff --git a/tasks/utils/i18n_verify_keys.js b/tasks/utils/i18n_verify_keys.js deleted file mode 100644 index fe62a5b999aa7c..00000000000000 --- a/tasks/utils/i18n_verify_keys.js +++ /dev/null @@ -1,99 +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 fs from 'fs'; -import glob from 'glob'; -import path from 'path'; -import Promise from 'bluebird'; -import _ from 'lodash'; - -const readFile = Promise.promisify(fs.readFile); -const globProm = Promise.promisify(glob); - -/** - * Return all the translation keys found for the file pattern - * @param {String} translationPattern - regEx pattern for translations - * @param {Array} filesPatterns - List of file patterns to be checked for translation keys - * @return {Promise} - A Promise object which will return a String Array of the translation keys - * not translated then the Object will contain all non translated translation keys with value of file the key is from - */ -export function getTranslationKeys(translationPattern, filesPatterns) { - return getFilesToVerify(filesPatterns) - .then(function (filesToVerify) { - return getKeys(translationPattern, filesToVerify); - }); -} - -/** - * Return translation keys that are not translated - * @param {Array} translationKeys - List of translation keys to be checked if translated - * @param {Object} localeTranslations - Object of locales and their translations - * @return {Object} - A object which will be empty if all translation keys are translated. If translation keys are - * not translated then the Object will contain all non translated translation keys per localem - */ -export function getNonTranslatedKeys(translationKeys, localeTranslations) { - const keysNotTranslatedPerLocale = {}; - _.forEach(localeTranslations, (translations, locale) => { - const keysNotTranslated = _.difference(translationKeys, Object.keys(translations)); - if (!_.isEmpty(keysNotTranslated)) { - keysNotTranslatedPerLocale[locale] = keysNotTranslated; - } - }); - return keysNotTranslatedPerLocale; -} - -function getFilesToVerify(verifyFilesPatterns) { - const filesToVerify = []; - - return Promise.map(verifyFilesPatterns, (verifyFilesPattern) => { - const baseSearchDir = path.dirname(verifyFilesPattern); - const pattern = path.join('**', path.basename(verifyFilesPattern)); - return globProm(pattern, { cwd: baseSearchDir, matchBase: true }) - .then(function (files) { - for (const file of files) { - filesToVerify.push(path.join(baseSearchDir, file)); - } - }); - }) - .then(function () { - return filesToVerify; - }); -} - -function getKeys(translationPattern, filesToVerify) { - const translationKeys = []; - const translationRegEx = new RegExp(translationPattern, 'g'); - - const filePromises = _.map(filesToVerify, (file) => { - return readFile(file, 'utf8') - .then(function (fileContents) { - let regexMatch; - while ((regexMatch = translationRegEx.exec(fileContents)) !== null) { - if (regexMatch.length >= 2) { - const translationKey = regexMatch[1]; - translationKeys.push(translationKey); - } - } - }); - }); - return Promise.all(filePromises) - .then(function () { - return _.uniq(translationKeys); - }); -} diff --git a/tasks/verify_translations.js b/tasks/verify_translations.js index 1820fe14ac4911..48a47a27f3ebf0 100644 --- a/tasks/verify_translations.js +++ b/tasks/verify_translations.js @@ -17,74 +17,30 @@ * under the License. */ -// TODO: Integrate a new tool for translations checking -// https://github.com/elastic/kibana/pull/19826 -import { i18nLoader } from '@kbn/i18n'; +const { resolve } = require('path'); -import { toArray } from 'rxjs/operators'; -import { fromRoot, formatListAsProse } from '../src/utils'; -import { findPluginSpecs } from '../src/plugin_discovery'; -import { collectUiExports } from '../src/ui'; - -import * as i18nVerify from './utils/i18n_verify_keys'; - -export default function (grunt) { - grunt.registerTask('verifyTranslations', async function () { +module.exports = function (grunt) { + grunt.registerTask('verifyTranslations', function () { const done = this.async(); - try { - const { spec$ } = findPluginSpecs({ - env: 'production', - plugins: { - scanDirs: [fromRoot('src/core_plugins')] + const serverCmd = { + cmd: 'node', + args: [resolve(__dirname, '../scripts/extract_default_translations')], + opts: { stdio: 'inherit' }, + }; + + new Promise((resolve, reject) => { + grunt.util.spawn(serverCmd, (error, result, code) => { + if (error || code !== 0) { + const error = new Error(`verifyTranslations exited with code ${code}`); + grunt.fail.fatal(error); + reject(error); + return; } - }); - const specs = await spec$.pipe(toArray()).toPromise(); - const uiExports = collectUiExports(specs); - await verifyTranslations(uiExports); - - done(); - } catch (error) { - done(error); - } + grunt.log.writeln(result); + resolve(); + }); + }).then(done, done); }); - -} - -async function verifyTranslations(uiExports) { - const keysUsedInViews = []; - - // Search files for used translation keys - const translationPatterns = [ - { regexp: 'i18n\\(\'(.*)\'\\)', - parsePaths: [fromRoot('src/ui/ui_render/views/*.pug')] } - ]; - for (const { regexp, parsePaths } of translationPatterns) { - const keys = await i18nVerify.getTranslationKeys(regexp, parsePaths); - for (const key of keys) { - keysUsedInViews.push(key); - } - } - - // get all of the translations from uiExports - const translations = await i18nLoader.getAllTranslationsFromPaths(uiExports.translationPaths); - const keysWithoutTranslations = Object.entries( - i18nVerify.getNonTranslatedKeys(keysUsedInViews, translations) - ); - - if (!keysWithoutTranslations.length) { - return; - } - - throw new Error( - '\n' + - '\n' + - 'The following keys are used in angular/pug views but are not translated:\n' + - keysWithoutTranslations.map(([locale, keys]) => ( - ` - ${locale}: ${formatListAsProse(keys)}` - )).join('\n') + - '\n' + - '\n' - ); -} +}; From df5cb71131ecc63074c5d89638bd0d3b951c65c8 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Fri, 24 Aug 2018 17:12:39 +0300 Subject: [PATCH 2/4] Resolve comments --- .../translations/es.json | 6 +-- .../translations/es.json | 6 +-- .../translations/es.json | 6 +-- ..._default_translations.js => i18n_check.js} | 2 +- ...ault_translations.js => run_i18n_check.js} | 0 src/ui/translations/en.json | 4 -- tasks/config/run.js | 9 ++++ tasks/jenkins.js | 2 +- tasks/test.js | 1 + tasks/verify_translations.js | 46 ------------------- 10 files changed, 21 insertions(+), 61 deletions(-) rename scripts/{extract_default_translations.js => i18n_check.js} (93%) rename src/dev/{run_extract_default_translations.js => run_i18n_check.js} (100%) delete mode 100644 src/ui/translations/en.json delete mode 100644 tasks/verify_translations.js diff --git a/packages/kbn-plugin-helpers/tasks/build/__fixtures__/build_action_test_plugin/translations/es.json b/packages/kbn-plugin-helpers/tasks/build/__fixtures__/build_action_test_plugin/translations/es.json index 2f0e09a5542ed4..b553d7b7d5fe86 100644 --- a/packages/kbn-plugin-helpers/tasks/build/__fixtures__/build_action_test_plugin/translations/es.json +++ b/packages/kbn-plugin-helpers/tasks/build/__fixtures__/build_action_test_plugin/translations/es.json @@ -1,4 +1,4 @@ { - "UI-WELCOME_MESSAGE": "Cargando Kibana", - "UI-WELCOME_ERROR": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información." -} \ No newline at end of file + "common.ui.welcomeMessage": "Cargando Kibana", + "common.ui.welcomeError": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información." +} diff --git a/packages/kbn-plugin-helpers/tasks/build/__fixtures__/create_build_test_plugin/translations/es.json b/packages/kbn-plugin-helpers/tasks/build/__fixtures__/create_build_test_plugin/translations/es.json index 2f0e09a5542ed4..b553d7b7d5fe86 100644 --- a/packages/kbn-plugin-helpers/tasks/build/__fixtures__/create_build_test_plugin/translations/es.json +++ b/packages/kbn-plugin-helpers/tasks/build/__fixtures__/create_build_test_plugin/translations/es.json @@ -1,4 +1,4 @@ { - "UI-WELCOME_MESSAGE": "Cargando Kibana", - "UI-WELCOME_ERROR": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información." -} \ No newline at end of file + "common.ui.welcomeMessage": "Cargando Kibana", + "common.ui.welcomeError": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información." +} diff --git a/packages/kbn-plugin-helpers/tasks/build/__fixtures__/create_package_test_plugin/translations/es.json b/packages/kbn-plugin-helpers/tasks/build/__fixtures__/create_package_test_plugin/translations/es.json index 2f0e09a5542ed4..b553d7b7d5fe86 100644 --- a/packages/kbn-plugin-helpers/tasks/build/__fixtures__/create_package_test_plugin/translations/es.json +++ b/packages/kbn-plugin-helpers/tasks/build/__fixtures__/create_package_test_plugin/translations/es.json @@ -1,4 +1,4 @@ { - "UI-WELCOME_MESSAGE": "Cargando Kibana", - "UI-WELCOME_ERROR": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información." -} \ No newline at end of file + "common.ui.welcomeMessage": "Cargando Kibana", + "common.ui.welcomeError": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información." +} diff --git a/scripts/extract_default_translations.js b/scripts/i18n_check.js similarity index 93% rename from scripts/extract_default_translations.js rename to scripts/i18n_check.js index 4de2184cb1be29..f461e1514e69f5 100644 --- a/scripts/extract_default_translations.js +++ b/scripts/i18n_check.js @@ -18,4 +18,4 @@ */ require('../src/setup_node_env'); -require('../src/dev/run_extract_default_translations'); +require('../src/dev/run_i18n_check'); diff --git a/src/dev/run_extract_default_translations.js b/src/dev/run_i18n_check.js similarity index 100% rename from src/dev/run_extract_default_translations.js rename to src/dev/run_i18n_check.js diff --git a/src/ui/translations/en.json b/src/ui/translations/en.json deleted file mode 100644 index ac491cf6f34657..00000000000000 --- a/src/ui/translations/en.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "UI-WELCOME_MESSAGE": "Loading", - "UI-WELCOME_ERROR": "" -} diff --git a/tasks/config/run.js b/tasks/config/run.js index 8facd9e0822fdb..5c17bd408dbd5e 100644 --- a/tasks/config/run.js +++ b/tasks/config/run.js @@ -99,6 +99,15 @@ module.exports = function (grunt) { ] }, + // used by the test and jenkins:unit tasks + // runs the i18n_check script to check i18n engine usage + i18nCheck: { + cmd: process.execPath, + args: [ + require.resolve('../../scripts/i18n_check'), + ] + }, + // used by the test:server task // runs all node.js/server mocha tests mocha: { diff --git a/tasks/jenkins.js b/tasks/jenkins.js index a68f81f34020dd..e0f587e53bc903 100644 --- a/tasks/jenkins.js +++ b/tasks/jenkins.js @@ -26,6 +26,7 @@ module.exports = function (grunt) { 'run:eslint', 'run:tslint', 'run:typeCheck', + 'run:i18nCheck', 'run:checkFileCasing', 'licenses', 'verifyDependencyVersions', @@ -36,7 +37,6 @@ module.exports = function (grunt) { 'test:projects', 'test:browser-ci', 'run:apiIntegrationTests', - 'verifyTranslations', ]); grunt.registerTask('jenkins:selenium', [ diff --git a/tasks/test.js b/tasks/test.js index 4a05c408864f51..807f2043bf29f6 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -68,6 +68,7 @@ module.exports = function (grunt) { !grunt.option('quick') && 'run:eslint', !grunt.option('quick') && 'run:tslint', !grunt.option('quick') && 'run:typeCheck', + !grunt.option('quick') && 'run:i18nCheck', 'run:checkFileCasing', 'licenses', 'test:quick', diff --git a/tasks/verify_translations.js b/tasks/verify_translations.js deleted file mode 100644 index 48a47a27f3ebf0..00000000000000 --- a/tasks/verify_translations.js +++ /dev/null @@ -1,46 +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. - */ - -const { resolve } = require('path'); - -module.exports = function (grunt) { - grunt.registerTask('verifyTranslations', function () { - const done = this.async(); - - const serverCmd = { - cmd: 'node', - args: [resolve(__dirname, '../scripts/extract_default_translations')], - opts: { stdio: 'inherit' }, - }; - - new Promise((resolve, reject) => { - grunt.util.spawn(serverCmd, (error, result, code) => { - if (error || code !== 0) { - const error = new Error(`verifyTranslations exited with code ${code}`); - grunt.fail.fatal(error); - reject(error); - return; - } - - grunt.log.writeln(result); - resolve(); - }); - }).then(done, done); - }); -}; From b90f21a2121d729b458016daddb6aa91fb03b941 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Mon, 27 Aug 2018 12:50:13 +0300 Subject: [PATCH 3/4] Remove old task --- src/dev/i18n/extract_react_messages.js | 2 +- tasks/test.js | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/dev/i18n/extract_react_messages.js b/src/dev/i18n/extract_react_messages.js index 3c6f9c4ecb5a0a..b220f077e6f637 100644 --- a/src/dev/i18n/extract_react_messages.js +++ b/src/dev/i18n/extract_react_messages.js @@ -112,7 +112,7 @@ export function extractFormattedMessages(node) { : undefined; if (!message) { - throw new Error(`Default message in is not allowed ("${messageId}").`); + throw new Error(`Empty default message in is not allowed ("${messageId}").`); } const context = contextProperty diff --git a/tasks/test.js b/tasks/test.js index 807f2043bf29f6..d106b53d278294 100644 --- a/tasks/test.js +++ b/tasks/test.js @@ -72,7 +72,6 @@ module.exports = function (grunt) { 'run:checkFileCasing', 'licenses', 'test:quick', - 'verifyTranslations', ]) ); }); From 08f91bc22d969729c6c0980dec1aecb40c350c32 Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Tue, 28 Aug 2018 14:53:54 +0300 Subject: [PATCH 4/4] Replace default Error with FailError --- .../extract_code_messages.test.js.snap | 4 +- .../extract_default_translations.test.js.snap | 8 +++- .../extract_handlebars_messages.test.js.snap | 8 ++-- .../extract_html_messages.test.js.snap | 4 +- .../extract_i18n_call_messages.test.js.snap | 8 ++-- .../extract_pug_messages.test.js.snap | 4 +- .../extract_react_messages.test.js.snap | 6 +-- src/dev/i18n/extract_default_translations.js | 16 +++++--- .../i18n/extract_default_translations.test.js | 5 +-- src/dev/i18n/extract_handlebars_messages.js | 35 ++++++++++++----- src/dev/i18n/extract_html_messages.js | 35 +++++++++++++---- src/dev/i18n/extract_i18n_call_messages.js | 32 ++++++++++----- src/dev/i18n/extract_react_messages.js | 39 +++++++++++++++---- src/dev/run_i18n_check.js | 16 +++----- 14 files changed, 146 insertions(+), 74 deletions(-) diff --git a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap index e9c1972c181f46..507efdfd615955 100644 --- a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap @@ -26,6 +26,6 @@ Array [ ] `; -exports[`extractCodeMessages throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; +exports[`extractCodeMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; -exports[`extractCodeMessages throws on missing defaultMessage 1`] = `"Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`; +exports[`extractCodeMessages throws on missing defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap index b84544e6207039..750280b7f8d954 100644 --- a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap @@ -133,4 +133,10 @@ exports[`dev/i18n/extract_default_translations injects default formats into en.j }" `; -exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `"Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See i18nrc.json for the list of supported namespaces."`; +exports[`dev/i18n/extract_default_translations throws on id collision 1`] = ` +" I18N ERROR  Error in src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3/test_file.jsx +Error:  I18N ERROR  There is more than one default message for the same id \\"plugin_3.duplicate_id\\": +\\"Message 1\\" and \\"Message 2\\"" +`; + +exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `" I18N ERROR  Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See i18nrc.json for the list of supported namespaces."`; diff --git a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap index 4506967f0eac09..7c9f72a6921ba6 100644 --- a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap @@ -12,10 +12,10 @@ Array [ ] `; -exports[`dev/i18n/extract_handlebars_messages throws on empty id 1`] = `"Empty id argument in Handlebars i18n is not allowed."`; +exports[`dev/i18n/extract_handlebars_messages throws on empty id 1`] = `" I18N ERROR  Empty id argument in Handlebars i18n is not allowed."`; -exports[`dev/i18n/extract_handlebars_messages throws on missing defaultMessage property 1`] = `"Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`; +exports[`dev/i18n/extract_handlebars_messages throws on missing defaultMessage property 1`] = `" I18N ERROR  Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`; -exports[`dev/i18n/extract_handlebars_messages throws on wrong number of arguments 1`] = `"Wrong number of arguments for handlebars i18n call."`; +exports[`dev/i18n/extract_handlebars_messages throws on wrong number of arguments 1`] = `" I18N ERROR  Wrong number of arguments for handlebars i18n call."`; -exports[`dev/i18n/extract_handlebars_messages throws on wrong properties argument type 1`] = `"Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`; +exports[`dev/i18n/extract_handlebars_messages throws on wrong properties argument type 1`] = `" I18N ERROR  Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap index 2e18503b5d35b2..aa6048b92b84ce 100644 --- a/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap @@ -26,6 +26,6 @@ Array [ ] `; -exports[`dev/i18n/extract_html_messages throws on empty i18n-id 1`] = `"Empty \\"i18n-id\\" value in angular directive is not allowed."`; +exports[`dev/i18n/extract_html_messages throws on empty i18n-id 1`] = `" I18N ERROR  Empty \\"i18n-id\\" value in angular directive is not allowed."`; -exports[`dev/i18n/extract_html_messages throws on missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; +exports[`dev/i18n/extract_html_messages throws on missing i18n-default-message attribute 1`] = `" I18N ERROR  Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap index 396c735726b294..13a79e578861ca 100644 --- a/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap @@ -20,10 +20,10 @@ Array [ ] `; -exports[`extractI18nCallMessages throws if defaultMessage is not a string literal 1`] = `"defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`; +exports[`extractI18nCallMessages throws if defaultMessage is not a string literal 1`] = `" I18N ERROR  defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`; -exports[`extractI18nCallMessages throws if message id value is not a string literal 1`] = `"Message id in i18n() or i18n.translate() should be a string literal."`; +exports[`extractI18nCallMessages throws if message id value is not a string literal 1`] = `" I18N ERROR  Message id in i18n() or i18n.translate() should be a string literal."`; -exports[`extractI18nCallMessages throws if properties object is not provided 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; +exports[`extractI18nCallMessages throws if properties object is not provided 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; -exports[`extractI18nCallMessages throws on empty defaultMessage 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; +exports[`extractI18nCallMessages throws on empty defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap index d67546739a9c16..16767f882063af 100644 --- a/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap @@ -10,6 +10,6 @@ Array [ ] `; -exports[`extractPugMessages throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; +exports[`extractPugMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; -exports[`extractPugMessages throws on missing default message 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; +exports[`extractPugMessages throws on missing default message 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap index 542cd6817e4937..2bf17cab30c282 100644 --- a/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap @@ -20,8 +20,8 @@ Array [ ] `; -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if context value is not a string literal 1`] = `"context value should be a string literal (\\"message-id\\")."`; +exports[`dev/i18n/extract_react_messages extractIntlMessages throws if context value is not a string literal 1`] = `" I18N ERROR  context value should be a string literal (\\"message-id\\")."`; -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `"defaultMessage value should be a string literal (\\"message-id\\")."`; +exports[`dev/i18n/extract_react_messages extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `" I18N ERROR  defaultMessage value should be a string literal (\\"message-id\\")."`; -exports[`dev/i18n/extract_react_messages extractIntlMessages throws if message id is not a string literal 1`] = `"Message id should be a string literal."`; +exports[`dev/i18n/extract_react_messages extractIntlMessages throws if message id is not a string literal 1`] = `" I18N ERROR  Message id should be a string literal."`; diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js index 645c43ed319d15..5bbfaa221b433a 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -21,6 +21,7 @@ import path from 'path'; import { i18n } from '@kbn/i18n'; import JSON5 from 'json5'; import normalize from 'normalize-path'; +import chalk from 'chalk'; import { extractHtmlMessages } from './extract_html_messages'; import { extractCodeMessages } from './extract_code_messages'; @@ -28,16 +29,16 @@ import { extractPugMessages } from './extract_pug_messages'; import { extractHandlebarsMessages } from './extract_handlebars_messages'; import { globAsync, readFileAsync, writeFileAsync } from './utils'; import { paths, exclude } from '../../../.i18nrc.json'; +import { createFailError } from '../run'; const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; function addMessageToMap(targetMap, key, value) { const existingValue = targetMap.get(key); if (targetMap.has(key) && existingValue.message !== value.message) { - throw new Error( - `There is more than one default message for the same id "${key}": \ -"${existingValue.message}" and "${value.message}"` - ); + throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ +There is more than one default message for the same id "${key}": +"${existingValue.message}" and "${value.message}"`); } targetMap.set(key, value); } @@ -78,7 +79,8 @@ export function validateMessageNamespace(id, filePath) { ); if (!id.startsWith(`${expectedNamespace}.`)) { - throw new Error(`Expected "${id}" id to have "${expectedNamespace}" namespace. \ + throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ +Expected "${id}" id to have "${expectedNamespace}" namespace. \ See i18nrc.json for the list of supported namespaces.`); } } @@ -131,7 +133,9 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) { addMessageToMap(targetMap, id, value); } } catch (error) { - throw new Error(`Error in ${name}\n${error.message || error}`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}` + ); } } }) diff --git a/src/dev/i18n/extract_default_translations.test.js b/src/dev/i18n/extract_default_translations.test.js index b1e3ade402ebc5..57e89f731fc6af 100644 --- a/src/dev/i18n/extract_default_translations.test.js +++ b/src/dev/i18n/extract_default_translations.test.js @@ -76,10 +76,7 @@ describe('dev/i18n/extract_default_translations', () => { const [, , pluginPath] = pluginsPaths; await expect( extractDefaultTranslations({ paths: [pluginPath], output: pluginPath }) - ).rejects.toMatchObject({ - message: `Error in ${path.join(pluginPath, 'test_file.jsx')} -There is more than one default message for the same id "plugin_3.duplicate_id": "Message 1" and "Message 2"`, - }); + ).rejects.toThrowErrorMatchingSnapshot(); }); test('validates message namespace', () => { diff --git a/src/dev/i18n/extract_handlebars_messages.js b/src/dev/i18n/extract_handlebars_messages.js index b63e30a7dd7d95..1aabdb61e2be13 100644 --- a/src/dev/i18n/extract_handlebars_messages.js +++ b/src/dev/i18n/extract_handlebars_messages.js @@ -17,7 +17,10 @@ * under the License. */ +import chalk from 'chalk'; + import { formatJSString } from './utils'; +import { createFailError } from '../run'; const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g; const TOKENS_REGEX = /[^'\s]+|(?:'([^'\\]|\\[\s\S])*')/g; @@ -36,22 +39,29 @@ export function* extractHandlebarsMessages(buffer) { } if (tokens.length !== 3) { - throw new Error('Wrong number of arguments for handlebars i18n call.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Wrong number of arguments for handlebars i18n call.` + ); } if (!idString.startsWith(`'`) || !idString.endsWith(`'`)) { - throw new Error('Message id should be a string literal.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` + ); } const messageId = formatJSString(idString.slice(1, -1)); if (!messageId) { - throw new Error(`Empty id argument in Handlebars i18n is not allowed.`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Empty id argument in Handlebars i18n is not allowed.` + ); } if (!propertiesString.startsWith(`'`) || !propertiesString.endsWith(`'`)) { - throw new Error( - `Properties string in Handlebars i18n should be a string literal ("${messageId}").` + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Properties string in Handlebars i18n should be a string literal ("${messageId}").` ); } @@ -59,19 +69,26 @@ export function* extractHandlebarsMessages(buffer) { const message = formatJSString(properties.defaultMessage); if (typeof message !== 'string') { - throw new Error( - `defaultMessage value in Handlebars i18n should be a string ("${messageId}").` + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +defaultMessage value in Handlebars i18n should be a string ("${messageId}").` ); } if (!message) { - throw new Error(`Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` + ); } const context = formatJSString(properties.context); if (context != null && typeof context !== 'string') { - throw new Error(`Context value in Handlebars i18n should be a string ("${messageId}").`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Context value in Handlebars i18n should be a string ("${messageId}").` + ); } yield [messageId, { message, context }]; diff --git a/src/dev/i18n/extract_html_messages.js b/src/dev/i18n/extract_html_messages.js index 456916daf350ae..4c8cad3ce10088 100644 --- a/src/dev/i18n/extract_html_messages.js +++ b/src/dev/i18n/extract_html_messages.js @@ -17,12 +17,14 @@ * under the License. */ +import chalk from 'chalk'; import { jsdom } from 'jsdom'; import { parse } from '@babel/parser'; import { isDirectiveLiteral, isObjectExpression, isStringLiteral } from '@babel/types'; import { isPropertyWithKey, formatHTMLString, formatJSString, traverseNodes } from './utils'; import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; +import { createFailError } from '../run'; /** * Find all substrings of "{{ any text }}" pattern @@ -51,13 +53,17 @@ function parseFilterObjectExpression(expression) { for (const property of node.properties) { if (isPropertyWithKey(property, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(property.value)) { - throw new Error('defaultMessage value should be a string literal.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} defaultMessage value should be a string literal.` + ); } message = formatJSString(property.value.value); } else if (isPropertyWithKey(property, CONTEXT_KEY)) { if (!isStringLiteral(property.value)) { - throw new Error('context value should be a string literal.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal.` + ); } context = formatJSString(property.value.value); @@ -95,20 +101,27 @@ function* getFilterMessages(htmlContent) { const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim(); if (!filterObjectExpression || !idExpression) { - throw new Error(`Cannot parse i18n filter expression: {{ ${expression} }}`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Cannot parse i18n filter expression: {{ ${expression} }}` + ); } const messageId = parseIdExpression(idExpression); if (!messageId) { - throw new Error('Empty "id" value in angular filter expression is not allowed.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty "id" value in angular filter expression is not allowed.` + ); } const { message, context } = parseFilterObjectExpression(filterObjectExpression) || {}; if (!message) { - throw new Error( - `Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` ); } @@ -124,12 +137,18 @@ function* getDirectiveMessages(htmlContent) { for (const element of document.querySelectorAll('[i18n-id]')) { const messageId = formatHTMLString(element.getAttribute('i18n-id')); if (!messageId) { - throw new Error('Empty "i18n-id" value in angular directive is not allowed.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty "i18n-id" value in angular directive is not allowed.` + ); } const message = formatHTMLString(element.getAttribute('i18n-default-message')); if (!message) { - throw new Error(`Empty defaultMessage in angular directive is not allowed ("${messageId}").`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty defaultMessage in angular directive is not allowed ("${messageId}").` + ); } const context = formatHTMLString(element.getAttribute('i18n-context')) || undefined; diff --git a/src/dev/i18n/extract_i18n_call_messages.js b/src/dev/i18n/extract_i18n_call_messages.js index 5b537ba4e01d23..ba146c06621fe3 100644 --- a/src/dev/i18n/extract_i18n_call_messages.js +++ b/src/dev/i18n/extract_i18n_call_messages.js @@ -17,10 +17,12 @@ * under the License. */ +import chalk from 'chalk'; import { isObjectExpression, isStringLiteral } from '@babel/types'; import { isPropertyWithKey, formatJSString } from './utils'; import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; +import { createFailError } from '../run'; /** * Extract messages from `funcName('id', { defaultMessage: 'Message text' })` call expression AST @@ -29,37 +31,46 @@ export function extractI18nCallMessages(node) { const [idSubTree, optionsSubTree] = node.arguments; if (!isStringLiteral(idSubTree)) { - throw new Error('Message id in i18n() or i18n.translate() should be a string literal.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Message id in i18n() or i18n.translate() should be a string literal.` + ); } const messageId = idSubTree.value; if (!messageId) { - throw new Error('Empty "id" value in i18n() or i18n.translate() is not allowed.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty "id" value in i18n() or i18n.translate() is not allowed.` + ); } let message; let context; if (!isObjectExpression(optionsSubTree)) { - throw new Error( - `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } for (const prop of optionsSubTree.properties) { if (isPropertyWithKey(prop, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(prop.value)) { - throw new Error( - `defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } message = formatJSString(prop.value.value); } else if (isPropertyWithKey(prop, CONTEXT_KEY)) { if (!isStringLiteral(prop.value)) { - throw new Error( - `context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } @@ -68,8 +79,9 @@ export function extractI18nCallMessages(node) { } if (!message) { - throw new Error( - `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_react_messages.js b/src/dev/i18n/extract_react_messages.js index b220f077e6f637..014f1214d0a18a 100644 --- a/src/dev/i18n/extract_react_messages.js +++ b/src/dev/i18n/extract_react_messages.js @@ -18,13 +18,17 @@ */ import { isJSXIdentifier, isObjectExpression, isStringLiteral } from '@babel/types'; +import chalk from 'chalk'; import { isPropertyWithKey, formatJSString, formatHTMLString } from './utils'; import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; +import { createFailError } from '../run'; function extractMessageId(value) { if (!isStringLiteral(value)) { - throw new Error('Message id should be a string literal.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` + ); } return value.value; @@ -32,7 +36,10 @@ function extractMessageId(value) { function extractMessageValue(value, id) { if (!isStringLiteral(value)) { - throw new Error(`defaultMessage value should be a string literal ("${id}").`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +defaultMessage value should be a string literal ("${id}").` + ); } return value.value; @@ -40,7 +47,9 @@ function extractMessageValue(value, id) { function extractContextValue(value, id) { if (!isStringLiteral(value)) { - throw new Error(`context value should be a string literal ("${id}").`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal ("${id}").` + ); } return value.value; @@ -55,7 +64,10 @@ export function extractIntlMessages(node) { const options = node.arguments[0]; if (!isObjectExpression(options)) { - throw new Error('Object with defaultMessage property is not passed to intl.formatMessage().'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Object with defaultMessage property is not passed to intl.formatMessage().` + ); } const [messageIdProperty, messageProperty, contextProperty] = [ @@ -69,7 +81,10 @@ export function extractIntlMessages(node) { : undefined; if (!messageId) { - throw new Error('Empty "id" value in intl.formatMessage() is not allowed.'); + createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty "id" value in intl.formatMessage() is not allowed.` + ); } const message = messageProperty @@ -77,7 +92,10 @@ export function extractIntlMessages(node) { : undefined; if (!message) { - throw new Error(`Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").` + ); } const context = contextProperty @@ -104,7 +122,9 @@ export function extractFormattedMessages(node) { : undefined; if (!messageId) { - throw new Error('Empty "id" value in is not allowed.'); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Empty "id" value in is not allowed.` + ); } const message = messageProperty @@ -112,7 +132,10 @@ export function extractFormattedMessages(node) { : undefined; if (!message) { - throw new Error(`Empty default message in is not allowed ("${messageId}").`); + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +Empty default message in is not allowed ("${messageId}").` + ); } const context = contextProperty diff --git a/src/dev/run_i18n_check.js b/src/dev/run_i18n_check.js index 65648113dda6a1..9d5f0011d6f387 100644 --- a/src/dev/run_i18n_check.js +++ b/src/dev/run_i18n_check.js @@ -17,19 +17,13 @@ * under the License. */ -import chalk from 'chalk'; import { run } from './run'; import { extractDefaultTranslations } from './i18n/extract_default_translations'; run(async ({ flags: { path, output, 'output-format': outputFormat } }) => { - try { - await extractDefaultTranslations({ - paths: Array.isArray(path) ? path : [path || './'], - output, - outputFormat, - }); - } catch (e) { - console.error(`${chalk.white.bgRed(' I18N ERROR ')} ${e.message || e}`); - process.exit(1); - } + await extractDefaultTranslations({ + paths: Array.isArray(path) ? path : [path || './'], + output, + outputFormat, + }); });