Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .i18nrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,8 @@
"x-pack/plugins/infra/public/utils/loading_state/loading_result.ts",
"x-pack/plugins/infra/server/graphql/types.ts",
"x-pack/plugins/infra/server/lib/domains/log_entries_domain/log_entries_domain.ts"
],
"translations": [
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

note: we need to track these files somewhere for a compatibility checks and automatic fixes.

"x-pack/plugins/translations/translations/zh-CN.json"
]
}
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@
"@types/eslint": "^4.16.2",
"@types/execa": "^0.9.0",
"@types/fetch-mock": "7.2.1",
"@types/json5": "^0.0.30",
"@types/getopts": "^2.0.0",
"@types/glob": "^5.0.35",
"@types/globby": "^8.0.0",
Expand Down
7 changes: 4 additions & 3 deletions packages/kbn-i18n/GUIDELINE.md
Original file line number Diff line number Diff line change
Expand Up @@ -440,9 +440,10 @@ it('should render normally', async () => {

3. Check functionality of an element (button is clicked, checkbox is checked/unchecked, etc.).

4. Run i18n validation tool and skim through created `en.json`:
```js
node scripts/i18n_check --output ./
4. Run i18n validation/extraction tools and skim through created `en.json`:
```bash
$ node scripts/i18n_check --ignore-missing
$ node scripts/i18n_extract --output-dir ./
```

5. Run linters and type checker as you normally do.
Expand Down
4 changes: 2 additions & 2 deletions src/dev/i18n/serializers/index.js → scripts/i18n_extract.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,5 @@
* under the License.
*/

export { serializeToJson } from './json';
export { serializeToJson5 } from './json5';
require('../src/setup_node_env');
require('../src/dev/run_i18n_extract');
31 changes: 23 additions & 8 deletions src/dev/i18n/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,19 +140,20 @@ The `description` is optional, `values` is optional too unless `defaultMessage`
### Usage

```bash
node scripts/i18n_check --path path/to/plugin --path path/to/another/plugin --output ./translations --output-format json5
node scripts/i18n_extract --path path/to/plugin --path path/to/another/plugin --output-dir ./translations --output-format json5
```

* `path/to/plugin` is an example of path to a directory(-es) where messages searching should start. By default `--path` is `.`, it means that messages from all paths in `.i18nrc.json` will be parsed. Each specified path should start with any path in `.i18nrc.json` or be a part of it.
* `--output` specifies a path to a directory, where `en.json` will be created, if `--output` is not provided, `en.json` generation will be skipped. It is useful if you want to validate i18n engine usage.\
* `--output-dir` specifies a path to a directory, where `en.json` will be created.\
In case of parsing issues, exception with the necessary information will be thrown to console and extraction will be aborted.
* `--output-format` specifies format of generated `en.json` (if `--output` is provided). By default it is `json`. Use it only if you need a JSON5 file.
* `--output-format` specifies format of generated `en.json`. By default it is `json`. Use it only if you need a JSON5 file.
* `--include-config` specifies additional paths to `.i18nrc.json` files (may be useful for 3rd-party plugins)

### Output

`<output_path>/en.json`

The tool generates a JSON/JSON5 file only if `--output` path is provided. It contains injected `formats` object and `messages` object with `id: message` or `id: {text, comment}` pairs. Messages are sorted by id.
The generated JSON/JSON5 file contains `formats` object and `messages` object with `id: message` or `id: {text, comment}` pairs. Messages are sorted by id.

**Example**:

Expand All @@ -169,11 +170,12 @@ The tool generates a JSON/JSON5 file only if `--output` path is provided. It con
}
```

## Locale files verification / integration tool
## Locale files integration tool

### Description

The tool is used for verifying locale file, finding unused / missing messages, key duplications, grouping messages by namespaces and creating JSON files in right folders.
The tool is used for verifying locale file, finding unused / missing messages, key duplications and value references mismatches. If all these
checks are passing, the tool groups messages by namespaces and creates JSON files in right folders.

### Notes

Expand All @@ -182,9 +184,22 @@ The tool throws an exception if `formats` object is missing in locale file.
### Usage

```bash
node scripts/i18n_integrate --path path/to/locale.json
node scripts/i18n_integrate --source path/to/locale.json --target x-pack/plugins/translations/translations/locale.json
```

* `--source` path to the JSON file with translations that should be integrated.
* `--target` defines a single path to the JSON file where translations should be integrated to, path mappings from
[.i18nrc.json](../../../.i18nrc.json) are ignored in this case. It's currently used for integrating of Kibana built-in
translations that are located in a single JSON file within `x-pack/translations` plugin.
* `--dry-run` tells the tool to exit after verification phase and not write translations to the disk.
* `--ignore-incompatible` specifies whether tool should ignore incompatible translations. It may be useful when the code base you're
integrating translations to has changed and some default messages switched to ICU structure that is incompatible with the one used in corresponding translation.
* `--ignore-missing` specifies whether tool should ignore missing translations. It may be useful when the code base you're
integrating translations to has moved forward since the revision translations were created for.
* `--ignore-unused` specifies whether tool should ignore unused translations. It may be useful when the code base you're
integrating translations to has changed and some translations are not needed anymore.
* `--include-config` specifies additional paths to `.i18nrc.json` files (may be useful for 3rd-party plugins)

### Output

The tool generates locale files in plugin folders and few other special locations based on namespaces and corresponding mappings defined in [.i18nrc.json](../../../.i18nrc.json).
Unless `--target` is specified, the tool generates locale files in plugin folders and few other special locations based on namespaces and corresponding mappings defined in [.i18nrc.json](../../../.i18nrc.json).
Original file line number Diff line number Diff line change
Expand Up @@ -142,22 +142,28 @@ Array [
]
`;

exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 1`] = `
exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id, missing id or the incompatible ones 1`] = `
"
Missing translations:
1 missing translation(s):
plugin-1.message-id-2"
`;

exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 2`] = `
exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id, missing id or the incompatible ones 2`] = `
"
Unused translations:
1 unused translation(s):
plugin-1.message-id-3"
`;

exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id and missing id 3`] = `
exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id, missing id or the incompatible ones 3`] = `
"
Unused translations:
1 unused translation(s):
plugin-2.message
Missing translations:
1 missing translation(s):
plugin-2.message-id"
`;

exports[`dev/i18n/integrate_locale_files verifyMessages throws an error for unused id, missing id or the incompatible ones 4`] = `
"
Incompatible translation: some properties are missing in \\"values\\" object (\\"plugin-1.message-id-2\\"): [value].
"
`;
25 changes: 5 additions & 20 deletions src/dev/i18n/__snapshots__/utils.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -20,27 +20,12 @@ exports[`i18n utils should not escape linebreaks 1`] = `

exports[`i18n utils should parse string concatenation 1`] = `"Very long concatenated string"`;

exports[`i18n utils should throw if "values" has a value that is unused in the message 1`] = `
"\\"values\\" object contains unused properties (\\"namespace.message.id\\"):
[url]."
`;
exports[`i18n utils should throw if "values" has a value that is unused in the message 1`] = `"\\"values\\" object contains unused properties (\\"namespace.message.id\\"): [url]."`;

exports[`i18n utils should throw if "values" property is not provided and defaultMessage requires it 1`] = `
"some properties are missing in \\"values\\" object (\\"namespace.message.id\\"):
[username,password,url]."
`;
exports[`i18n utils should throw if "values" property is not provided and defaultMessage requires it 1`] = `"some properties are missing in \\"values\\" object (\\"namespace.message.id\\"): [username,password,url]."`;

exports[`i18n utils should throw if "values" property is provided and defaultMessage doesn't include any references 1`] = `
"\\"values\\" object contains unused properties (\\"namespace.message.id\\"):
[url,username]."
`;
exports[`i18n utils should throw if "values" property is provided and defaultMessage doesn't include any references 1`] = `"\\"values\\" object contains unused properties (\\"namespace.message.id\\"): [url,username]."`;

exports[`i18n utils should throw if some key is missing in "values" 1`] = `
"some properties are missing in \\"values\\" object (\\"namespace.message.id\\"):
[password]."
`;
exports[`i18n utils should throw if some key is missing in "values" 1`] = `"some properties are missing in \\"values\\" object (\\"namespace.message.id\\"): [password]."`;

exports[`i18n utils should throw on wrong nested ICU message 1`] = `
"\\"values\\" object contains unused properties (\\"namespace.message.id\\"):
[third]."
`;
exports[`i18n utils should throw on wrong nested ICU message 1`] = `"\\"values\\" object contains unused properties (\\"namespace.message.id\\"): [third]."`;
93 changes: 93 additions & 0 deletions src/dev/i18n/config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* Licensed to Elasticsearch B.V. under one or more contributor
* license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright
* ownership. Elasticsearch B.V. licenses this file to you under
* the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/

import { resolve } from 'path';

// @ts-ignore
import { normalizePath, readFileAsync } from '.';
// @ts-ignore
import rootConfig from '../../../.i18nrc.json';

export interface I18nConfig {
paths: Record<string, string>;
exclude: string[];
translations: string[];
}

/**
* Merges root .i18nrc.json config with any other additional configs (e.g. from
* third-party plugins).
* @param configPaths List of config paths.
*/
export async function mergeConfigs(configPaths: string | string[] = []) {
const mergedConfig: I18nConfig = { exclude: [], translations: [], ...rootConfig };

for (const configPath of Array.isArray(configPaths) ? configPaths : [configPaths]) {
const additionalConfig: I18nConfig = {
paths: {},
exclude: [],
translations: [],
...JSON.parse(await readFileAsync(resolve(configPath))),
};

for (const [namespace, path] of Object.entries(additionalConfig.paths)) {
mergedConfig.paths[namespace] = normalizePath(resolve(configPath, '..', path));
}

for (const exclude of additionalConfig.exclude) {
mergedConfig.exclude.push(normalizePath(resolve(configPath, '..', exclude)));
}

for (const translations of additionalConfig.translations) {
mergedConfig.translations.push(normalizePath(resolve(configPath, '..', translations)));
}
}

return mergedConfig;
}

/**
* Filters out custom paths based on the paths defined in config and that are
* known to contain i18n strings.
* @param inputPaths List of paths to filter.
* @param config I18n config instance.
*/
export function filterConfigPaths(inputPaths: string[], config: I18nConfig) {
const availablePaths = Object.values(config.paths);
const pathsForExtraction = new Set();

for (const inputPath of inputPaths) {
const normalizedPath = normalizePath(inputPath);

// If input path is the sub path of or equal to any available path, include it.
if (
availablePaths.some(path => normalizedPath.startsWith(`${path}/`) || path === normalizedPath)
) {
pathsForExtraction.add(normalizedPath);
} else {
// Otherwise go through all available paths and see if any of them is the sub
// path of the input path (empty normalized path corresponds to root or above).
availablePaths
.filter(path => !normalizedPath || path.startsWith(`${normalizedPath}/`))
.forEach(ePath => pathsForExtraction.add(ePath));
}
}

return [...pathsForExtraction];
}
File renamed without changes.
34 changes: 0 additions & 34 deletions src/dev/i18n/extract_default_translations.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,30 +42,6 @@ function addMessageToMap(targetMap, key, value, reporter) {
}
}

export function filterPaths(inputPaths, paths) {
const availablePaths = Object.values(paths);
const pathsForExtraction = new Set();

for (const inputPath of inputPaths) {
const normalizedPath = normalizePath(inputPath);

// If input path is the sub path of or equal to any available path, include it.
if (
availablePaths.some(path => normalizedPath.startsWith(`${path}/`) || path === normalizedPath)
) {
pathsForExtraction.add(normalizedPath);
} else {
// Otherwise go through all available paths and see if any of them is the sub
// path of the input path (empty normalized path corresponds to root or above).
availablePaths
.filter(path => !normalizedPath || path.startsWith(`${normalizedPath}/`))
.forEach(ePath => pathsForExtraction.add(ePath));
}
}

return [...pathsForExtraction];
}

function filterEntries(entries, exclude) {
return entries.filter(entry =>
exclude.every(excludedPath => !normalizePath(entry).startsWith(excludedPath))
Expand Down Expand Up @@ -148,13 +124,3 @@ export async function extractMessagesFromPathToMap(inputPath, targetMap, config,
})
);
}

export async function getDefaultMessagesMap(inputPaths, config, reporter) {
const defaultMessagesMap = new Map();

for (const inputPath of filterPaths(inputPaths, config.paths)) {
await extractMessagesFromPathToMap(inputPath, defaultMessagesMap, config, reporter);
}

return defaultMessagesMap;
}
2 changes: 1 addition & 1 deletion src/dev/i18n/extractors/react.js
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ export function extractIntlMessages(node) {
: undefined;

if (!messageId) {
createFailError(`Empty "id" value in intl.formatMessage() is not allowed.`);
throw createFailError(`Empty "id" value in intl.formatMessage() is not allowed.`);
}

const message = messageProperty
Expand Down
6 changes: 5 additions & 1 deletion src/dev/i18n/index.js → src/dev/i18n/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
* under the License.
*/

export { filterPaths, extractMessagesFromPathToMap } from './extract_default_translations';
// @ts-ignore
export { extractMessagesFromPathToMap } from './extract_default_translations';
// @ts-ignore
export { writeFileAsync, readFileAsync, normalizePath, ErrorReporter } from './utils';
export { serializeToJson, serializeToJson5 } from './serializers';
export { I18nConfig, filterConfigPaths, mergeConfigs } from './config';
export { integrateLocaleFiles } from './integrate_locale_files';
Loading