mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
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:
parent
925e13f709
commit
1e5d82c2ab
26 changed files with 164 additions and 270 deletions
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -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."
|
||||
}
|
||||
|
|
|
@ -18,4 +18,4 @@
|
|||
*/
|
||||
|
||||
require('../src/setup_node_env');
|
||||
require('../src/dev/run_extract_default_translations');
|
||||
require('../src/dev/run_i18n_check');
|
|
@ -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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m Empty defaultMessage in intl.formatMessage() is not allowed (\\"message-id\\")."`;
|
||||
|
|
|
@ -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`] = `
|
||||
"[37m[41m I18N ERROR [49m[39m Error in src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3/test_file.jsx
|
||||
Error: [37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m Expected \\"wrong_plugin_namespace.message-id\\" id to have \\"plugin_2\\" namespace. See i18nrc.json for the list of supported namespaces."`;
|
||||
|
|
|
@ -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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m Properties string in Handlebars i18n should be a string literal (\\"ui.id-1\\")."`;
|
||||
|
|
|
@ -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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m Empty defaultMessage in angular directive is not allowed (\\"message-id\\")."`;
|
||||
|
|
|
@ -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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;
|
||||
|
|
|
@ -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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m Empty defaultMessage in i18n() or i18n.translate() is not allowed (\\"message-id\\")."`;
|
||||
|
|
|
@ -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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m 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`] = `"[37m[41m I18N ERROR [49m[39m Message id should be a string literal."`;
|
||||
|
|
|
@ -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}`
|
||||
);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
@ -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', () => {
|
||||
|
|
|
@ -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 }];
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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}").`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,4 +0,0 @@
|
|||
{
|
||||
"UI-WELCOME_MESSAGE": "Loading",
|
||||
"UI-WELCOME_ERROR": ""
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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', [
|
||||
|
|
|
@ -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',
|
||||
])
|
||||
);
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
|
@ -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'
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue