[I18n] Add include option to i18n_check for 3rd party plugins (#26963)
* [I18n] Add include/exclude options to i18n_check tool for 3rd-party plugins * Implement a better solution * Update .i18nrc.json template * Resolve comment * Add conditional ejs expressions for i18n in plugin generator * Hide package.json from Jest * Complete template translation * Resolve comments
This commit is contained in:
parent
76ae4e6923
commit
973fad3b0a
|
@ -72,6 +72,7 @@ module.exports = function({ name }) {
|
|||
filters: {
|
||||
'public/**/*': 'generateApp',
|
||||
'translations/**/*': 'generateTranslations',
|
||||
'.i18nrc.json': 'generateTranslations',
|
||||
'public/hack.js': 'generateHack',
|
||||
'server/**/*': 'generateApi',
|
||||
'public/app.scss': 'generateScss',
|
||||
|
@ -80,6 +81,7 @@ module.exports = function({ name }) {
|
|||
move: {
|
||||
gitignore: '.gitignore',
|
||||
eslintrc: '.eslintrc',
|
||||
'package_template.json': 'package.json',
|
||||
},
|
||||
data: answers =>
|
||||
Object.assign(
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
{
|
||||
"paths": {
|
||||
"<%= camelCase(name) %>": "./"
|
||||
}
|
||||
}
|
|
@ -17,6 +17,11 @@
|
|||
"test:browser": "plugin-helpers test:browser",
|
||||
"build": "plugin-helpers build"
|
||||
},
|
||||
<%_ if (generateTranslations) { _%>
|
||||
"dependencies": {
|
||||
"@kbn/i18n": "link:../../kibana/packages/kbn-i18n"
|
||||
},
|
||||
<%_ } _%>
|
||||
"devDependencies": {
|
||||
"@elastic/eslint-config-kibana": "link:../../kibana/packages/eslint-config-kibana",
|
||||
"@elastic/eslint-import-resolver-kibana": "link:../../kibana/packages/kbn-eslint-import-resolver-kibana",
|
|
@ -2,6 +2,9 @@ import React from 'react';
|
|||
import { uiModules } from 'ui/modules';
|
||||
import chrome from 'ui/chrome';
|
||||
import { render, unmountComponentAtNode } from 'react-dom';
|
||||
<%_ if (generateTranslations) { _%>
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
<%_ } _%>
|
||||
|
||||
import 'ui/autoload/styles';
|
||||
import './less/main.less';
|
||||
|
@ -24,7 +27,16 @@ function RootController($scope, $element, $http) {
|
|||
const domNode = $element[0];
|
||||
|
||||
// render react to DOM
|
||||
<%_ if (generateTranslations) { _%>
|
||||
render(
|
||||
<I18nProvider>
|
||||
<Main title="<%= name %>" httpClient={$http} />
|
||||
</I18nProvider>,
|
||||
domNode
|
||||
);
|
||||
<%_ } else { _%>
|
||||
render(<Main title="<%= name %>" httpClient={$http} />, domNode);
|
||||
<%_ } _%>
|
||||
|
||||
// unmount react on controller destroy
|
||||
$scope.$on('$destroy', () => {
|
||||
|
|
|
@ -9,6 +9,9 @@ import {
|
|||
EuiPageContentBody,
|
||||
EuiText
|
||||
} from '@elastic/eui';
|
||||
<%_ if (generateTranslations) { _%>
|
||||
import { FormattedMessage } from '@kbn/i18n/react';
|
||||
<%_ } _%>
|
||||
|
||||
export class Main extends React.Component {
|
||||
constructor(props) {
|
||||
|
@ -33,19 +36,57 @@ export class Main extends React.Component {
|
|||
<EuiPageBody>
|
||||
<EuiPageHeader>
|
||||
<EuiTitle size="l">
|
||||
<h1>{title} Hello World!</h1>
|
||||
<h1>
|
||||
<%_ if (generateTranslations) { _%>
|
||||
<FormattedMessage
|
||||
id="<%= camelCase(name) %>.helloWorldText"
|
||||
defaultMessage="{title} Hello World!"
|
||||
values={{ title }}
|
||||
/>
|
||||
<%_ } else { _%>
|
||||
{title} Hello World!
|
||||
<%_ } _%>
|
||||
</h1>
|
||||
</EuiTitle>
|
||||
</EuiPageHeader>
|
||||
<EuiPageContent>
|
||||
<EuiPageContentHeader>
|
||||
<EuiTitle>
|
||||
<h2>Congratulations</h2>
|
||||
<h2>
|
||||
<%_ if (generateTranslations) { _%>
|
||||
<FormattedMessage
|
||||
id="<%= camelCase(name) %>.congratulationsTitle"
|
||||
defaultMessage="Congratulations"
|
||||
/>
|
||||
<%_ } else { _%>
|
||||
Congratulations
|
||||
<%_ } _%>
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
</EuiPageContentHeader>
|
||||
<EuiPageContentBody>
|
||||
<EuiText>
|
||||
<h3>You have successfully created your first Kibana Plugin!</h3>
|
||||
<p>The server time (via API call) is {this.state.time || 'NO API CALL YET'}</p>
|
||||
<h3>
|
||||
<%_ if (generateTranslations) { _%>
|
||||
<FormattedMessage
|
||||
id="<%= camelCase(name) %>.congratulationsText"
|
||||
defaultMessage="You have successfully created your first Kibana Plugin!"
|
||||
/>
|
||||
<%_ } else { _%>
|
||||
You have successfully created your first Kibana Plugin!
|
||||
<%_ } _%>
|
||||
</h3>
|
||||
<p>
|
||||
<%_ if (generateTranslations) { _%>
|
||||
<FormattedMessage
|
||||
id="<%= camelCase(name) %>.serverTimeText"
|
||||
defaultMessage="The server time (via API call) is {time}"
|
||||
values={{ time: this.state.time || 'NO API CALL YET' }}
|
||||
/>
|
||||
<%_ } else { _%>
|
||||
The server time (via API call) is {this.state.time || 'NO API CALL YET'}
|
||||
<%_ } _%>
|
||||
</p>
|
||||
</EuiText>
|
||||
</EuiPageContentBody>
|
||||
</EuiPageContent>
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
{
|
||||
"formats": {
|
||||
"number": {
|
||||
"currency": {
|
||||
"style": "currency"
|
||||
},
|
||||
"percent": {
|
||||
"style": "percent"
|
||||
}
|
||||
},
|
||||
"date": {
|
||||
"short": {
|
||||
"month": "numeric",
|
||||
"day": "numeric",
|
||||
"year": "2-digit"
|
||||
},
|
||||
"medium": {
|
||||
"month": "short",
|
||||
"day": "numeric",
|
||||
"year": "numeric"
|
||||
},
|
||||
"long": {
|
||||
"month": "long",
|
||||
"day": "numeric",
|
||||
"year": "numeric"
|
||||
},
|
||||
"full": {
|
||||
"weekday": "long",
|
||||
"month": "long",
|
||||
"day": "numeric",
|
||||
"year": "numeric"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"short": {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric"
|
||||
},
|
||||
"medium": {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric"
|
||||
},
|
||||
"long": {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short"
|
||||
},
|
||||
"full": {
|
||||
"hour": "numeric",
|
||||
"minute": "numeric",
|
||||
"second": "numeric",
|
||||
"timeZoneName": "short"
|
||||
}
|
||||
},
|
||||
"relative": {
|
||||
"years": {
|
||||
"units": "year"
|
||||
},
|
||||
"months": {
|
||||
"units": "month"
|
||||
},
|
||||
"days": {
|
||||
"units": "day"
|
||||
},
|
||||
"hours": {
|
||||
"units": "hour"
|
||||
},
|
||||
"minutes": {
|
||||
"units": "minute"
|
||||
},
|
||||
"seconds": {
|
||||
"units": "second"
|
||||
}
|
||||
}
|
||||
},
|
||||
"messages": {
|
||||
"<%= camelCase(name) %>.congratulationsText": "您已经成功创建第一个 Kibana 插件。",
|
||||
"<%= camelCase(name) %>.congratulationsTitle": "恭喜!",
|
||||
"<%= camelCase(name) %>.helloWorldText": "{title} 您好,世界!",
|
||||
"<%= camelCase(name) %>.serverTimeText": "服务器时间(通过 API 调用)为 {time}"
|
||||
}
|
||||
}
|
|
@ -18,7 +18,6 @@
|
|||
*/
|
||||
|
||||
import path from 'path';
|
||||
import normalize from 'normalize-path';
|
||||
import chalk from 'chalk';
|
||||
|
||||
import {
|
||||
|
@ -27,8 +26,7 @@ import {
|
|||
extractPugMessages,
|
||||
extractHandlebarsMessages,
|
||||
} from './extractors';
|
||||
import { globAsync, readFileAsync } from './utils';
|
||||
import { paths, exclude } from '../../../.i18nrc.json';
|
||||
import { globAsync, readFileAsync, normalizePath } from './utils';
|
||||
import { createFailError, isFailError } from '../run';
|
||||
|
||||
function addMessageToMap(targetMap, key, value) {
|
||||
|
@ -42,11 +40,7 @@ function addMessageToMap(targetMap, key, value) {
|
|||
targetMap.set(key, value);
|
||||
}
|
||||
|
||||
function normalizePath(inputPath) {
|
||||
return normalize(path.relative('.', inputPath));
|
||||
}
|
||||
|
||||
export function filterPaths(inputPaths) {
|
||||
export function filterPaths(inputPaths, paths) {
|
||||
const availablePaths = Object.values(paths);
|
||||
const pathsForExtraction = new Set();
|
||||
|
||||
|
@ -70,16 +64,16 @@ export function filterPaths(inputPaths) {
|
|||
return [...pathsForExtraction];
|
||||
}
|
||||
|
||||
function filterEntries(entries) {
|
||||
function filterEntries(entries, exclude) {
|
||||
return entries.filter(entry =>
|
||||
exclude.every(excludedPath => !normalizePath(entry).startsWith(excludedPath))
|
||||
);
|
||||
}
|
||||
|
||||
export function validateMessageNamespace(id, filePath) {
|
||||
export function validateMessageNamespace(id, filePath, allowedPaths) {
|
||||
const normalizedPath = normalizePath(filePath);
|
||||
|
||||
const [expectedNamespace] = Object.entries(paths).find(([, pluginPath]) =>
|
||||
const [expectedNamespace] = Object.entries(allowedPaths).find(([, pluginPath]) =>
|
||||
normalizedPath.startsWith(`${pluginPath}/`)
|
||||
);
|
||||
|
||||
|
@ -89,7 +83,7 @@ See .i18nrc.json for the list of supported namespaces.`);
|
|||
}
|
||||
}
|
||||
|
||||
export async function extractMessagesFromPathToMap(inputPath, targetMap) {
|
||||
export async function extractMessagesFromPathToMap(inputPath, targetMap, config) {
|
||||
const entries = await globAsync('*.{js,jsx,pug,ts,tsx,html,hbs,handlebars}', {
|
||||
cwd: inputPath,
|
||||
matchBase: true,
|
||||
|
@ -123,7 +117,7 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) {
|
|||
[hbsEntries, extractHandlebarsMessages],
|
||||
].map(async ([entries, extractFunction]) => {
|
||||
const files = await Promise.all(
|
||||
filterEntries(entries).map(async entry => {
|
||||
filterEntries(entries, config.exclude).map(async entry => {
|
||||
return {
|
||||
name: entry,
|
||||
content: await readFileAsync(entry),
|
||||
|
@ -134,7 +128,7 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap) {
|
|||
for (const { name, content } of files) {
|
||||
try {
|
||||
for (const [id, value] of extractFunction(content)) {
|
||||
validateMessageNamespace(id, name);
|
||||
validateMessageNamespace(id, name, config.paths);
|
||||
addMessageToMap(targetMap, id, value);
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
|
@ -31,21 +31,21 @@ const pluginsPaths = [
|
|||
path.join(fixturesPath, 'test_plugin_3'),
|
||||
];
|
||||
|
||||
jest.mock('../../../.i18nrc.json', () => ({
|
||||
const config = {
|
||||
paths: {
|
||||
plugin_1: 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_1',
|
||||
plugin_2: 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_2',
|
||||
plugin_3: 'src/dev/i18n/__fixtures__/extract_default_translations/test_plugin_3',
|
||||
},
|
||||
exclude: [],
|
||||
}));
|
||||
};
|
||||
|
||||
describe('dev/i18n/extract_default_translations', () => {
|
||||
test('extracts messages from path to map', async () => {
|
||||
const [pluginPath] = pluginsPaths;
|
||||
const resultMap = new Map();
|
||||
|
||||
await extractMessagesFromPathToMap(pluginPath, resultMap);
|
||||
await extractMessagesFromPathToMap(pluginPath, resultMap, config);
|
||||
|
||||
expect([...resultMap].sort()).toMatchSnapshot();
|
||||
});
|
||||
|
@ -54,7 +54,7 @@ describe('dev/i18n/extract_default_translations', () => {
|
|||
const [, , pluginPath] = pluginsPaths;
|
||||
|
||||
await expect(
|
||||
extractMessagesFromPathToMap(pluginPath, new Map())
|
||||
extractMessagesFromPathToMap(pluginPath, new Map(), config)
|
||||
).rejects.toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
|
||||
|
@ -64,7 +64,7 @@ describe('dev/i18n/extract_default_translations', () => {
|
|||
__dirname,
|
||||
'__fixtures__/extract_default_translations/test_plugin_2/test_file.html'
|
||||
);
|
||||
expect(() => validateMessageNamespace(id, filePath)).not.toThrow();
|
||||
expect(() => validateMessageNamespace(id, filePath, config.paths)).not.toThrow();
|
||||
});
|
||||
|
||||
test('throws on wrong message namespace', () => {
|
||||
|
@ -73,6 +73,8 @@ describe('dev/i18n/extract_default_translations', () => {
|
|||
__dirname,
|
||||
'__fixtures__/extract_default_translations/test_plugin_2/test_file.html'
|
||||
);
|
||||
expect(() => validateMessageNamespace(id, filePath)).toThrowErrorMatchingSnapshot();
|
||||
expect(() =>
|
||||
validateMessageNamespace(id, filePath, config.paths)
|
||||
).toThrowErrorMatchingSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,5 +18,5 @@
|
|||
*/
|
||||
|
||||
export { filterPaths, extractMessagesFromPathToMap } from './extract_default_translations';
|
||||
export { writeFileAsync } from './utils';
|
||||
export { writeFileAsync, readFileAsync, normalizePath } from './utils';
|
||||
export { serializeToJson, serializeToJson5 } from './serializers';
|
||||
|
|
|
@ -33,6 +33,8 @@ import glob from 'glob';
|
|||
import { promisify } from 'util';
|
||||
import chalk from 'chalk';
|
||||
import parser from 'intl-messageformat-parser';
|
||||
import normalize from 'normalize-path';
|
||||
import path from 'path';
|
||||
|
||||
import { createFailError } from '../run';
|
||||
|
||||
|
@ -288,3 +290,7 @@ export function extractValuesKeysFromNode(node, messageId) {
|
|||
property => (isStringLiteral(property.key) ? property.key.value : property.key.name)
|
||||
);
|
||||
}
|
||||
|
||||
export function normalizePath(inputPath) {
|
||||
return normalize(path.relative('.', inputPath));
|
||||
}
|
||||
|
|
|
@ -22,17 +22,35 @@ import Listr from 'listr';
|
|||
import { resolve } from 'path';
|
||||
|
||||
import { run, createFailError } from './run';
|
||||
import config from '../../.i18nrc.json';
|
||||
import {
|
||||
filterPaths,
|
||||
extractMessagesFromPathToMap,
|
||||
writeFileAsync,
|
||||
readFileAsync,
|
||||
serializeToJson,
|
||||
serializeToJson5,
|
||||
normalizePath,
|
||||
} from './i18n/';
|
||||
|
||||
run(async ({ flags: { path, output, 'output-format': outputFormat } }) => {
|
||||
run(async ({ flags: { path, output, 'output-format': outputFormat, include = [] } }) => {
|
||||
const paths = Array.isArray(path) ? path : [path || './'];
|
||||
const filteredPaths = filterPaths(paths);
|
||||
const additionalI18nConfigPaths = Array.isArray(include) ? include : [include];
|
||||
const mergedConfig = { exclude: [], ...config };
|
||||
|
||||
for (const configPath of additionalI18nConfigPaths) {
|
||||
const additionalConfig = JSON.parse(await readFileAsync(resolve(configPath)));
|
||||
|
||||
for (const [pathNamespace, pathValue] of Object.entries(additionalConfig.paths)) {
|
||||
mergedConfig.paths[pathNamespace] = normalizePath(resolve(configPath, '..', pathValue));
|
||||
}
|
||||
|
||||
for (const exclude of additionalConfig.exclude || []) {
|
||||
mergedConfig.exclude.push(normalizePath(resolve(configPath, '..', exclude)));
|
||||
}
|
||||
}
|
||||
|
||||
const filteredPaths = filterPaths(paths, mergedConfig.paths);
|
||||
|
||||
if (filteredPaths.length === 0) {
|
||||
throw createFailError(
|
||||
|
@ -43,7 +61,7 @@ None of input paths is available for extraction or validation. See .i18nrc.json.
|
|||
|
||||
const list = new Listr(
|
||||
filteredPaths.map(filteredPath => ({
|
||||
task: messages => extractMessagesFromPathToMap(filteredPath, messages),
|
||||
task: messages => extractMessagesFromPathToMap(filteredPath, messages, mergedConfig),
|
||||
title: filteredPath,
|
||||
}))
|
||||
);
|
||||
|
|
Loading…
Reference in a new issue