From 4cf727aa980c7b5596c330c8e7082c097f42caea Mon Sep 17 00:00:00 2001 From: Leanid Shutau Date: Wed, 5 Sep 2018 14:02:15 +0300 Subject: [PATCH] Add logging to messages validation (#22296) * Add logging and parallelization to messages validation * Refactor dev/i18n * Resolve comments * Remove parallelism and fix tests * Resolve comments --- .../extract_code_messages.test.js.snap | 31 --- .../extract_default_translations.test.js.snap | 179 +++++------------- .../extract_handlebars_messages.test.js.snap | 21 -- .../extract_html_messages.test.js.snap | 31 --- .../extract_i18n_call_messages.test.js.snap | 29 --- .../extract_pug_messages.test.js.snap | 15 -- .../extract_react_messages.test.js.snap | 27 --- src/dev/i18n/extract_default_translations.js | 101 ++-------- .../i18n/extract_default_translations.test.js | 35 +--- .../__snapshots__/code.test.js.snap | 31 +++ .../__snapshots__/handlebars.test.js.snap | 21 ++ .../__snapshots__/html.test.js.snap | 31 +++ .../__snapshots__/i18n_call.test.js.snap | 29 +++ .../extractors/__snapshots__/pug.test.js.snap | 15 ++ .../__snapshots__/react.test.js.snap | 27 +++ .../code.js} | 6 +- .../code.test.js} | 18 +- .../handlebars.js} | 30 +-- .../handlebars.test.js} | 4 +- .../html.js} | 36 +--- .../html.test.js} | 4 +- .../i18n_call.js} | 29 +-- .../i18n_call.test.js} | 6 +- src/dev/i18n/extractors/index.js | 25 +++ .../pug.js} | 4 +- .../pug.test.js} | 4 +- .../react.js} | 38 ++-- .../react.test.js} | 6 +- src/dev/i18n/index.js | 22 +++ .../__snapshots__/json.test.js.snap | 67 +++++++ .../__snapshots__/json5.test.js.snap | 65 +++++++ src/dev/i18n/serializers/index.js | 21 ++ src/dev/i18n/serializers/json.js | 34 ++++ src/dev/i18n/serializers/json.test.js | 37 ++++ src/dev/i18n/serializers/json5.js | 48 +++++ src/dev/i18n/serializers/json5.test.js | 42 ++++ src/dev/run/index.js | 2 +- src/dev/run_i18n_check.js | 47 ++++- 38 files changed, 701 insertions(+), 517 deletions(-) delete mode 100644 src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap delete mode 100644 src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/code.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/html.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/pug.test.js.snap create mode 100644 src/dev/i18n/extractors/__snapshots__/react.test.js.snap rename src/dev/i18n/{extract_code_messages.js => extractors/code.js} (94%) rename src/dev/i18n/{extract_code_messages.test.js => extractors/code.test.js} (89%) rename src/dev/i18n/{extract_handlebars_messages.js => extractors/handlebars.js} (68%) rename src/dev/i18n/{extract_handlebars_messages.test.js => extractors/handlebars.test.js} (95%) rename src/dev/i18n/{extract_html_messages.js => extractors/html.js} (78%) rename src/dev/i18n/{extract_html_messages.test.js => extractors/html.test.js} (94%) rename src/dev/i18n/{extract_i18n_call_messages.js => extractors/i18n_call.js} (64%) rename src/dev/i18n/{extract_i18n_call_messages.test.js => extractors/i18n_call.test.js} (95%) create mode 100644 src/dev/i18n/extractors/index.js rename src/dev/i18n/{extract_pug_messages.js => extractors/pug.js} (91%) rename src/dev/i18n/{extract_pug_messages.test.js => extractors/pug.test.js} (94%) rename src/dev/i18n/{extract_react_messages.js => extractors/react.js} (73%) rename src/dev/i18n/{extract_react_messages.test.js => extractors/react.test.js} (97%) create mode 100644 src/dev/i18n/index.js create mode 100644 src/dev/i18n/serializers/__snapshots__/json.test.js.snap create mode 100644 src/dev/i18n/serializers/__snapshots__/json5.test.js.snap create mode 100644 src/dev/i18n/serializers/index.js create mode 100644 src/dev/i18n/serializers/json.js create mode 100644 src/dev/i18n/serializers/json.test.js create mode 100644 src/dev/i18n/serializers/json5.js create mode 100644 src/dev/i18n/serializers/json5.test.js diff --git a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap deleted file mode 100644 index 507efdfd61595..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_code_messages.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractCodeMessages extracts React, server-side and angular service default messages 1`] = ` -Array [ - Array [ - "kbn.mgmt.id-1", - Object { - "context": undefined, - "message": "Message text 1", - }, - ], - Array [ - "kbn.mgmt.id-2", - Object { - "context": "Message context", - "message": "Message text 2", - }, - ], - Array [ - "kbn.mgmt.id-3", - Object { - "context": undefined, - "message": "Message text 3", - }, - ], -] -`; - -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`] = `" 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 750280b7f8d95..1a9997dc07dd9 100644 --- a/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap +++ b/src/dev/i18n/__snapshots__/extract_default_translations.test.js.snap @@ -1,142 +1,63 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`dev/i18n/extract_default_translations extracts messages to en.json 1`] = ` -"{ - \\"formats\\": { - \\"number\\": { - \\"currency\\": { - \\"style\\": \\"currency\\" - }, - \\"percent\\": { - \\"style\\": \\"percent\\" - } +exports[`dev/i18n/extract_default_translations extracts messages from path to map 1`] = ` +Array [ + Array [ + "plugin_1.id_1", + Object { + "context": undefined, + "message": "Message 1", }, - \\"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\\" - } + ], + Array [ + "plugin_1.id_2", + Object { + "context": "Message context", + "message": "Message 2", }, - \\"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.id_1\\": \\"Message 1\\", - \\"plugin_1.id_2\\": { - \\"text\\": \\"Message 2\\", - \\"comment\\": \\"Message context\\" - }, - \\"plugin_1.id_3\\": \\"Message 3\\", - \\"plugin_1.id_4\\": \\"Message 4\\", - \\"plugin_1.id_5\\": \\"Message 5\\", - \\"plugin_1.id_6\\": \\"Message 6\\", - \\"plugin_1.id_7\\": \\"Message 7\\" -}" -`; - -exports[`dev/i18n/extract_default_translations injects default formats into en.json 1`] = ` -"{ - \\"formats\\": { - \\"number\\": { - \\"currency\\": { - \\"style\\": \\"currency\\" - }, - \\"percent\\": { - \\"style\\": \\"percent\\" - } + ], + Array [ + "plugin_1.id_3", + Object { + "context": undefined, + "message": "Message 3", + }, + ], + Array [ + "plugin_1.id_4", + Object { + "context": undefined, + "message": "Message 4", + }, + ], + Array [ + "plugin_1.id_5", + Object { + "context": undefined, + "message": "Message 5", + }, + ], + Array [ + "plugin_1.id_6", + Object { + "context": "", + "message": "Message 6", }, - \\"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\\" - } + ], + Array [ + "plugin_1.id_7", + Object { + "context": undefined, + "message": "Message 7", }, - \\"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\\": \\"Message text\\" -}" + ], +] `; 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\\": +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."`; +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."`; diff --git a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap b/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap deleted file mode 100644 index 7c9f72a6921ba..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_handlebars_messages.test.js.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_handlebars_messages extracts handlebars default messages 1`] = ` -Array [ - Array [ - "ui.id-1", - Object { - "context": "Message context", - "message": "Message text", - }, - ], -] -`; - -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`] = `" 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`] = `" I18N ERROR  Wrong number of arguments for handlebars i18n call."`; - -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 deleted file mode 100644 index aa6048b92b84c..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_html_messages.test.js.snap +++ /dev/null @@ -1,31 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_html_messages extracts default messages from HTML 1`] = ` -Array [ - Array [ - "kbn.dashboard.id-1", - Object { - "context": "Message context 1", - "message": "Message text 1", - }, - ], - Array [ - "kbn.dashboard.id-2", - Object { - "context": undefined, - "message": "Message text 2", - }, - ], - Array [ - "kbn.dashboard.id-3", - Object { - "context": "Message context 3", - "message": "Message text 3", - }, - ], -] -`; - -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`] = `" 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 deleted file mode 100644 index 13a79e578861c..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_i18n_call_messages.test.js.snap +++ /dev/null @@ -1,29 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractI18nCallMessages extracts "i18n" and "i18n.translate" functions call message 1`] = ` -Array [ - "message-id-1", - Object { - "context": "Message context 1", - "message": "Default message 1", - }, -] -`; - -exports[`extractI18nCallMessages extracts "i18n" and "i18n.translate" functions call message 2`] = ` -Array [ - "message-id-2", - Object { - "context": "Message context 2", - "message": "Default message 2", - }, -] -`; - -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`] = `" I18N ERROR  Message id in i18n() or i18n.translate() should be a string literal."`; - -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`] = `" 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 deleted file mode 100644 index 16767f882063a..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_pug_messages.test.js.snap +++ /dev/null @@ -1,15 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`extractPugMessages extracts messages from pug template 1`] = ` -Array [ - "message-id", - Object { - "context": "Message context", - "message": "Default message", - }, -] -`; - -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`] = `" 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 deleted file mode 100644 index 2bf17cab30c28..0000000000000 --- a/src/dev/i18n/__snapshots__/extract_react_messages.test.js.snap +++ /dev/null @@ -1,27 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`dev/i18n/extract_react_messages extractFormattedMessages extracts messages from "" element 1`] = ` -Array [ - "message-id-2", - Object { - "context": "Message context 2", - "message": "Default message 2", - }, -] -`; - -exports[`dev/i18n/extract_react_messages extractIntlMessages extracts messages from "intl.formatMessage" function call 1`] = ` -Array [ - "message-id-1", - Object { - "context": "Message context 1", - "message": "Default message 1", - }, -] -`; - -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`] = `" 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`] = `" 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 5bbfaa221b433..06d397961e233 100644 --- a/src/dev/i18n/extract_default_translations.js +++ b/src/dev/i18n/extract_default_translations.js @@ -18,28 +18,27 @@ */ 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'; -import { extractPugMessages } from './extract_pug_messages'; -import { extractHandlebarsMessages } from './extract_handlebars_messages'; -import { globAsync, readFileAsync, writeFileAsync } from './utils'; +import { + extractHtmlMessages, + extractCodeMessages, + extractPugMessages, + extractHandlebarsMessages, +} from './extractors'; +import { globAsync, readFileAsync } from './utils'; import { paths, exclude } from '../../../.i18nrc.json'; -import { createFailError } from '../run'; - -const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; +import { createFailError, isFailError } from '../run'; function addMessageToMap(targetMap, key, value) { const existingValue = targetMap.get(key); + if (targetMap.has(key) && existingValue.message !== value.message) { - throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ -There is more than one default message for the same id "${key}": + throw createFailError(`There is more than one default message for the same id "${key}": "${existingValue.message}" and "${value.message}"`); } + targetMap.set(key, value); } @@ -47,7 +46,7 @@ function normalizePath(inputPath) { return normalize(path.relative('.', inputPath)); } -function filterPaths(inputPaths) { +export function filterPaths(inputPaths) { const availablePaths = Object.values(paths); const pathsForExtraction = new Set(); @@ -79,9 +78,8 @@ export function validateMessageNamespace(id, filePath) { ); if (!id.startsWith(`${expectedNamespace}.`)) { - throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \ -Expected "${id}" id to have "${expectedNamespace}" namespace. \ -See i18nrc.json for the list of supported namespaces.`); + throw createFailError(`Expected "${id}" id to have "${expectedNamespace}" namespace. \ +See .i18nrc.json for the list of supported namespaces.`); } } @@ -133,72 +131,15 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) { addMessageToMap(targetMap, id, value); } } catch (error) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}` - ); + if (isFailError(error)) { + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}` + ); + } + + throw error; } } }) ); } - -function serializeToJson5(defaultMessages) { - // .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) - ); - - for (const [mapKey, mapValue] of defaultMessages) { - const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); - const formattedContext = mapValue.context - ? mapValue.context.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') - : ''; - - jsonBuffer = Buffer.concat([ - jsonBuffer, - Buffer.from(` '${mapKey}': '${formattedMessage}',`), - Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), - ]); - } - - // append previously removed closing curly brace - jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); - - return jsonBuffer; -} - -function serializeToJson(defaultMessages) { - const resultJsonObject = { formats: i18n.formats }; - - for (const [mapKey, mapValue] of defaultMessages) { - if (mapValue.context) { - resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; - } else { - resultJsonObject[mapKey] = mapValue.message; - } - } - - return JSON.stringify(resultJsonObject, undefined, 2); -} - -export async function extractDefaultTranslations({ paths, output, outputFormat }) { - const defaultMessagesMap = new Map(); - - for (const inputPath of filterPaths(paths)) { - await extractMessagesFromPathToMap(inputPath, defaultMessagesMap); - } - - // messages shouldn't be extracted to a file if output is not supplied - if (!output || !defaultMessagesMap.size) { - return; - } - - const defaultMessages = [...defaultMessagesMap].sort(([key1], [key2]) => - key1.localeCompare(key2) - ); - - await writeFileAsync( - path.resolve(output, 'en.json'), - outputFormat === 'json5' ? serializeToJson5(defaultMessages) : serializeToJson(defaultMessages) - ); -} diff --git a/src/dev/i18n/extract_default_translations.test.js b/src/dev/i18n/extract_default_translations.test.js index 57e89f731fc6a..b89361e87fcf7 100644 --- a/src/dev/i18n/extract_default_translations.test.js +++ b/src/dev/i18n/extract_default_translations.test.js @@ -20,7 +20,7 @@ import path from 'path'; import { - extractDefaultTranslations, + extractMessagesFromPathToMap, validateMessageNamespace, } from './extract_default_translations'; @@ -40,42 +40,21 @@ jest.mock('../../../.i18nrc.json', () => ({ exclude: [], })); -const utils = require('./utils'); -utils.writeFileAsync = jest.fn(); - describe('dev/i18n/extract_default_translations', () => { - test('extracts messages to en.json', async () => { + test('extracts messages from path to map', async () => { const [pluginPath] = pluginsPaths; + const resultMap = new Map(); - utils.writeFileAsync.mockClear(); - await extractDefaultTranslations({ - paths: [pluginPath], - output: pluginPath, - }); - - const [[, json]] = utils.writeFileAsync.mock.calls; - - expect(json.toString()).toMatchSnapshot(); - }); - - test('injects default formats into en.json', async () => { - const [, pluginPath] = pluginsPaths; - - utils.writeFileAsync.mockClear(); - await extractDefaultTranslations({ - paths: [pluginPath], - output: pluginPath, - }); + await extractMessagesFromPathToMap(pluginPath, resultMap); - const [[, json]] = utils.writeFileAsync.mock.calls; - - expect(json.toString()).toMatchSnapshot(); + expect([...resultMap].sort()).toMatchSnapshot(); }); test('throws on id collision', async () => { const [, , pluginPath] = pluginsPaths; + await expect( - extractDefaultTranslations({ paths: [pluginPath], output: pluginPath }) + extractMessagesFromPathToMap(pluginPath, new Map()) ).rejects.toThrowErrorMatchingSnapshot(); }); diff --git a/src/dev/i18n/extractors/__snapshots__/code.test.js.snap b/src/dev/i18n/extractors/__snapshots__/code.test.js.snap new file mode 100644 index 0000000000000..26c621e32964d --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/code.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/code extracts React, server-side and angular service default messages 1`] = ` +Array [ + Array [ + "kbn.mgmt.id-1", + Object { + "context": undefined, + "message": "Message text 1", + }, + ], + Array [ + "kbn.mgmt.id-2", + Object { + "context": "Message context", + "message": "Message text 2", + }, + ], + Array [ + "kbn.mgmt.id-3", + Object { + "context": undefined, + "message": "Message text 3", + }, + ], +] +`; + +exports[`dev/i18n/extractors/code throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; + +exports[`dev/i18n/extractors/code throws on missing defaultMessage 1`] = `"Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap b/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap new file mode 100644 index 0000000000000..7ca5178c7538f --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/handlebars.test.js.snap @@ -0,0 +1,21 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/handlebars extracts handlebars default messages 1`] = ` +Array [ + Array [ + "ui.id-1", + Object { + "context": "Message context", + "message": "Message text", + }, + ], +] +`; + +exports[`dev/i18n/extractors/handlebars throws on empty id 1`] = `"Empty id argument in Handlebars i18n is not allowed."`; + +exports[`dev/i18n/extractors/handlebars throws on missing defaultMessage property 1`] = `"Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/handlebars throws on wrong number of arguments 1`] = `"Wrong number of arguments for handlebars i18n call."`; + +exports[`dev/i18n/extractors/handlebars throws on wrong properties argument type 1`] = `"Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/html.test.js.snap b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap new file mode 100644 index 0000000000000..982341c880074 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/html.test.js.snap @@ -0,0 +1,31 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/html extracts default messages from HTML 1`] = ` +Array [ + Array [ + "kbn.dashboard.id-1", + Object { + "context": "Message context 1", + "message": "Message text 1", + }, + ], + Array [ + "kbn.dashboard.id-2", + Object { + "context": undefined, + "message": "Message text 2", + }, + ], + Array [ + "kbn.dashboard.id-3", + Object { + "context": "Message context 3", + "message": "Message text 3", + }, + ], +] +`; + +exports[`dev/i18n/extractors/html throws on empty i18n-id 1`] = `"Empty \\"i18n-id\\" value in angular directive is not allowed."`; + +exports[`dev/i18n/extractors/html throws on missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap b/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap new file mode 100644 index 0000000000000..c9bf2f07716d4 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/i18n_call.test.js.snap @@ -0,0 +1,29 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/i18n_call extracts "i18n" and "i18n.translate" functions call message 1`] = ` +Array [ + "message-id-1", + Object { + "context": "Message context 1", + "message": "Default message 1", + }, +] +`; + +exports[`dev/i18n/extractors/i18n_call extracts "i18n" and "i18n.translate" functions call message 2`] = ` +Array [ + "message-id-2", + Object { + "context": "Message context 2", + "message": "Default message 2", + }, +] +`; + +exports[`dev/i18n/extractors/i18n_call throws if defaultMessage is not a string literal 1`] = `"defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/i18n_call throws if message id value is not a string literal 1`] = `"Message id in i18n() or i18n.translate() should be a string literal."`; + +exports[`dev/i18n/extractors/i18n_call throws if properties object is not provided 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/i18n_call throws on empty defaultMessage 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap b/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap new file mode 100644 index 0000000000000..c95fb0d149cd0 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/pug.test.js.snap @@ -0,0 +1,15 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/pug extracts messages from pug template 1`] = ` +Array [ + "message-id", + Object { + "context": "Message context", + "message": "Default message", + }, +] +`; + +exports[`dev/i18n/extractors/pug throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`; + +exports[`dev/i18n/extractors/pug throws on missing default message 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`; diff --git a/src/dev/i18n/extractors/__snapshots__/react.test.js.snap b/src/dev/i18n/extractors/__snapshots__/react.test.js.snap new file mode 100644 index 0000000000000..6a51a5e216004 --- /dev/null +++ b/src/dev/i18n/extractors/__snapshots__/react.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/extractors/react extractFormattedMessages extracts messages from "" element 1`] = ` +Array [ + "message-id-2", + Object { + "context": "Message context 2", + "message": "Default message 2", + }, +] +`; + +exports[`dev/i18n/extractors/react extractIntlMessages extracts messages from "intl.formatMessage" function call 1`] = ` +Array [ + "message-id-1", + Object { + "context": "Message context 1", + "message": "Default message 1", + }, +] +`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if context value is not a string literal 1`] = `"context value should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `"defaultMessage value should be a string literal (\\"message-id\\")."`; + +exports[`dev/i18n/extractors/react extractIntlMessages throws if message id is not a string literal 1`] = `"Message id should be a string literal."`; diff --git a/src/dev/i18n/extract_code_messages.js b/src/dev/i18n/extractors/code.js similarity index 94% rename from src/dev/i18n/extract_code_messages.js rename to src/dev/i18n/extractors/code.js index e7b72e6efa162..e7477b17e2759 100644 --- a/src/dev/i18n/extract_code_messages.js +++ b/src/dev/i18n/extractors/code.js @@ -26,9 +26,9 @@ import { isMemberExpression, } from '@babel/types'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { isI18nTranslateFunction, traverseNodes } from './utils'; -import { extractIntlMessages, extractFormattedMessages } from './extract_react_messages'; +import { extractI18nCallMessages } from './i18n_call'; +import { isI18nTranslateFunction, traverseNodes } from '../utils'; +import { extractIntlMessages, extractFormattedMessages } from './react'; /** * Detect Intl.formatMessage() function call (React). diff --git a/src/dev/i18n/extract_code_messages.test.js b/src/dev/i18n/extractors/code.test.js similarity index 89% rename from src/dev/i18n/extract_code_messages.test.js rename to src/dev/i18n/extractors/code.test.js index 5b3e64ebb4f07..3cc7d39f78d40 100644 --- a/src/dev/i18n/extract_code_messages.test.js +++ b/src/dev/i18n/extractors/code.test.js @@ -24,8 +24,8 @@ import { extractCodeMessages, isFormattedMessageElement, isIntlFormatMessageFunction, -} from './extract_code_messages'; -import { traverseNodes } from './utils'; +} from './code'; +import { traverseNodes } from '../utils'; const extractCodeMessagesSource = Buffer.from(` i18n('kbn.mgmt.id-1', { defaultMessage: 'Message text 1' }); @@ -65,7 +65,7 @@ function f() { } `; -describe('extractCodeMessages', () => { +describe('dev/i18n/extractors/code', () => { test('extracts React, server-side and angular service default messages', () => { const actual = Array.from(extractCodeMessages(extractCodeMessagesSource)); expect(actual.sort()).toMatchSnapshot(); @@ -84,12 +84,16 @@ describe('extractCodeMessages', () => { describe('isIntlFormatMessageFunction', () => { test('detects intl.formatMessage call expression', () => { - const callExpressionNodes = [...traverseNodes(parse(intlFormatMessageSource).program.body)].filter( - node => isCallExpression(node) - ); + const callExpressionNodes = [ + ...traverseNodes(parse(intlFormatMessageSource).program.body), + ].filter(node => isCallExpression(node)); expect(callExpressionNodes).toHaveLength(4); - expect(callExpressionNodes.every(callExpressionNode => isIntlFormatMessageFunction(callExpressionNode))).toBe(true); + expect( + callExpressionNodes.every(callExpressionNode => + isIntlFormatMessageFunction(callExpressionNode) + ) + ).toBe(true); }); }); diff --git a/src/dev/i18n/extract_handlebars_messages.js b/src/dev/i18n/extractors/handlebars.js similarity index 68% rename from src/dev/i18n/extract_handlebars_messages.js rename to src/dev/i18n/extractors/handlebars.js index 1aabdb61e2be1..7c57c8d0da731 100644 --- a/src/dev/i18n/extract_handlebars_messages.js +++ b/src/dev/i18n/extractors/handlebars.js @@ -17,10 +17,8 @@ * under the License. */ -import chalk from 'chalk'; - -import { formatJSString } from './utils'; -import { createFailError } from '../run'; +import { formatJSString } from '../utils'; +import { createFailError } from '../../run'; const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g; const TOKENS_REGEX = /[^'\s]+|(?:'([^'\\]|\\[\s\S])*')/g; @@ -39,29 +37,22 @@ export function* extractHandlebarsMessages(buffer) { } if (tokens.length !== 3) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Wrong number of arguments for handlebars i18n call.` - ); + throw createFailError(`Wrong number of arguments for handlebars i18n call.`); } if (!idString.startsWith(`'`) || !idString.endsWith(`'`)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` - ); + throw createFailError(`Message id should be a string literal.`); } const messageId = formatJSString(idString.slice(1, -1)); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Empty id argument in Handlebars i18n is not allowed.` - ); + throw createFailError(`Empty id argument in Handlebars i18n is not allowed.`); } if (!propertiesString.startsWith(`'`) || !propertiesString.endsWith(`'`)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Properties string in Handlebars i18n should be a string literal ("${messageId}").` + `Properties string in Handlebars i18n should be a string literal ("${messageId}").` ); } @@ -70,15 +61,13 @@ Properties string in Handlebars i18n should be a string literal ("${messageId}") if (typeof message !== 'string') { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value in Handlebars i18n should be a string ("${messageId}").` + `defaultMessage value in Handlebars i18n should be a string ("${messageId}").` ); } if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` + `Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` ); } @@ -86,8 +75,7 @@ Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").` if (context != null && typeof context !== 'string') { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Context value in Handlebars i18n should be a string ("${messageId}").` + `Context value in Handlebars i18n should be a string ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_handlebars_messages.test.js b/src/dev/i18n/extractors/handlebars.test.js similarity index 95% rename from src/dev/i18n/extract_handlebars_messages.test.js rename to src/dev/i18n/extractors/handlebars.test.js index e4f53852b6cc3..52365989bd7fd 100644 --- a/src/dev/i18n/extract_handlebars_messages.test.js +++ b/src/dev/i18n/extractors/handlebars.test.js @@ -17,9 +17,9 @@ * under the License. */ -import { extractHandlebarsMessages } from './extract_handlebars_messages'; +import { extractHandlebarsMessages } from './handlebars'; -describe('dev/i18n/extract_handlebars_messages', () => { +describe('dev/i18n/extractors/handlebars', () => { test('extracts handlebars default messages', () => { const source = Buffer.from(`\ window.onload = function () { diff --git a/src/dev/i18n/extract_html_messages.js b/src/dev/i18n/extractors/html.js similarity index 78% rename from src/dev/i18n/extract_html_messages.js rename to src/dev/i18n/extractors/html.js index 4c8cad3ce1008..b576acb31c6d2 100644 --- a/src/dev/i18n/extract_html_messages.js +++ b/src/dev/i18n/extractors/html.js @@ -17,14 +17,13 @@ * 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'; +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 @@ -53,17 +52,13 @@ function parseFilterObjectExpression(expression) { for (const property of node.properties) { if (isPropertyWithKey(property, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(property.value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} defaultMessage value should be a string literal.` - ); + throw createFailError(`defaultMessage value should be a string literal.`); } message = formatJSString(property.value.value); } else if (isPropertyWithKey(property, CONTEXT_KEY)) { if (!isStringLiteral(property.value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal.` - ); + throw createFailError(`context value should be a string literal.`); } context = formatJSString(property.value.value); @@ -101,27 +96,20 @@ function* getFilterMessages(htmlContent) { const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim(); if (!filterObjectExpression || !idExpression) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Cannot parse i18n filter expression: {{ ${expression} }}` - ); + throw createFailError(`Cannot parse i18n filter expression: {{ ${expression} }}`); } const messageId = parseIdExpression(idExpression); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in angular filter expression is not allowed.` - ); + throw createFailError(`Empty "id" value in angular filter expression is not allowed.`); } const { message, context } = parseFilterObjectExpression(filterObjectExpression) || {}; if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` + `Empty defaultMessage in angular filter expression is not allowed ("${messageId}").` ); } @@ -137,17 +125,13 @@ function* getDirectiveMessages(htmlContent) { for (const element of document.querySelectorAll('[i18n-id]')) { const messageId = formatHTMLString(element.getAttribute('i18n-id')); if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "i18n-id" value in angular directive is not allowed.` - ); + throw createFailError(`Empty "i18n-id" value in angular directive is not allowed.`); } const message = formatHTMLString(element.getAttribute('i18n-default-message')); if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in angular directive is not allowed ("${messageId}").` + `Empty defaultMessage in angular directive is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_html_messages.test.js b/src/dev/i18n/extractors/html.test.js similarity index 94% rename from src/dev/i18n/extract_html_messages.test.js rename to src/dev/i18n/extractors/html.test.js index d5cf7d6fd5ee2..40664edd81e4a 100644 --- a/src/dev/i18n/extract_html_messages.test.js +++ b/src/dev/i18n/extractors/html.test.js @@ -17,7 +17,7 @@ * under the License. */ -import { extractHtmlMessages } from './extract_html_messages'; +import { extractHtmlMessages } from './html'; const htmlSourceBuffer = Buffer.from(`
@@ -37,7 +37,7 @@ const htmlSourceBuffer = Buffer.from(`
`); -describe('dev/i18n/extract_html_messages', () => { +describe('dev/i18n/extractors/html', () => { test('extracts default messages from HTML', () => { const actual = Array.from(extractHtmlMessages(htmlSourceBuffer)); expect(actual.sort()).toMatchSnapshot(); diff --git a/src/dev/i18n/extract_i18n_call_messages.js b/src/dev/i18n/extractors/i18n_call.js similarity index 64% rename from src/dev/i18n/extract_i18n_call_messages.js rename to src/dev/i18n/extractors/i18n_call.js index ba146c06621fe..1adcf42598e16 100644 --- a/src/dev/i18n/extract_i18n_call_messages.js +++ b/src/dev/i18n/extractors/i18n_call.js @@ -17,12 +17,11 @@ * 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'; +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 @@ -31,19 +30,13 @@ export function extractI18nCallMessages(node) { const [idSubTree, optionsSubTree] = node.arguments; if (!isStringLiteral(idSubTree)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Message id in i18n() or i18n.translate() should be a string literal.` - ); + throw createFailError(`Message id in i18n() or i18n.translate() should be a string literal.`); } const messageId = idSubTree.value; if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in i18n() or i18n.translate() is not allowed.` - ); + throw createFailError(`Empty "id" value in i18n() or i18n.translate() is not allowed.`); } let message; @@ -51,8 +44,7 @@ Empty "id" value in i18n() or i18n.translate() is not allowed.` if (!isObjectExpression(optionsSubTree)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } @@ -60,8 +52,7 @@ Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId} if (isPropertyWithKey(prop, DEFAULT_MESSAGE_KEY)) { if (!isStringLiteral(prop.value)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + `defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } @@ -69,8 +60,7 @@ defaultMessage value in i18n() or i18n.translate() should be a string literal (" } else if (isPropertyWithKey(prop, CONTEXT_KEY)) { if (!isStringLiteral(prop.value)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` + `context value in i18n() or i18n.translate() should be a string literal ("${messageId}").` ); } @@ -80,8 +70,7 @@ context value in i18n() or i18n.translate() should be a string literal ("${messa if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` + `Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_i18n_call_messages.test.js b/src/dev/i18n/extractors/i18n_call.test.js similarity index 95% rename from src/dev/i18n/extract_i18n_call_messages.test.js rename to src/dev/i18n/extractors/i18n_call.test.js index 0985233e4b3dd..f3ab92f4f1d6e 100644 --- a/src/dev/i18n/extract_i18n_call_messages.test.js +++ b/src/dev/i18n/extractors/i18n_call.test.js @@ -20,8 +20,8 @@ import { parse } from '@babel/parser'; import { isCallExpression } from '@babel/types'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { traverseNodes } from './utils'; +import { extractI18nCallMessages } from './i18n_call'; +import { traverseNodes } from '../utils'; const i18nCallMessageSource = ` i18n('message-id-1', { defaultMessage: 'Default message 1', context: 'Message context 1' }); @@ -31,7 +31,7 @@ const translateCallMessageSource = ` i18n.translate('message-id-2', { defaultMessage: 'Default message 2', context: 'Message context 2' }); `; -describe('extractI18nCallMessages', () => { +describe('dev/i18n/extractors/i18n_call', () => { test('extracts "i18n" and "i18n.translate" functions call message', () => { let callExpressionNode = [...traverseNodes(parse(i18nCallMessageSource).program.body)].find( node => isCallExpression(node) diff --git a/src/dev/i18n/extractors/index.js b/src/dev/i18n/extractors/index.js new file mode 100644 index 0000000000000..7362eeb4e7003 --- /dev/null +++ b/src/dev/i18n/extractors/index.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. + */ + +export { extractCodeMessages } from './code'; +export { extractHandlebarsMessages } from './handlebars'; +export { extractHtmlMessages } from './html'; +export { extractI18nCallMessages } from './i18n_call'; +export { extractPugMessages } from './pug'; +export { extractFormattedMessages, extractIntlMessages } from './react'; diff --git a/src/dev/i18n/extract_pug_messages.js b/src/dev/i18n/extractors/pug.js similarity index 91% rename from src/dev/i18n/extract_pug_messages.js rename to src/dev/i18n/extractors/pug.js index 8451c0b11db24..59851d19e88ab 100644 --- a/src/dev/i18n/extract_pug_messages.js +++ b/src/dev/i18n/extractors/pug.js @@ -19,8 +19,8 @@ import { parse } from '@babel/parser'; -import { extractI18nCallMessages } from './extract_i18n_call_messages'; -import { isI18nTranslateFunction, traverseNodes } from './utils'; +import { extractI18nCallMessages } from './i18n_call'; +import { isI18nTranslateFunction, traverseNodes } from '../utils'; /** * Matches `i18n(...)` in `#{i18n('id', { defaultMessage: 'Message text' })}` diff --git a/src/dev/i18n/extract_pug_messages.test.js b/src/dev/i18n/extractors/pug.test.js similarity index 94% rename from src/dev/i18n/extract_pug_messages.test.js rename to src/dev/i18n/extractors/pug.test.js index 0f72c13a6a339..7f901d1d992db 100644 --- a/src/dev/i18n/extract_pug_messages.test.js +++ b/src/dev/i18n/extractors/pug.test.js @@ -17,9 +17,9 @@ * under the License. */ -import { extractPugMessages } from './extract_pug_messages'; +import { extractPugMessages } from './pug'; -describe('extractPugMessages', () => { +describe('dev/i18n/extractors/pug', () => { test('extracts messages from pug template', () => { const source = Buffer.from(`\ #{i18n('message-id', { defaultMessage: 'Default message', context: 'Message context' })} diff --git a/src/dev/i18n/extract_react_messages.js b/src/dev/i18n/extractors/react.js similarity index 73% rename from src/dev/i18n/extract_react_messages.js rename to src/dev/i18n/extractors/react.js index 014f1214d0a18..074af4a76d5b4 100644 --- a/src/dev/i18n/extract_react_messages.js +++ b/src/dev/i18n/extractors/react.js @@ -18,17 +18,14 @@ */ 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'; +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 createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.` - ); + throw createFailError(`Message id should be a string literal.`); } return value.value; @@ -36,10 +33,7 @@ function extractMessageId(value) { function extractMessageValue(value, id) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -defaultMessage value should be a string literal ("${id}").` - ); + throw createFailError(`defaultMessage value should be a string literal ("${id}").`); } return value.value; @@ -47,9 +41,7 @@ defaultMessage value should be a string literal ("${id}").` function extractContextValue(value, id) { if (!isStringLiteral(value)) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal ("${id}").` - ); + throw createFailError(`context value should be a string literal ("${id}").`); } return value.value; @@ -65,8 +57,7 @@ export function extractIntlMessages(node) { if (!isObjectExpression(options)) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Object with defaultMessage property is not passed to intl.formatMessage().` + `Object with defaultMessage property is not passed to intl.formatMessage().` ); } @@ -81,10 +72,7 @@ Object with defaultMessage property is not passed to intl.formatMessage().` : undefined; if (!messageId) { - createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty "id" value in intl.formatMessage() is not allowed.` - ); + createFailError(`Empty "id" value in intl.formatMessage() is not allowed.`); } const message = messageProperty @@ -93,8 +81,7 @@ Empty "id" value in intl.formatMessage() is not allowed.` if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").` + `Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").` ); } @@ -122,9 +109,7 @@ export function extractFormattedMessages(node) { : undefined; if (!messageId) { - throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} Empty "id" value in is not allowed.` - ); + throw createFailError(`Empty "id" value in is not allowed.`); } const message = messageProperty @@ -133,8 +118,7 @@ export function extractFormattedMessages(node) { if (!message) { throw createFailError( - `${chalk.white.bgRed(' I18N ERROR ')} \ -Empty default message in is not allowed ("${messageId}").` + `Empty default message in is not allowed ("${messageId}").` ); } diff --git a/src/dev/i18n/extract_react_messages.test.js b/src/dev/i18n/extractors/react.test.js similarity index 97% rename from src/dev/i18n/extract_react_messages.test.js rename to src/dev/i18n/extractors/react.test.js index 00233ac1abed2..91e65a0ecc20f 100644 --- a/src/dev/i18n/extract_react_messages.test.js +++ b/src/dev/i18n/extractors/react.test.js @@ -20,8 +20,8 @@ import { parse } from '@babel/parser'; import { isCallExpression, isJSXOpeningElement, isJSXIdentifier } from '@babel/types'; -import { extractIntlMessages, extractFormattedMessages } from './extract_react_messages'; -import { traverseNodes } from './utils'; +import { extractIntlMessages, extractFormattedMessages } from './react'; +import { traverseNodes } from '../utils'; const intlFormatMessageCallSource = ` const MyComponentContent = ({ intl }) => ( @@ -79,7 +79,7 @@ intl.formatMessage({ `, ]; -describe('dev/i18n/extract_react_messages', () => { +describe('dev/i18n/extractors/react', () => { describe('extractIntlMessages', () => { test('extracts messages from "intl.formatMessage" function call', () => { const ast = parse(intlFormatMessageCallSource, { plugins: ['jsx'] }); diff --git a/src/dev/i18n/index.js b/src/dev/i18n/index.js new file mode 100644 index 0000000000000..703e6ac682855 --- /dev/null +++ b/src/dev/i18n/index.js @@ -0,0 +1,22 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +export { filterPaths, extractMessagesFromPathToMap } from './extract_default_translations'; +export { writeFileAsync } from './utils'; +export { serializeToJson, serializeToJson5 } from './serializers'; diff --git a/src/dev/i18n/serializers/__snapshots__/json.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap new file mode 100644 index 0000000000000..c35e91e25cbb6 --- /dev/null +++ b/src/dev/i18n/serializers/__snapshots__/json.test.js.snap @@ -0,0 +1,67 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/serializers/json should serialize default messages to JSON 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\\" + } + } + }, + \\"plugin1.message.id-1\\": \\"Message text 1 \\", + \\"plugin2.message.id-2\\": { + \\"text\\": \\"Message text 2\\", + \\"comment\\": \\"Message context\\" + } +}" +`; diff --git a/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap new file mode 100644 index 0000000000000..2166b32f28fd1 --- /dev/null +++ b/src/dev/i18n/serializers/__snapshots__/json5.test.js.snap @@ -0,0 +1,65 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`dev/i18n/serializers/json5 should serialize default messages to JSON5 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', + }, + }, + }, + 'plugin1.message.id-1': 'Message text 1', + 'plugin2.message.id-2': 'Message text 2', // Message context +} +" +`; diff --git a/src/dev/i18n/serializers/index.js b/src/dev/i18n/serializers/index.js new file mode 100644 index 0000000000000..3c10d7754563d --- /dev/null +++ b/src/dev/i18n/serializers/index.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. + */ + +export { serializeToJson } from './json'; +export { serializeToJson5 } from './json5'; diff --git a/src/dev/i18n/serializers/json.js b/src/dev/i18n/serializers/json.js new file mode 100644 index 0000000000000..8e615af1e81d3 --- /dev/null +++ b/src/dev/i18n/serializers/json.js @@ -0,0 +1,34 @@ +/* + * 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'; + +export function serializeToJson(defaultMessages) { + const resultJsonObject = { formats: i18n.formats }; + + for (const [mapKey, mapValue] of defaultMessages) { + if (mapValue.context) { + resultJsonObject[mapKey] = { text: mapValue.message, comment: mapValue.context }; + } else { + resultJsonObject[mapKey] = mapValue.message; + } + } + + return JSON.stringify(resultJsonObject, undefined, 2); +} diff --git a/src/dev/i18n/serializers/json.test.js b/src/dev/i18n/serializers/json.test.js new file mode 100644 index 0000000000000..9486a999fe7db --- /dev/null +++ b/src/dev/i18n/serializers/json.test.js @@ -0,0 +1,37 @@ +/* + * 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 { serializeToJson } from './json'; + +describe('dev/i18n/serializers/json', () => { + test('should serialize default messages to JSON', () => { + const messages = new Map([ + ['plugin1.message.id-1', { message: 'Message text 1 ' }], + [ + 'plugin2.message.id-2', + { + message: 'Message text 2', + context: 'Message context', + }, + ], + ]); + + expect(serializeToJson(messages)).toMatchSnapshot(); + }); +}); diff --git a/src/dev/i18n/serializers/json5.js b/src/dev/i18n/serializers/json5.js new file mode 100644 index 0000000000000..0156053d5f43b --- /dev/null +++ b/src/dev/i18n/serializers/json5.js @@ -0,0 +1,48 @@ +/* + * 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 JSON5 from 'json5'; +import { i18n } from '@kbn/i18n'; + +const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g; + +export function serializeToJson5(defaultMessages) { + // .slice(0, -1): remove closing curly brace from json to append messages + let jsonBuffer = Buffer.from( + JSON5.stringify({ formats: i18n.formats }, { quote: `'`, space: 2 }).slice(0, -1) + ); + + for (const [mapKey, mapValue] of defaultMessages) { + const formattedMessage = mapValue.message.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2'); + const formattedContext = mapValue.context + ? mapValue.context.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2') + : ''; + + jsonBuffer = Buffer.concat([ + jsonBuffer, + Buffer.from(` '${mapKey}': '${formattedMessage}',`), + Buffer.from(formattedContext ? ` // ${formattedContext}\n` : '\n'), + ]); + } + + // append previously removed closing curly brace + jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); + + return jsonBuffer; +} diff --git a/src/dev/i18n/serializers/json5.test.js b/src/dev/i18n/serializers/json5.test.js new file mode 100644 index 0000000000000..90be880bd32a3 --- /dev/null +++ b/src/dev/i18n/serializers/json5.test.js @@ -0,0 +1,42 @@ +/* + * 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 { serializeToJson5 } from './json5'; + +describe('dev/i18n/serializers/json5', () => { + test('should serialize default messages to JSON5', () => { + const messages = new Map([ + [ + 'plugin1.message.id-1', + { + message: 'Message text 1', + }, + ], + [ + 'plugin2.message.id-2', + { + message: 'Message text 2', + context: 'Message context', + }, + ], + ]); + + expect(serializeToJson5(messages).toString()).toMatchSnapshot(); + }); +}); diff --git a/src/dev/run/index.js b/src/dev/run/index.js index 1eef88d60b0a5..b176ac365fcf4 100644 --- a/src/dev/run/index.js +++ b/src/dev/run/index.js @@ -18,4 +18,4 @@ */ export { run } from './run'; -export { createFailError, combineErrors } from './fail'; +export { createFailError, combineErrors, isFailError } from './fail'; diff --git a/src/dev/run_i18n_check.js b/src/dev/run_i18n_check.js index 9d5f0011d6f38..02d70622b54cd 100644 --- a/src/dev/run_i18n_check.js +++ b/src/dev/run_i18n_check.js @@ -17,13 +17,46 @@ * under the License. */ -import { run } from './run'; -import { extractDefaultTranslations } from './i18n/extract_default_translations'; +import chalk from 'chalk'; +import Listr from 'listr'; +import { resolve } from 'path'; + +import { run, createFailError } from './run'; +import { + filterPaths, + extractMessagesFromPathToMap, + writeFileAsync, + serializeToJson, + serializeToJson5, +} from './i18n/'; run(async ({ flags: { path, output, 'output-format': outputFormat } }) => { - await extractDefaultTranslations({ - paths: Array.isArray(path) ? path : [path || './'], - output, - outputFormat, - }); + const paths = Array.isArray(path) ? path : [path || './']; + const filteredPaths = filterPaths(paths); + + if (filteredPaths.length === 0) { + throw createFailError( + `${chalk.white.bgRed(' I18N ERROR ')} \ +None of input paths is available for extraction or validation. See .i18nrc.json.` + ); + } + + const list = new Listr( + filteredPaths.map(filteredPath => ({ + task: messages => extractMessagesFromPathToMap(filteredPath, messages), + title: filteredPath, + })) + ); + + // messages shouldn't be extracted to a file if output is not supplied + const messages = await list.run(new Map()); + if (!output || !messages.size) { + return; + } + + const sortedMessages = [...messages].sort(([key1], [key2]) => key1.localeCompare(key2)); + await writeFileAsync( + resolve(output, 'en.json'), + outputFormat === 'json5' ? serializeToJson5(sortedMessages) : serializeToJson(sortedMessages) + ); });