Implement a build tool for default messages extraction (#19620)

* Implement a build tool for default messages extraction

* Refactor extraction tool, resolve review comments, fix bugs

* Resolve comments, refactor extraction tool, fix bugs

* Add context to messages extraction tool

* Resolve comments

* Fix bugs

* Add messages extraction from .jade files, refactor code

* Add template literals parsing

* Return defaultMessages.json to plain structure

* Refactor utils

* Fix bugs

* Refactor code, resolve review comments, fix bugs

* Fix minor bug

* Get rid of '@babel/traverse' and add its native implementation

* Add handlebars messages extraction

* Fix mkdir on macOS

* Fix bugs, inject default formats, finalize the tool

* Fix filsystem permissions

* Refactor nodes traversal

* Update code style

* Downgrade @babel/types to fix build issues

* Resolve comments

* Fix minor bugs
This commit is contained in:
Leanid Shutau 2018-07-17 15:56:55 +03:00 committed by GitHub
parent 6939f5073c
commit 9c9159ce0c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 811 additions and 0 deletions

View file

@ -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",
@ -307,6 +309,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",

View file

@ -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');

21
src/dev/i18n/constants.js Normal file
View file

@ -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';

View file

@ -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 <FormattedMessage> elements in JSX.
*
* Example: `<FormattedMessage id="message-id" defaultMessage="Message text"/>`
*/
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);
}
}
}

View file

@ -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);
}

View file

@ -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 }];
}
}

View file

@ -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);
}

View file

@ -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 }];
}

View file

@ -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;
}
}
}
}

View file

@ -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 <FormattedMessage> 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 <FormattedMessage> 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 }];
}

94
src/dev/i18n/utils.js Normal file
View file

@ -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 = /(?<!\\)\\\n/g;
const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g;
const HTML_LINE_BREAK_REGEX = /[\s]*\n[\s]*/g;
export const readFileAsync = promisify(fs.readFile);
export const writeFileAsync = promisify(fs.writeFile);
export const globAsync = promisify(glob);
export const makeDirAsync = promisify(fs.mkdir);
export const accessAsync = promisify(fs.access);
export function isPropertyWithKey(property, identifierName) {
return isObjectProperty(property) && isIdentifier(property.key, { name: identifierName });
}
/**
* Detect angular i18n service call or `@kbn/i18n` translate function call.
*
* Service call example: `i18n('message-id', { defaultMessage: 'Message text'})`
*
* `@kbn/i18n` example: `i18n.translate('message-id', { defaultMessage: 'Message text'})`
*/
export function isI18nTranslateFunction(node) {
return (
isCallExpression(node) &&
(isIdentifier(node.callee, { name: 'i18n' }) ||
(isMemberExpression(node.callee) &&
isIdentifier(node.callee.object, { name: 'i18n' }) &&
isIdentifier(node.callee.property, { name: 'translate' })))
);
}
export function formatJSString(string) {
return (string || '')
.replace(ESCAPE_LINE_BREAK_REGEX, '')
.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2')
.replace('\n', '\\n');
}
export function formatHTMLString(string) {
return (string || '')
.replace(ESCAPE_SINGLE_QUOTE_REGEX, '\\$1$2')
.replace(HTML_LINE_BREAK_REGEX, ' ');
}
/**
* Traverse an array of nodes using default depth-first traversal algorithm.
* We don't use `@babel/traverse` because of its bug: https://github.com/babel/babel/issues/8262
*
* @generator
* @param {object[]} nodes array of nodes or objects with Node values
* @yields {Node} each node
*/
export function* traverseNodes(nodes) {
for (const node of nodes) {
if (isNode(node)) {
yield node;
}
// if node is an object / array, traverse all of its object values
if (node && typeof node === 'object') {
yield* traverseNodes(Object.values(node).filter(value => value && typeof value === 'object'));
}
}
}

View file

@ -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);
}
});

View file

@ -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"