diff --git a/package.json b/package.json index 4b7724cabd434..00d64056b47da 100644 --- a/package.json +++ b/package.json @@ -224,6 +224,8 @@ "yauzl": "2.7.0" }, "devDependencies": { + "@babel/parser": "7.0.0-beta.52", + "@babel/types": "7.0.0-beta.31", "@elastic/eslint-config-kibana": "link:packages/eslint-config-kibana", "@elastic/eslint-plugin-kibana-custom": "link:packages/eslint-plugin-kibana-custom", "@kbn/es": "link:packages/kbn-es", @@ -308,6 +310,7 @@ "jest-raw-loader": "^1.0.1", "jimp": "0.2.28", "jsdom": "9.9.1", + "json5": "^1.0.1", "karma": "1.7.0", "karma-chrome-launcher": "2.1.1", "karma-coverage": "1.1.1", diff --git a/scripts/extract_default_translations.js b/scripts/extract_default_translations.js new file mode 100644 index 0000000000000..4de2184cb1be2 --- /dev/null +++ b/scripts/extract_default_translations.js @@ -0,0 +1,21 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +require('../src/setup_node_env'); +require('../src/dev/run_extract_default_translations'); diff --git a/src/dev/i18n/constants.js b/src/dev/i18n/constants.js new file mode 100644 index 0000000000000..355c12d5b3060 --- /dev/null +++ b/src/dev/i18n/constants.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 const DEFAULT_MESSAGE_KEY = 'defaultMessage'; +export const CONTEXT_KEY = 'context'; diff --git a/src/dev/i18n/extract_code_messages.js b/src/dev/i18n/extract_code_messages.js new file mode 100644 index 0000000000000..1582588654cb5 --- /dev/null +++ b/src/dev/i18n/extract_code_messages.js @@ -0,0 +1,71 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { parse } from '@babel/parser'; +import { + isCallExpression, + isIdentifier, + isJSXIdentifier, + isJSXOpeningElement, + isMemberExpression, +} from '@babel/types'; + +import { extractI18nCallMessages } from './extract_i18n_call_messages'; +import { isI18nTranslateFunction, traverseNodes } from './utils'; +import { extractIntlMessages, extractFormattedMessages } from './extract_react_messages'; + +/** + * Detect Intl.formatMessage() function call (React). + * + * Example: `intl.formatMessage({ id: 'message-id', defaultMessage: 'Message text' });` + */ +function isIntlFormatMessageFunction(node) { + return ( + isCallExpression(node) && + isMemberExpression(node.callee) && + isIdentifier(node.callee.object, { name: 'intl' }) && + isIdentifier(node.callee.property, { name: 'formatMessage' }) + ); +} + +/** + * Detect elements in JSX. + * + * Example: `` + */ +function isFormattedMessageElement(node) { + return isJSXOpeningElement(node) && isJSXIdentifier(node.name, { name: 'FormattedMessage' }); +} + +export function* extractCodeMessages(buffer) { + const content = parse(buffer.toString(), { + sourceType: 'module', + plugins: ['jsx', 'typescript', 'objectRestSpread', 'classProperties', 'asyncGenerators'], + }); + + for (const node of traverseNodes(content.program.body)) { + if (isI18nTranslateFunction(node)) { + yield extractI18nCallMessages(node); + } else if (isIntlFormatMessageFunction(node)) { + yield extractIntlMessages(node); + } else if (isFormattedMessageElement(node)) { + yield extractFormattedMessages(node); + } + } +} diff --git a/src/dev/i18n/extract_default_translations.js b/src/dev/i18n/extract_default_translations.js new file mode 100644 index 0000000000000..d63c7b88092dd --- /dev/null +++ b/src/dev/i18n/extract_default_translations.js @@ -0,0 +1,120 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { resolve } from 'path'; +import { formats } from '@kbn/i18n'; +import JSON5 from 'json5'; + +import { extractHtmlMessages } from './extract_html_messages'; +import { extractCodeMessages } from './extract_code_messages'; +import { extractJadeMessages } from './extract_jade_messages'; +import { extractHandlebarsMessages } from './extract_handlebars_messages'; +import { globAsync, makeDirAsync, accessAsync, readFileAsync, writeFileAsync } from './utils'; + +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}" and "${value}"` + ); + } + targetMap.set(key, value); +} + +export async function extractDefaultTranslations(inputPath) { + const entries = await globAsync('*.{js,jsx,jade,ts,tsx,html,hbs,handlebars}', { + cwd: inputPath, + matchBase: true, + }); + + const { htmlEntries, codeEntries, jadeEntries, hbsEntries } = entries.reduce( + (paths, entry) => { + const resolvedPath = resolve(inputPath, entry); + + if (resolvedPath.endsWith('.html')) { + paths.htmlEntries.push(resolvedPath); + } else if (resolvedPath.endsWith('.jade')) { + paths.jadeEntries.push(resolvedPath); + } else if (resolvedPath.endsWith('.hbs') || resolvedPath.endsWith('.handlebars')) { + paths.hbsFiles.push(resolvedPath); + } else { + paths.codeEntries.push(resolvedPath); + } + + return paths; + }, + { htmlEntries: [], codeEntries: [], jadeEntries: [], hbsEntries: [] } + ); + + const defaultMessagesMap = new Map(); + + await Promise.all( + [ + [htmlEntries, extractHtmlMessages], + [codeEntries, extractCodeMessages], + [jadeEntries, extractJadeMessages], + [hbsEntries, extractHandlebarsMessages], + ].map(async ([entries, extractFunction]) => { + const files = await Promise.all( + entries.map(async entry => { + return { + name: entry, + content: await readFileAsync(entry), + }; + }) + ); + + for (const { name, content } of files) { + try { + for (const [id, value] of extractFunction(content)) { + addMessageToMap(defaultMessagesMap, id, value); + } + } catch (error) { + throw new Error(`Error in ${name}\n${error.message || error}`); + } + } + }) + ); + + // .slice(0, -1): remove closing curly brace from json to append messages + let jsonBuffer = Buffer.from(JSON5.stringify({ formats }, { quote: `'`, space: 2 }).slice(0, -1)); + + const defaultMessages = [...defaultMessagesMap].sort(([key1], [key2]) => { + return key1 < key2 ? -1 : 1; + }); + + for (const [mapKey, mapValue] of defaultMessages) { + jsonBuffer = Buffer.concat([ + jsonBuffer, + Buffer.from(` '${mapKey}': '${mapValue.message}',`), + Buffer.from(mapValue.context ? ` // ${mapValue.context}\n` : '\n'), + ]); + } + + // append previously removed closing curly brace + jsonBuffer = Buffer.concat([jsonBuffer, Buffer.from('}\n')]); + + try { + await accessAsync(resolve(inputPath, 'translations')); + } catch (_) { + await makeDirAsync(resolve(inputPath, 'translations')); + } + + await writeFileAsync(resolve(inputPath, 'translations', 'en.json'), jsonBuffer); +} diff --git a/src/dev/i18n/extract_handlebars_messages.js b/src/dev/i18n/extract_handlebars_messages.js new file mode 100644 index 0000000000000..cb0e45f1b31b8 --- /dev/null +++ b/src/dev/i18n/extract_handlebars_messages.js @@ -0,0 +1,71 @@ +/* + * 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 { formatJSString } from './utils'; + +const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g; +const TOKENS_REGEX = /[^'\s]+|(?:'([^'\\]|\\[\s\S])*')/g; + +/** + * Example: `'{{i18n 'message-id' '{"defaultMessage": "Message text"}'}}'` + */ +export function* extractHandlebarsMessages(buffer) { + for (const expression of buffer.toString().match(HBS_REGEX) || []) { + const tokens = expression.match(TOKENS_REGEX); + + const [functionName, idString, propertiesString] = tokens; + + if (functionName !== 'i18n') { + continue; + } + + if (tokens.length !== 3) { + throw new Error('Wrong arguments amount for handlebars i18n call.'); + } + + if (!idString.startsWith(`'`) || !idString.endsWith(`'`)) { + throw new Error('Message id should be a string literal.'); + } + + const messageId = formatJSString(idString.slice(1, -1)); + + if (!propertiesString.startsWith(`'`) || !propertiesString.endsWith(`'`)) { + throw new Error( + `Cannot parse "${messageId}" message: properties string should be a string literal.` + ); + } + + const properties = JSON.parse(propertiesString.slice(1, -1)); + const message = formatJSString(properties.defaultMessage); + + if (typeof message !== 'string') { + throw new Error( + `Cannot parse "${messageId}" message: defaultMessage value should be a string.` + ); + } + + const context = formatJSString(properties.context); + + if (context != null && typeof context !== 'string') { + throw new Error(`Cannot parse "${messageId}" message: context value should be a string.`); + } + + yield [messageId, { message, context }]; + } +} diff --git a/src/dev/i18n/extract_html_messages.js b/src/dev/i18n/extract_html_messages.js new file mode 100644 index 0000000000000..15059a672b25d --- /dev/null +++ b/src/dev/i18n/extract_html_messages.js @@ -0,0 +1,142 @@ +/* + * 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 { 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'; + +/** + * Find all substrings of "{{ any text }}" pattern + */ +const ANGULAR_EXPRESSION_REGEX = /\{\{+([\s\S]*?)\}\}+/g; + +const I18N_FILTER_MARKER = '| i18n: '; + +/** + * Extract default message from an angular filter expression argument + * @param {string} expression JavaScript code containing a filter object + * @returns {string} Default message + */ +function parseFilterObjectExpression(expression) { + // parse an object expression instead of block statement + const nodes = parse(`+${expression}`).program.body; + + for (const node of traverseNodes(nodes)) { + if (!isObjectExpression(node)) { + continue; + } + + let message; + let context; + + 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.'); + } + + 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.'); + } + + context = formatJSString(property.value.value); + } + } + + return { message, context }; + } + + return null; +} + +function parseIdExpression(expression) { + for (const node of traverseNodes(parse(expression).program.directives)) { + if (isDirectiveLiteral(node)) { + return formatJSString(node.value); + } + } + + return null; +} + +function trimCurlyBraces(string) { + return string.slice(2, -2).trim(); +} + +function* getFilterMessages(htmlContent) { + const expressions = (htmlContent.match(ANGULAR_EXPRESSION_REGEX) || []) + .filter(expression => expression.includes(I18N_FILTER_MARKER)) + .map(trimCurlyBraces); + + for (const expression of expressions) { + const filterStart = expression.indexOf(I18N_FILTER_MARKER); + const idExpression = expression.slice(0, filterStart).trim(); + const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim(); + + if (!filterObjectExpression || !idExpression) { + throw new 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.'); + } + + const { message, context } = parseFilterObjectExpression(filterObjectExpression) || {}; + + if (!message) { + throw new Error(`Cannot parse "${messageId}" message: default message is required`); + } + + yield [messageId, { message, context }]; + } +} + +function* getDirectiveMessages(htmlContent) { + const document = jsdom(htmlContent, { + features: { ProcessExternalResources: false }, + }).defaultView.document; + + for (const element of document.querySelectorAll('[i18n-id]')) { + const messageId = formatHTMLString(element.getAttribute('i18n-id')); + if (!messageId) { + throw new Error('Empty "i18n-id" value is not allowed.'); + } + + const message = formatHTMLString(element.getAttribute('i18n-default-message')); + if (!message) { + throw new Error(`Cannot parse "${messageId}" message: default message is required.`); + } + + const context = formatHTMLString(element.getAttribute('i18n-context')); + yield [messageId, { message, context }]; + } +} + +export function* extractHtmlMessages(buffer) { + const content = buffer.toString(); + yield* getDirectiveMessages(content); + yield* getFilterMessages(content); +} diff --git a/src/dev/i18n/extract_i18n_call_messages.js b/src/dev/i18n/extract_i18n_call_messages.js new file mode 100644 index 0000000000000..cc72cd1ea84c6 --- /dev/null +++ b/src/dev/i18n/extract_i18n_call_messages.js @@ -0,0 +1,70 @@ +/* + * 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 { isObjectExpression, isStringLiteral } from '@babel/types'; + +import { isPropertyWithKey, formatJSString } from './utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; + +/** + * Extract messages from `funcName('id', { defaultMessage: 'Message text' })` call expression AST + */ +export function extractI18nCallMessages(node) { + const [idSubTree, optionsSubTree] = node.arguments; + + if (!isStringLiteral(idSubTree)) { + throw new Error('Message id should be a string literal.'); + } + + const messageId = idSubTree.value; + let message; + let context; + + if (!isObjectExpression(optionsSubTree)) { + throw new Error( + `Cannot parse "${messageId}" message: object with defaultMessage property is not provided.` + ); + } + + for (const prop of optionsSubTree.properties) { + if (isPropertyWithKey(prop, DEFAULT_MESSAGE_KEY)) { + if (!isStringLiteral(prop.value)) { + throw new Error( + `Cannot parse "${messageId}" message: defaultMessage value should be a string literal.` + ); + } + + message = formatJSString(prop.value.value); + } else if (isPropertyWithKey(prop, CONTEXT_KEY)) { + if (!isStringLiteral(prop.value)) { + throw new Error( + `Cannot parse "${messageId}" message: context value should be a string literal.` + ); + } + + context = formatJSString(prop.value.value); + } + } + + if (!message) { + throw new Error(`Cannot parse "${messageId}" message: defaultMessage is required`); + } + + return [messageId, { message, context }]; +} diff --git a/src/dev/i18n/extract_jade_messages.js b/src/dev/i18n/extract_jade_messages.js new file mode 100644 index 0000000000000..272240271527a --- /dev/null +++ b/src/dev/i18n/extract_jade_messages.js @@ -0,0 +1,44 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { parse } from '@babel/parser'; + +import { extractI18nCallMessages } from './extract_i18n_call_messages'; +import { isI18nTranslateFunction, traverseNodes } from './utils'; + +/** + * Matches `i18n(...)` in `#{i18n('id', { defaultMessage: 'Message text' })}` + */ +const JADE_I18N_REGEX = /(?<=\#\{)i18n\((([^)']|'([^'\\]|\\.)*')*\)(?=\}))/g; + +/** + * Example: `#{i18n('message-id', { defaultMessage: 'Message text' })}` + */ +export function* extractJadeMessages(buffer) { + const expressions = buffer.toString().match(JADE_I18N_REGEX) || []; + + for (const expression of expressions) { + for (const node of traverseNodes(parse(expression).program.body)) { + if (isI18nTranslateFunction(node)) { + yield extractI18nCallMessages(node); + break; + } + } + } +} diff --git a/src/dev/i18n/extract_react_messages.js b/src/dev/i18n/extract_react_messages.js new file mode 100644 index 0000000000000..61518d238499d --- /dev/null +++ b/src/dev/i18n/extract_react_messages.js @@ -0,0 +1,123 @@ +/* + * 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 { isJSXIdentifier, isObjectExpression, isStringLiteral } from '@babel/types'; + +import { isPropertyWithKey, formatJSString, formatHTMLString } from './utils'; +import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants'; + +function extractMessageId(value) { + if (!isStringLiteral(value)) { + throw new Error('Message id should be a string literal.'); + } + + return value.value; +} + +function extractMessageValue(value, id) { + if (!isStringLiteral(value)) { + throw new Error(`defaultMessage value should be a string literal for id: ${id}.`); + } + + return value.value; +} + +function extractContextValue(value, id) { + if (!isStringLiteral(value)) { + throw new Error(`context value should be a string literal for id: ${id}.`); + } + + return value.value; +} + +/** + * Extract default messages from ReactJS intl.formatMessage(...) AST + * @param node Babel parser AST node + * @returns {[string, string][]} Array of id-message tuples + */ +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().'); + } + + const [messageIdProperty, messageProperty, contextProperty] = [ + 'id', + DEFAULT_MESSAGE_KEY, + CONTEXT_KEY, + ].map(key => options.properties.find(property => isPropertyWithKey(property, key))); + + const messageId = messageIdProperty + ? formatJSString(extractMessageId(messageIdProperty.value)) + : undefined; + + if (!messageId) { + throw new Error('Empty "id" value in intl.formatMessage() is not allowed.'); + } + + const message = messageProperty + ? formatJSString(extractMessageValue(messageProperty.value, messageId)) + : undefined; + + if (!message) { + throw new Error(`Default message is required for id: ${messageId}.`); + } + + const context = contextProperty + ? formatJSString(extractContextValue(contextProperty.value, messageId)) + : undefined; + + return [messageId, { message, context }]; +} + +/** + * Extract default messages from ReactJS element + * @param node Babel parser AST node + * @returns {[string, string][]} Array of id-message tuples + */ +export function extractFormattedMessages(node) { + const [messageIdProperty, messageProperty, contextProperty] = [ + 'id', + DEFAULT_MESSAGE_KEY, + CONTEXT_KEY, + ].map(key => node.attributes.find(attribute => isJSXIdentifier(attribute.name, { name: key }))); + + const messageId = messageIdProperty + ? formatHTMLString(extractMessageId(messageIdProperty.value)) + : undefined; + + if (!messageId) { + throw new Error('Empty "id" value in is not allowed.'); + } + + const message = messageProperty + ? formatHTMLString(extractMessageValue(messageProperty.value, messageId)) + : undefined; + + if (!message) { + throw new Error(`Default message is required for id: ${messageId}.`); + } + + const context = contextProperty + ? formatHTMLString(extractContextValue(contextProperty.value, messageId)) + : undefined; + + return [messageId, { message, context }]; +} diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js new file mode 100644 index 0000000000000..54a70bcca0d6f --- /dev/null +++ b/src/dev/i18n/utils.js @@ -0,0 +1,94 @@ +/* + * 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 { + isCallExpression, + isIdentifier, + isObjectProperty, + isMemberExpression, + isNode, +} from '@babel/types'; +import fs from 'fs'; +import glob from 'glob'; +import { promisify } from 'util'; + +const ESCAPE_LINE_BREAK_REGEX = /(? value && typeof value === 'object')); + } + } +} diff --git a/src/dev/run_extract_default_translations.js b/src/dev/run_extract_default_translations.js new file mode 100644 index 0000000000000..f883172d9af59 --- /dev/null +++ b/src/dev/run_extract_default_translations.js @@ -0,0 +1,27 @@ +/* + * Licensed to Elasticsearch B.V. under one or more contributor + * license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright + * ownership. Elasticsearch B.V. licenses this file to you under + * the Apache License, Version 2.0 (the "License"); you may + * not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +import { run } from './run'; +import { extractDefaultTranslations } from './i18n/extract_default_translations'; + +run(async () => { + for (const inputPath of process.argv.slice(2)) { + await extractDefaultTranslations(inputPath); + } +}); diff --git a/yarn.lock b/yarn.lock index 6304318af89cc..d2f8aa442d188 100644 --- a/yarn.lock +++ b/yarn.lock @@ -39,6 +39,10 @@ esutils "^2.0.2" js-tokens "^3.0.0" +"@babel/parser@7.0.0-beta.52": + version "7.0.0-beta.52" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.0.0-beta.52.tgz#4e935b62cd9bf872bd37bcf1f63d82fe7b0237a2" + "@babel/template@7.0.0-beta.31": version "7.0.0-beta.31" resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.0.0-beta.31.tgz#577bb29389f6c497c3e7d014617e7d6713f68bda"