Integrate main i18n tool into build pipeline (#22254)

* Integrate main i18n tool to build process

* Resolve comments

* Remove old task

* Replace default Error with FailError
This commit is contained in:
Leanid Shutau 2018-08-29 11:55:34 +03:00 committed by GitHub
parent 925e13f709
commit 1e5d82c2ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 164 additions and 270 deletions

View file

@ -1,4 +1,4 @@
{
"UI-WELCOME_MESSAGE": "Cargando Kibana",
"UI-WELCOME_ERROR": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información."
}
"common.ui.welcomeMessage": "Cargando Kibana",
"common.ui.welcomeError": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información."
}

View file

@ -1,4 +1,4 @@
{
"UI-WELCOME_MESSAGE": "Cargando Kibana",
"UI-WELCOME_ERROR": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información."
}
"common.ui.welcomeMessage": "Cargando Kibana",
"common.ui.welcomeError": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información."
}

View file

@ -1,4 +1,4 @@
{
"UI-WELCOME_MESSAGE": "Cargando Kibana",
"UI-WELCOME_ERROR": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información."
}
"common.ui.welcomeMessage": "Cargando Kibana",
"common.ui.welcomeError": "Kibana no se cargó correctamente. Heck la salida del servidor para obtener más información."
}

View file

@ -18,4 +18,4 @@
*/
require('../src/setup_node_env');
require('../src/dev/run_extract_default_translations');
require('../src/dev/run_i18n_check');

View file

@ -26,6 +26,6 @@ Array [
]
`;
exports[`extractCodeMessages throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`;
exports[`extractCodeMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`;
exports[`extractCodeMessages throws on missing defaultMessage 1`] = `"Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`;
exports[`extractCodeMessages throws on missing defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`;

View file

@ -133,4 +133,10 @@ exports[`dev/i18n/extract_default_translations injects default formats into en.j
}"
`;
exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `"Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See i18nrc.json for the list of supported namespaces."`;
exports[`dev/i18n/extract_default_translations throws on id collision 1`] = `
" I18N ERROR  Error in src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3/test_file.jsx
Error:  I18N ERROR  There is more than one default message for the same id \\"plugin_3.duplicate_id\\":
\\"Message 1\\" and \\"Message 2\\""
`;
exports[`dev/i18n/extract_default_translations throws on wrong message namespace 1`] = `" I18N ERROR  Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See i18nrc.json for the list of supported namespaces."`;

View file

@ -12,10 +12,10 @@ Array [
]
`;
exports[`dev/i18n/extract_handlebars_messages throws on empty id 1`] = `"Empty id argument in Handlebars i18n is not allowed."`;
exports[`dev/i18n/extract_handlebars_messages throws on empty id 1`] = `" I18N ERROR  Empty id argument in Handlebars i18n is not allowed."`;
exports[`dev/i18n/extract_handlebars_messages throws on missing defaultMessage property 1`] = `"Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`;
exports[`dev/i18n/extract_handlebars_messages throws on missing defaultMessage property 1`] = `" I18N ERROR  Empty defaultMessage in Handlebars i18n is not allowed (\\"message-id\\")."`;
exports[`dev/i18n/extract_handlebars_messages throws on wrong number of arguments 1`] = `"Wrong number of arguments for handlebars i18n call."`;
exports[`dev/i18n/extract_handlebars_messages throws on wrong number of arguments 1`] = `" I18N ERROR  Wrong number of arguments for handlebars i18n call."`;
exports[`dev/i18n/extract_handlebars_messages throws on wrong properties argument type 1`] = `"Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`;
exports[`dev/i18n/extract_handlebars_messages throws on wrong properties argument type 1`] = `" I18N ERROR  Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`;

View file

@ -26,6 +26,6 @@ Array [
]
`;
exports[`dev/i18n/extract_html_messages throws on empty i18n-id 1`] = `"Empty \\"i18n-id\\" value in angular directive is not allowed."`;
exports[`dev/i18n/extract_html_messages throws on empty i18n-id 1`] = `" I18N ERROR  Empty \\"i18n-id\\" value in angular directive is not allowed."`;
exports[`dev/i18n/extract_html_messages throws on missing i18n-default-message attribute 1`] = `"Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`;
exports[`dev/i18n/extract_html_messages throws on missing i18n-default-message attribute 1`] = `" I18N ERROR  Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`;

View file

@ -20,10 +20,10 @@ Array [
]
`;
exports[`extractI18nCallMessages throws if defaultMessage is not a string literal 1`] = `"defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`;
exports[`extractI18nCallMessages throws if defaultMessage is not a string literal 1`] = `" I18N ERROR  defaultMessage value in i18n() or i18n.translate() should be a string literal (\\"message-id\\")."`;
exports[`extractI18nCallMessages throws if message id value is not a string literal 1`] = `"Message id in i18n() or i18n.translate() should be a string literal."`;
exports[`extractI18nCallMessages throws if message id value is not a string literal 1`] = `" I18N ERROR  Message id in i18n() or i18n.translate() should be a string literal."`;
exports[`extractI18nCallMessages throws if properties object is not provided 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;
exports[`extractI18nCallMessages throws if properties object is not provided 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;
exports[`extractI18nCallMessages throws on empty defaultMessage 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;
exports[`extractI18nCallMessages throws on empty defaultMessage 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;

View file

@ -10,6 +10,6 @@ Array [
]
`;
exports[`extractPugMessages throws on empty id 1`] = `"Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`;
exports[`extractPugMessages throws on empty id 1`] = `" I18N ERROR  Empty \\"id\\" value in i18n() or i18n.translate() is not allowed."`;
exports[`extractPugMessages throws on missing default message 1`] = `"Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;
exports[`extractPugMessages throws on missing default message 1`] = `" I18N ERROR  Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;

View file

@ -20,8 +20,8 @@ Array [
]
`;
exports[`dev/i18n/extract_react_messages extractIntlMessages throws if context value is not a string literal 1`] = `"context value should be a string literal (\\"message-id\\")."`;
exports[`dev/i18n/extract_react_messages extractIntlMessages throws if context value is not a string literal 1`] = `" I18N ERROR  context value should be a string literal (\\"message-id\\")."`;
exports[`dev/i18n/extract_react_messages extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `"defaultMessage value should be a string literal (\\"message-id\\")."`;
exports[`dev/i18n/extract_react_messages extractIntlMessages throws if defaultMessage value is not a string literal 1`] = `" I18N ERROR  defaultMessage value should be a string literal (\\"message-id\\")."`;
exports[`dev/i18n/extract_react_messages extractIntlMessages throws if message id is not a string literal 1`] = `"Message id should be a string literal."`;
exports[`dev/i18n/extract_react_messages extractIntlMessages throws if message id is not a string literal 1`] = `" I18N ERROR  Message id should be a string literal."`;

View file

@ -21,6 +21,7 @@ import path from 'path';
import { i18n } from '@kbn/i18n';
import JSON5 from 'json5';
import normalize from 'normalize-path';
import chalk from 'chalk';
import { extractHtmlMessages } from './extract_html_messages';
import { extractCodeMessages } from './extract_code_messages';
@ -28,16 +29,16 @@ import { extractPugMessages } from './extract_pug_messages';
import { extractHandlebarsMessages } from './extract_handlebars_messages';
import { globAsync, readFileAsync, writeFileAsync } from './utils';
import { paths, exclude } from '../../../.i18nrc.json';
import { createFailError } from '../run';
const ESCAPE_SINGLE_QUOTE_REGEX = /\\([\s\S])|(')/g;
function addMessageToMap(targetMap, key, value) {
const existingValue = targetMap.get(key);
if (targetMap.has(key) && existingValue.message !== value.message) {
throw new Error(
`There is more than one default message for the same id "${key}": \
"${existingValue.message}" and "${value.message}"`
);
throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \
There is more than one default message for the same id "${key}":
"${existingValue.message}" and "${value.message}"`);
}
targetMap.set(key, value);
}
@ -78,7 +79,8 @@ export function validateMessageNamespace(id, filePath) {
);
if (!id.startsWith(`${expectedNamespace}.`)) {
throw new Error(`Expected "${id}" id to have "${expectedNamespace}" namespace. \
throw createFailError(`${chalk.white.bgRed(' I18N ERROR ')} \
Expected "${id}" id to have "${expectedNamespace}" namespace. \
See i18nrc.json for the list of supported namespaces.`);
}
}
@ -131,7 +133,9 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) {
addMessageToMap(targetMap, id, value);
}
} catch (error) {
throw new Error(`Error in ${name}\n${error.message || error}`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} Error in ${normalizePath(name)}\n${error}`
);
}
}
})

View file

@ -76,10 +76,7 @@ describe('dev/i18n/extract_default_translations', () => {
const [, , pluginPath] = pluginsPaths;
await expect(
extractDefaultTranslations({ paths: [pluginPath], output: pluginPath })
).rejects.toMatchObject({
message: `Error in ${path.join(pluginPath, 'test_file.jsx')}
There is more than one default message for the same id "plugin_3.duplicate_id": "Message 1" and "Message 2"`,
});
).rejects.toThrowErrorMatchingSnapshot();
});
test('validates message namespace', () => {

View file

@ -17,7 +17,10 @@
* under the License.
*/
import chalk from 'chalk';
import { formatJSString } from './utils';
import { createFailError } from '../run';
const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g;
const TOKENS_REGEX = /[^'\s]+|(?:'([^'\\]|\\[\s\S])*')/g;
@ -36,22 +39,29 @@ export function* extractHandlebarsMessages(buffer) {
}
if (tokens.length !== 3) {
throw new Error('Wrong number of arguments for handlebars i18n call.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} Wrong number of arguments for handlebars i18n call.`
);
}
if (!idString.startsWith(`'`) || !idString.endsWith(`'`)) {
throw new Error('Message id should be a string literal.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.`
);
}
const messageId = formatJSString(idString.slice(1, -1));
if (!messageId) {
throw new Error(`Empty id argument in Handlebars i18n is not allowed.`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} Empty id argument in Handlebars i18n is not allowed.`
);
}
if (!propertiesString.startsWith(`'`) || !propertiesString.endsWith(`'`)) {
throw new Error(
`Properties string in Handlebars i18n should be a string literal ("${messageId}").`
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Properties string in Handlebars i18n should be a string literal ("${messageId}").`
);
}
@ -59,19 +69,26 @@ export function* extractHandlebarsMessages(buffer) {
const message = formatJSString(properties.defaultMessage);
if (typeof message !== 'string') {
throw new Error(
`defaultMessage value in Handlebars i18n should be a string ("${messageId}").`
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
defaultMessage value in Handlebars i18n should be a string ("${messageId}").`
);
}
if (!message) {
throw new Error(`Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").`
);
}
const context = formatJSString(properties.context);
if (context != null && typeof context !== 'string') {
throw new Error(`Context value in Handlebars i18n should be a string ("${messageId}").`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Context value in Handlebars i18n should be a string ("${messageId}").`
);
}
yield [messageId, { message, context }];

View file

@ -17,12 +17,14 @@
* under the License.
*/
import chalk from 'chalk';
import { jsdom } from 'jsdom';
import { parse } from '@babel/parser';
import { isDirectiveLiteral, isObjectExpression, isStringLiteral } from '@babel/types';
import { isPropertyWithKey, formatHTMLString, formatJSString, traverseNodes } from './utils';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants';
import { createFailError } from '../run';
/**
* Find all substrings of "{{ any text }}" pattern
@ -51,13 +53,17 @@ function parseFilterObjectExpression(expression) {
for (const property of node.properties) {
if (isPropertyWithKey(property, DEFAULT_MESSAGE_KEY)) {
if (!isStringLiteral(property.value)) {
throw new Error('defaultMessage value should be a string literal.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} defaultMessage value should be a string literal.`
);
}
message = formatJSString(property.value.value);
} else if (isPropertyWithKey(property, CONTEXT_KEY)) {
if (!isStringLiteral(property.value)) {
throw new Error('context value should be a string literal.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal.`
);
}
context = formatJSString(property.value.value);
@ -95,20 +101,27 @@ function* getFilterMessages(htmlContent) {
const filterObjectExpression = expression.slice(filterStart + I18N_FILTER_MARKER.length).trim();
if (!filterObjectExpression || !idExpression) {
throw new Error(`Cannot parse i18n filter expression: {{ ${expression} }}`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Cannot parse i18n filter expression: {{ ${expression} }}`
);
}
const messageId = parseIdExpression(idExpression);
if (!messageId) {
throw new Error('Empty "id" value in angular filter expression is not allowed.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty "id" value in angular filter expression is not allowed.`
);
}
const { message, context } = parseFilterObjectExpression(filterObjectExpression) || {};
if (!message) {
throw new Error(
`Empty defaultMessage in angular filter expression is not allowed ("${messageId}").`
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty defaultMessage in angular filter expression is not allowed ("${messageId}").`
);
}
@ -124,12 +137,18 @@ function* getDirectiveMessages(htmlContent) {
for (const element of document.querySelectorAll('[i18n-id]')) {
const messageId = formatHTMLString(element.getAttribute('i18n-id'));
if (!messageId) {
throw new Error('Empty "i18n-id" value in angular directive is not allowed.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty "i18n-id" value in angular directive is not allowed.`
);
}
const message = formatHTMLString(element.getAttribute('i18n-default-message'));
if (!message) {
throw new Error(`Empty defaultMessage in angular directive is not allowed ("${messageId}").`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty defaultMessage in angular directive is not allowed ("${messageId}").`
);
}
const context = formatHTMLString(element.getAttribute('i18n-context')) || undefined;

View file

@ -17,10 +17,12 @@
* under the License.
*/
import chalk from 'chalk';
import { isObjectExpression, isStringLiteral } from '@babel/types';
import { isPropertyWithKey, formatJSString } from './utils';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants';
import { createFailError } from '../run';
/**
* Extract messages from `funcName('id', { defaultMessage: 'Message text' })` call expression AST
@ -29,37 +31,46 @@ export function extractI18nCallMessages(node) {
const [idSubTree, optionsSubTree] = node.arguments;
if (!isStringLiteral(idSubTree)) {
throw new Error('Message id in i18n() or i18n.translate() should be a string literal.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Message id in i18n() or i18n.translate() should be a string literal.`
);
}
const messageId = idSubTree.value;
if (!messageId) {
throw new Error('Empty "id" value in i18n() or i18n.translate() is not allowed.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty "id" value in i18n() or i18n.translate() is not allowed.`
);
}
let message;
let context;
if (!isObjectExpression(optionsSubTree)) {
throw new Error(
`Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").`
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").`
);
}
for (const prop of optionsSubTree.properties) {
if (isPropertyWithKey(prop, DEFAULT_MESSAGE_KEY)) {
if (!isStringLiteral(prop.value)) {
throw new Error(
`defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").`
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
defaultMessage value in i18n() or i18n.translate() should be a string literal ("${messageId}").`
);
}
message = formatJSString(prop.value.value);
} else if (isPropertyWithKey(prop, CONTEXT_KEY)) {
if (!isStringLiteral(prop.value)) {
throw new Error(
`context value in i18n() or i18n.translate() should be a string literal ("${messageId}").`
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
context value in i18n() or i18n.translate() should be a string literal ("${messageId}").`
);
}
@ -68,8 +79,9 @@ export function extractI18nCallMessages(node) {
}
if (!message) {
throw new Error(
`Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").`
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty defaultMessage in i18n() or i18n.translate() is not allowed ("${messageId}").`
);
}

View file

@ -18,13 +18,17 @@
*/
import { isJSXIdentifier, isObjectExpression, isStringLiteral } from '@babel/types';
import chalk from 'chalk';
import { isPropertyWithKey, formatJSString, formatHTMLString } from './utils';
import { DEFAULT_MESSAGE_KEY, CONTEXT_KEY } from './constants';
import { createFailError } from '../run';
function extractMessageId(value) {
if (!isStringLiteral(value)) {
throw new Error('Message id should be a string literal.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} Message id should be a string literal.`
);
}
return value.value;
@ -32,7 +36,10 @@ function extractMessageId(value) {
function extractMessageValue(value, id) {
if (!isStringLiteral(value)) {
throw new Error(`defaultMessage value should be a string literal ("${id}").`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
defaultMessage value should be a string literal ("${id}").`
);
}
return value.value;
@ -40,7 +47,9 @@ function extractMessageValue(value, id) {
function extractContextValue(value, id) {
if (!isStringLiteral(value)) {
throw new Error(`context value should be a string literal ("${id}").`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} context value should be a string literal ("${id}").`
);
}
return value.value;
@ -55,7 +64,10 @@ export function extractIntlMessages(node) {
const options = node.arguments[0];
if (!isObjectExpression(options)) {
throw new Error('Object with defaultMessage property is not passed to intl.formatMessage().');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Object with defaultMessage property is not passed to intl.formatMessage().`
);
}
const [messageIdProperty, messageProperty, contextProperty] = [
@ -69,7 +81,10 @@ export function extractIntlMessages(node) {
: undefined;
if (!messageId) {
throw new Error('Empty "id" value in intl.formatMessage() is not allowed.');
createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty "id" value in intl.formatMessage() is not allowed.`
);
}
const message = messageProperty
@ -77,7 +92,10 @@ export function extractIntlMessages(node) {
: undefined;
if (!message) {
throw new Error(`Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty defaultMessage in intl.formatMessage() is not allowed ("${messageId}").`
);
}
const context = contextProperty
@ -104,7 +122,9 @@ export function extractFormattedMessages(node) {
: undefined;
if (!messageId) {
throw new Error('Empty "id" value in <FormattedMessage> is not allowed.');
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} Empty "id" value in <FormattedMessage> is not allowed.`
);
}
const message = messageProperty
@ -112,7 +132,10 @@ export function extractFormattedMessages(node) {
: undefined;
if (!message) {
throw new Error(`Default message in <FormattedMessage> is not allowed ("${messageId}").`);
throw createFailError(
`${chalk.white.bgRed(' I18N ERROR ')} \
Empty default message in <FormattedMessage> is not allowed ("${messageId}").`
);
}
const context = contextProperty

View file

@ -1,4 +0,0 @@
{
"UI-WELCOME_MESSAGE": "Loading",
"UI-WELCOME_ERROR": ""
}

View file

@ -33,7 +33,7 @@ window.onload = function () {
err.style['text-align'] = 'center';
err.style['background'] = '#F44336';
err.style['padding'] = '25px';
err.innerText = '{{i18n 'UI-WELCOME_ERROR' '{"defaultMessage": "Kibana did not load properly. Check the server output for more information."}'}}';
err.innerText = '{{i18n 'common.ui.welcomeError' '{"defaultMessage": "Kibana did not load properly. Check the server output for more information."}'}}';
document.body.innerHTML = err.outerHTML;
}

View file

@ -108,6 +108,6 @@ block content
.kibanaWelcomeLogoCircle
.kibanaWelcomeLogo
.kibanaWelcomeText
| #{i18n('UI-WELCOME_MESSAGE', { defaultMessage: 'Loading Kibana' })}
| #{i18n('common.ui.welcomeMessage', { defaultMessage: 'Loading Kibana' })}
script(src=bootstrapScriptUrl)

View file

@ -99,6 +99,15 @@ module.exports = function (grunt) {
]
},
// used by the test and jenkins:unit tasks
// runs the i18n_check script to check i18n engine usage
i18nCheck: {
cmd: process.execPath,
args: [
require.resolve('../../scripts/i18n_check'),
]
},
// used by the test:server task
// runs all node.js/server mocha tests
mocha: {

View file

@ -26,6 +26,7 @@ module.exports = function (grunt) {
'run:eslint',
'run:tslint',
'run:typeCheck',
'run:i18nCheck',
'run:checkFileCasing',
'licenses',
'verifyDependencyVersions',
@ -36,7 +37,6 @@ module.exports = function (grunt) {
'test:projects',
'test:browser-ci',
'run:apiIntegrationTests',
'verifyTranslations',
]);
grunt.registerTask('jenkins:selenium', [

View file

@ -68,10 +68,10 @@ module.exports = function (grunt) {
!grunt.option('quick') && 'run:eslint',
!grunt.option('quick') && 'run:tslint',
!grunt.option('quick') && 'run:typeCheck',
!grunt.option('quick') && 'run:i18nCheck',
'run:checkFileCasing',
'licenses',
'test:quick',
'verifyTranslations',
])
);
});

View file

@ -1,99 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
import fs from 'fs';
import glob from 'glob';
import path from 'path';
import Promise from 'bluebird';
import _ from 'lodash';
const readFile = Promise.promisify(fs.readFile);
const globProm = Promise.promisify(glob);
/**
* Return all the translation keys found for the file pattern
* @param {String} translationPattern - regEx pattern for translations
* @param {Array<String>} filesPatterns - List of file patterns to be checked for translation keys
* @return {Promise} - A Promise object which will return a String Array of the translation keys
* not translated then the Object will contain all non translated translation keys with value of file the key is from
*/
export function getTranslationKeys(translationPattern, filesPatterns) {
return getFilesToVerify(filesPatterns)
.then(function (filesToVerify) {
return getKeys(translationPattern, filesToVerify);
});
}
/**
* Return translation keys that are not translated
* @param {Array<String>} translationKeys - List of translation keys to be checked if translated
* @param {Object} localeTranslations - Object of locales and their translations
* @return {Object} - A object which will be empty if all translation keys are translated. If translation keys are
* not translated then the Object will contain all non translated translation keys per localem
*/
export function getNonTranslatedKeys(translationKeys, localeTranslations) {
const keysNotTranslatedPerLocale = {};
_.forEach(localeTranslations, (translations, locale) => {
const keysNotTranslated = _.difference(translationKeys, Object.keys(translations));
if (!_.isEmpty(keysNotTranslated)) {
keysNotTranslatedPerLocale[locale] = keysNotTranslated;
}
});
return keysNotTranslatedPerLocale;
}
function getFilesToVerify(verifyFilesPatterns) {
const filesToVerify = [];
return Promise.map(verifyFilesPatterns, (verifyFilesPattern) => {
const baseSearchDir = path.dirname(verifyFilesPattern);
const pattern = path.join('**', path.basename(verifyFilesPattern));
return globProm(pattern, { cwd: baseSearchDir, matchBase: true })
.then(function (files) {
for (const file of files) {
filesToVerify.push(path.join(baseSearchDir, file));
}
});
})
.then(function () {
return filesToVerify;
});
}
function getKeys(translationPattern, filesToVerify) {
const translationKeys = [];
const translationRegEx = new RegExp(translationPattern, 'g');
const filePromises = _.map(filesToVerify, (file) => {
return readFile(file, 'utf8')
.then(function (fileContents) {
let regexMatch;
while ((regexMatch = translationRegEx.exec(fileContents)) !== null) {
if (regexMatch.length >= 2) {
const translationKey = regexMatch[1];
translationKeys.push(translationKey);
}
}
});
});
return Promise.all(filePromises)
.then(function () {
return _.uniq(translationKeys);
});
}

View file

@ -1,90 +0,0 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
// TODO: Integrate a new tool for translations checking
// https://github.com/elastic/kibana/pull/19826
import { i18nLoader } from '@kbn/i18n';
import { toArray } from 'rxjs/operators';
import { fromRoot, formatListAsProse } from '../src/utils';
import { findPluginSpecs } from '../src/plugin_discovery';
import { collectUiExports } from '../src/ui';
import * as i18nVerify from './utils/i18n_verify_keys';
export default function (grunt) {
grunt.registerTask('verifyTranslations', async function () {
const done = this.async();
try {
const { spec$ } = findPluginSpecs({
env: 'production',
plugins: {
scanDirs: [fromRoot('src/core_plugins')]
}
});
const specs = await spec$.pipe(toArray()).toPromise();
const uiExports = collectUiExports(specs);
await verifyTranslations(uiExports);
done();
} catch (error) {
done(error);
}
});
}
async function verifyTranslations(uiExports) {
const keysUsedInViews = [];
// Search files for used translation keys
const translationPatterns = [
{ regexp: 'i18n\\(\'(.*)\'\\)',
parsePaths: [fromRoot('src/ui/ui_render/views/*.pug')] }
];
for (const { regexp, parsePaths } of translationPatterns) {
const keys = await i18nVerify.getTranslationKeys(regexp, parsePaths);
for (const key of keys) {
keysUsedInViews.push(key);
}
}
// get all of the translations from uiExports
const translations = await i18nLoader.getAllTranslationsFromPaths(uiExports.translationPaths);
const keysWithoutTranslations = Object.entries(
i18nVerify.getNonTranslatedKeys(keysUsedInViews, translations)
);
if (!keysWithoutTranslations.length) {
return;
}
throw new Error(
'\n' +
'\n' +
'The following keys are used in angular/pug views but are not translated:\n' +
keysWithoutTranslations.map(([locale, keys]) => (
` - ${locale}: ${formatListAsProse(keys)}`
)).join('\n') +
'\n' +
'\n'
);
}