[7.x] Get rid of Handlebars support in i18n tools. (#32205)

This commit is contained in:
Aleh Zasypkin 2019-02-28 19:59:37 +02:00 committed by GitHub
parent 13aef8b87d
commit ff74fea451
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
12 changed files with 8 additions and 301 deletions

View file

@ -47,9 +47,7 @@
"xpack.watcher": "x-pack/plugins/watcher"
},
"exclude": [
"src/legacy/ui/ui_render/bootstrap/app_bootstrap.js",
"src/legacy/ui/ui_render/ui_render_mixin.js",
"x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts"
"src/legacy/ui/ui_render/ui_render_mixin.js"
],
"translations": [
"x-pack/plugins/translations/translations/zh-CN.json"

View file

@ -4,7 +4,7 @@
### Description
The tool is used to extract default messages from all `*.{js, ts, jsx, tsx, html, handlebars, hbs, pug}` files in provided plugins directories to a JSON file.
The tool is used to extract default messages from all `*.{js, ts, jsx, tsx, html, pug}` files in provided plugins directories to a JSON file.
It uses Babel to parse code and build an AST for each file or a single JS expression if whole file parsing is impossible. The tool is able to validate, extract and match IDs, default messages and descriptions only if they are defined statically and together, otherwise it will fail with detailed explanation. That means one can't define ID in one place and default message in another, or use function call to dynamically create default message etc.
@ -128,15 +128,6 @@ The `description` is optional, `values` is optional too unless `defaultMessage`
* Expression in `#{...}` is parsed as a JS expression.
* **Handlebars (.handlebars, .hbs)**
```hbs
{{i18n 'pluginNamespace.messageId' '{"defaultMessage": "Default message string literal", "description": "Message context or description"}'}}
```
* The `values` and `description` are optional.
* The third token (the second argument of i18n function call) should be a string literal that contains a valid JSON.
### Usage
```bash

View file

@ -1,10 +0,0 @@
(function next() {
var failure = function () {
failure = function () {};
var err = document.createElement('h1');
err.innerText = '{{i18n 'plugin_1.id_6' '{"defaultMessage": "Message 6"}'}}';
document.body.innerHTML = err.outerHTML;
}
}());

View file

@ -37,13 +37,6 @@ Array [
"message": "Message 5",
},
],
Array [
"plugin_1.id_6",
Object {
"description": "",
"message": "Message 6",
},
],
Array [
"plugin_1.id_7",
Object {

View file

@ -23,7 +23,6 @@ import {
extractHtmlMessages,
extractCodeMessages,
extractPugMessages,
extractHandlebarsMessages,
} from './extractors';
import { globAsync, readFileAsync, normalizePath } from './utils';
@ -64,13 +63,13 @@ See .i18nrc.json for the list of supported namespaces.`)
}
export async function extractMessagesFromPathToMap(inputPath, targetMap, config, reporter) {
const entries = await globAsync('*.{js,jsx,pug,ts,tsx,html,hbs,handlebars}', {
const entries = await globAsync('*.{js,jsx,pug,ts,tsx,html}', {
cwd: inputPath,
matchBase: true,
ignore: ['**/node_modules/**', '**/__tests__/**', '**/*.test.{js,jsx,ts,tsx}', '**/*.d.ts'],
});
const { htmlEntries, codeEntries, pugEntries, hbsEntries } = entries.reduce(
const { htmlEntries, codeEntries, pugEntries } = entries.reduce(
(paths, entry) => {
const resolvedPath = path.resolve(inputPath, entry);
@ -78,15 +77,13 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap, config,
paths.htmlEntries.push(resolvedPath);
} else if (resolvedPath.endsWith('.pug')) {
paths.pugEntries.push(resolvedPath);
} else if (resolvedPath.endsWith('.hbs') || resolvedPath.endsWith('.handlebars')) {
paths.hbsEntries.push(resolvedPath);
} else {
paths.codeEntries.push(resolvedPath);
}
return paths;
},
{ htmlEntries: [], codeEntries: [], pugEntries: [], hbsEntries: [] }
{ htmlEntries: [], codeEntries: [], pugEntries: [] }
);
await Promise.all(
@ -94,7 +91,6 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap, config,
[htmlEntries, extractHtmlMessages],
[codeEntries, extractCodeMessages],
[pugEntries, extractPugMessages],
[hbsEntries, extractHandlebarsMessages],
].map(async ([entries, extractFunction]) => {
const files = await Promise.all(
filterEntries(entries, config.exclude).map(async entry => {

View file

@ -1,45 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`dev/i18n/extractors/handlebars extracts handlebars default messages 1`] = `
Array [
Array [
"ui.id-1",
Object {
"description": "Message description",
"message": "Message text",
},
],
]
`;
exports[`dev/i18n/extractors/handlebars throws on empty id 1`] = `
Array [
Array [
[Error: Empty id argument in Handlebars i18n is not allowed.],
],
]
`;
exports[`dev/i18n/extractors/handlebars throws on missing defaultMessage property 1`] = `
Array [
Array [
[Error: defaultMessage value in Handlebars i18n should be a string ("message-id").],
],
]
`;
exports[`dev/i18n/extractors/handlebars throws on wrong number of arguments 1`] = `
Array [
Array [
[Error: Wrong number of arguments for handlebars i18n call.],
],
]
`;
exports[`dev/i18n/extractors/handlebars throws on wrong properties argument type 1`] = `
Array [
Array [
[Error: Properties string in Handlebars i18n should be a string literal ("ui.id-1").],
],
]
`;

View file

@ -1,115 +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 { formatJSString, checkValuesProperty } from '../utils';
import { createFailError, isFailError } from '../../run';
import { DEFAULT_MESSAGE_KEY, DESCRIPTION_KEY } from '../constants';
const HBS_REGEX = /(?<=\{\{)([\s\S]*?)(?=\}\})/g;
const TOKENS_REGEX = /[^'\s]+|(?:'([^'\\]|\\[\s\S])*')/g;
/**
* Example: `'{{i18n 'message-id' '{"defaultMessage": "Message text"}'}}'`
*/
export function* extractHandlebarsMessages(buffer, reporter) {
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) {
reporter.report(createFailError(`Wrong number of arguments for handlebars i18n call.`));
continue;
}
if (!idString.startsWith(`'`) || !idString.endsWith(`'`)) {
reporter.report(createFailError(`Message id should be a string literal.`));
continue;
}
const messageId = formatJSString(idString.slice(1, -1));
if (!messageId) {
reporter.report(createFailError(`Empty id argument in Handlebars i18n is not allowed.`));
continue;
}
if (!propertiesString.startsWith(`'`) || !propertiesString.endsWith(`'`)) {
reporter.report(
createFailError(
`Properties string in Handlebars i18n should be a string literal ("${messageId}").`
)
);
continue;
}
const properties = JSON.parse(propertiesString.slice(1, -1));
if (typeof properties.defaultMessage !== 'string') {
reporter.report(
createFailError(
`defaultMessage value in Handlebars i18n should be a string ("${messageId}").`
)
);
continue;
}
if (properties[DESCRIPTION_KEY] != null && typeof properties[DESCRIPTION_KEY] !== 'string') {
reporter.report(
createFailError(`Description value in Handlebars i18n should be a string ("${messageId}").`)
);
continue;
}
const message = formatJSString(properties[DEFAULT_MESSAGE_KEY]);
const description = formatJSString(properties[DESCRIPTION_KEY]);
if (!message) {
reporter.report(
createFailError(`Empty defaultMessage in Handlebars i18n is not allowed ("${messageId}").`)
);
continue;
}
const valuesObject = properties.values;
if (valuesObject != null && typeof valuesObject !== 'object') {
reporter.report(
createFailError(`"values" value should be an object in Handlebars i18n ("${messageId}").`)
);
continue;
}
try {
checkValuesProperty(Object.keys(valuesObject || {}), message, messageId);
yield [messageId, { message, description }];
} catch (error) {
if (!isFailError(error)) {
throw error;
}
reporter.report(error);
}
}
}

View file

@ -1,94 +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 { extractHandlebarsMessages } from './handlebars';
const report = jest.fn();
describe('dev/i18n/extractors/handlebars', () => {
beforeEach(() => {
report.mockClear();
});
test('extracts handlebars default messages', () => {
const source = Buffer.from(`\
window.onload = function () {
(function next() {
var failure = function () {
failure = function () {};
var err = document.createElement('h1');
err.style['color'] = 'white';
err.innerText = '{{i18n 'ui.id-1' \
'{"defaultMessage": "Message text", "description": "Message description"}'}}';
document.body.innerHTML = err.outerHTML;
}
}());
};
`);
const actual = Array.from(extractHandlebarsMessages(source));
expect(actual).toMatchSnapshot();
});
test('throws on wrong number of arguments', () => {
const source = Buffer.from(`\
window.onload = function () {
err.innerText = '{{i18n 'ui.id-1'}}';
};
`);
expect(() => extractHandlebarsMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on wrong properties argument type', () => {
const source = Buffer.from(`\
window.onload = function () {
err.innerText = '{{i18n 'ui.id-1' propertiesJSONIdentifier}}';
};
`);
expect(() => extractHandlebarsMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on empty id', () => {
const source = Buffer.from(`\
window.onload = function () {
err.innerText = '{{i18n '' '{"defaultMessage": "Message text", "description": "Message description"}'}}';
};
`);
expect(() => extractHandlebarsMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
test('throws on missing defaultMessage property', () => {
const source = Buffer.from(`\
window.onload = function () {
err.innerText = '{{i18n 'message-id' '{"description": "Message description"}'}}';
};
`);
expect(() => extractHandlebarsMessages(source, { report }).next()).not.toThrow();
expect(report.mock.calls).toMatchSnapshot();
});
});

View file

@ -18,6 +18,5 @@
*/
export { extractCodeMessages } from './code';
export { extractHandlebarsMessages } from './handlebars';
export { extractHtmlMessages } from './html';
export { extractPugMessages } from './pug';

View file

@ -21,7 +21,6 @@ import Handlebars from 'handlebars';
import { createHash } from 'crypto';
import { readFile } from 'fs';
import { resolve } from 'path';
import { i18n } from '@kbn/i18n';
export class AppBootstrap {
constructor({ templateData }) {
@ -34,17 +33,13 @@ export class AppBootstrap {
this._rawTemplate = await loadRawTemplate();
}
Handlebars.registerHelper('i18n', (id, options) => i18n.translate(id, JSON.parse(options)));
const template = Handlebars.compile(this._rawTemplate, {
knownHelpers: { i18n: true },
knownHelpersOnly: true,
noEscape: true, // this is a js file, so html escaping isn't appropriate
strict: true,
});
const compiledJsFile = template(this.templateData);
Handlebars.unregisterHelper('i18n');
return compiledJsFile;
return template(this.templateData);
}
async getJsFileHash() {

View file

@ -20,7 +20,6 @@
const mockTemplate = `
{{appId}}
{{regularBundlePath}}
{{i18n 'foo' '{"defaultMessage": "bar"}'}}
`;
jest.mock('fs', () => ({

View file

@ -204,12 +204,12 @@ export interface LogEntryDocumentFields {
const convertLogDocumentToEntry = (
sourceId: string,
formatMessage: (fields: LogEntryDocumentFields) => InfraLogMessageSegment[]
formatLogMessage: (fields: LogEntryDocumentFields) => InfraLogMessageSegment[]
) => (document: LogEntryDocument): InfraLogEntry => ({
key: document.key,
gid: document.gid,
source: sourceId,
message: formatMessage(document.fields),
message: formatLogMessage(document.fields),
});
const convertDateRangeBucketToSummaryBucket = (