[I18n] verify select icu-message options are in english (#74963)
Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
parent
fe017f52dd
commit
ee9a8d29b1
|
@ -21,7 +21,6 @@
|
|||
export { extractMessagesFromPathToMap } from './extract_default_translations';
|
||||
// @ts-ignore
|
||||
export { matchEntriesWithExctractors } from './extract_default_translations';
|
||||
// @ts-ignore
|
||||
export { arrayify, writeFileAsync, readFileAsync, normalizePath, ErrorReporter } from './utils';
|
||||
export { serializeToJson, serializeToJson5 } from './serializers';
|
||||
export {
|
||||
|
|
|
@ -21,7 +21,6 @@ import { mockMakeDirAsync, mockWriteFileAsync } from './integrate_locale_files.t
|
|||
|
||||
import path from 'path';
|
||||
import { integrateLocaleFiles, verifyMessages } from './integrate_locale_files';
|
||||
// @ts-expect-error
|
||||
import { normalizePath } from './utils';
|
||||
|
||||
const localePath = path.resolve(__dirname, '__fixtures__', 'integrate_locale_files', 'fr.json');
|
||||
|
|
|
@ -32,7 +32,6 @@ import {
|
|||
readFileAsync,
|
||||
writeFileAsync,
|
||||
verifyICUMessage,
|
||||
// @ts-expect-error
|
||||
} from './utils';
|
||||
|
||||
import { I18nConfig } from './config';
|
||||
|
@ -112,7 +111,7 @@ export function verifyMessages(
|
|||
if (defaultMessage) {
|
||||
try {
|
||||
const message = localizedMessagesMap.get(messageId)!;
|
||||
verifyICUMessage(message);
|
||||
verifyICUMessage(typeof message === 'string' ? message : message?.text);
|
||||
} catch (err) {
|
||||
if (options.ignoreMalformed) {
|
||||
localizedMessagesMap.delete(messageId);
|
||||
|
|
|
@ -9,7 +9,7 @@ exports[`i18n utils should create verbose parser error message 1`] = `
|
|||
"
|
||||
`;
|
||||
|
||||
exports[`i18n utils should normalizePath 1`] = `"src/dev/i18n"`;
|
||||
exports[`i18n utils should normalizePath 1`] = `"src/dev/i18n/utils"`;
|
||||
|
||||
exports[`i18n utils should not escape linebreaks 1`] = `
|
||||
"Text
|
48
src/dev/i18n/utils/index.ts
Normal file
48
src/dev/i18n/utils/index.ts
Normal file
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export {
|
||||
// constants
|
||||
readFileAsync,
|
||||
writeFileAsync,
|
||||
makeDirAsync,
|
||||
accessAsync,
|
||||
globAsync,
|
||||
// functions
|
||||
normalizePath,
|
||||
difference,
|
||||
isPropertyWithKey,
|
||||
isI18nTranslateFunction,
|
||||
node,
|
||||
formatJSString,
|
||||
formatHTMLString,
|
||||
traverseNodes,
|
||||
createParserErrorMessage,
|
||||
checkValuesProperty,
|
||||
extractValueReferencesFromMessage,
|
||||
extractMessageIdFromNode,
|
||||
extractMessageValueFromNode,
|
||||
extractDescriptionValueFromNode,
|
||||
extractValuesKeysFromNode,
|
||||
arrayify,
|
||||
// classes
|
||||
ErrorReporter, // @ts-ignore
|
||||
} from './utils';
|
||||
|
||||
export { verifyICUMessage } from './verify_icu_message';
|
41
src/dev/i18n/utils/intl_types.ts
Normal file
41
src/dev/i18n/utils/intl_types.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export interface OptionalFormatPatternNode {
|
||||
type: 'optionalFormatPattern';
|
||||
selector: string;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface LinePosition {
|
||||
offset: number;
|
||||
line: number;
|
||||
column: number;
|
||||
}
|
||||
|
||||
export interface LocationNode {
|
||||
start: LinePosition;
|
||||
end: LinePosition;
|
||||
}
|
||||
|
||||
export interface SelectFormatNode {
|
||||
type: 'selectFormat';
|
||||
options: OptionalFormatPatternNode[];
|
||||
location: LocationNode;
|
||||
}
|
|
@ -208,28 +208,6 @@ export function checkValuesProperty(prefixedValuesKeys, defaultMessage, messageI
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies valid ICU message.
|
||||
* @param message ICU message.
|
||||
* @param messageId ICU message id
|
||||
* @returns {undefined}
|
||||
*/
|
||||
export function verifyICUMessage(message) {
|
||||
try {
|
||||
parser.parse(message);
|
||||
} catch (error) {
|
||||
if (error.name === 'SyntaxError') {
|
||||
const errorWithContext = createParserErrorMessage(message, {
|
||||
loc: {
|
||||
line: error.location.start.line,
|
||||
column: error.location.start.column - 1,
|
||||
},
|
||||
message: error.message,
|
||||
});
|
||||
throw errorWithContext;
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Extracts value references from the ICU message.
|
||||
* @param message ICU message.
|
91
src/dev/i18n/utils/verify_icu_message.test.ts
Normal file
91
src/dev/i18n/utils/verify_icu_message.test.ts
Normal file
|
@ -0,0 +1,91 @@
|
|||
/*
|
||||
* 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 { verifyICUMessage, checkEnglishOnly } from './verify_icu_message';
|
||||
|
||||
describe('verifyICUMessage', () => {
|
||||
it('passes on plain text', () => {
|
||||
const message = 'plain text here';
|
||||
expect(() => verifyICUMessage(message)).not.toThrowError();
|
||||
});
|
||||
|
||||
it('passes on empty string', () => {
|
||||
const message = '';
|
||||
expect(() => verifyICUMessage(message)).not.toThrowError();
|
||||
});
|
||||
|
||||
it('passes on variable icu-syntax', () => {
|
||||
const message = 'Your regular {foobar}';
|
||||
expect(() => verifyICUMessage(message)).not.toThrowError();
|
||||
});
|
||||
|
||||
it('passes on correct plural icu-syntax', () => {
|
||||
const message = `You have {itemCount, plural,
|
||||
=0 {no items}
|
||||
one {1 item}
|
||||
other {{itemCount} items}
|
||||
}.`;
|
||||
|
||||
expect(() => verifyICUMessage(message)).not.toThrowError();
|
||||
});
|
||||
|
||||
it('throws on malformed string', () => {
|
||||
const message =
|
||||
'CDATA[extended_bounds設定を使用すると、強制的にヒストグラムアグリゲーションを実行し、特定の最小値に対してバケットの作成を開始し、最大値までバケットを作成し続けます。 ]]></target>\n\t\t\t<note>Kibana-SW - String "data.search.aggs.buckets.dateHistogram.extendedBounds.help" in Json.Root "messages\\strings" ';
|
||||
|
||||
expect(() => verifyICUMessage(message)).toThrowError();
|
||||
});
|
||||
|
||||
it('throws on missing curly brackets', () => {
|
||||
const message = `A missing {curly`;
|
||||
|
||||
expect(() => verifyICUMessage(message)).toThrowError();
|
||||
});
|
||||
|
||||
it('throws on incorrect plural icu-syntax', () => {
|
||||
// Notice that small/Medium/Large constants are swapped with the translation strings.
|
||||
const message =
|
||||
'{textScale, select, small {小さい} 中くらい {Medium} 大きい {Large} その他の {{textScale}} }';
|
||||
|
||||
expect(() => verifyICUMessage(message)).toThrowError();
|
||||
});
|
||||
});
|
||||
|
||||
describe('checkEnglishOnly', () => {
|
||||
it('returns true on english only message', () => {
|
||||
const result = checkEnglishOnly('english');
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
it('returns true on empty message', () => {
|
||||
const result = checkEnglishOnly('');
|
||||
|
||||
expect(result).toEqual(true);
|
||||
});
|
||||
it('returns false on message containing numbers', () => {
|
||||
const result = checkEnglishOnly('english 123');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
it('returns false on message containing non-english alphabets', () => {
|
||||
const result = checkEnglishOnly('i am 大きい');
|
||||
|
||||
expect(result).toEqual(false);
|
||||
});
|
||||
});
|
74
src/dev/i18n/utils/verify_icu_message.ts
Normal file
74
src/dev/i18n/utils/verify_icu_message.ts
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
// @ts-ignore
|
||||
import parser from 'intl-messageformat-parser';
|
||||
// @ts-ignore
|
||||
import { createParserErrorMessage, traverseNodes } from './utils';
|
||||
import { SelectFormatNode } from './intl_types';
|
||||
|
||||
export function checkEnglishOnly(message: string) {
|
||||
return /^[a-z]*$/i.test(message);
|
||||
}
|
||||
|
||||
export function verifySelectFormatNode(node: SelectFormatNode) {
|
||||
if (node.type !== 'selectFormat') {
|
||||
throw new parser.SyntaxError(
|
||||
'Unable to verify select format icu-syntax',
|
||||
'selectFormat',
|
||||
node.type,
|
||||
node.location
|
||||
);
|
||||
}
|
||||
|
||||
for (const option of node.options) {
|
||||
if (option.type === 'optionalFormatPattern') {
|
||||
if (!checkEnglishOnly(option.selector)) {
|
||||
throw new parser.SyntaxError(
|
||||
'selectFormat Selector must be in english',
|
||||
'English only selector',
|
||||
option.selector,
|
||||
node.location
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function verifyICUMessage(message: string) {
|
||||
try {
|
||||
const results = parser.parse(message);
|
||||
for (const node of results.elements) {
|
||||
if (node.type === 'argumentElement' && node.format?.type === 'selectFormat') {
|
||||
verifySelectFormatNode(node.format);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === 'SyntaxError') {
|
||||
const errorWithContext = createParserErrorMessage(message, {
|
||||
loc: {
|
||||
line: error.location.start.line,
|
||||
column: error.location.start.column - 1,
|
||||
},
|
||||
message: error.message,
|
||||
});
|
||||
throw errorWithContext;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -30,7 +30,8 @@ import {
|
|||
mergeConfigs,
|
||||
} from './i18n/tasks';
|
||||
|
||||
const skipNoTranslations = ({ config }: { config: I18nConfig }) => !config.translations.length;
|
||||
const skipOnNoTranslations = ({ config }: { config: I18nConfig }) =>
|
||||
!config.translations.length && 'No translations found.';
|
||||
|
||||
run(
|
||||
async ({
|
||||
|
@ -40,6 +41,7 @@ run(
|
|||
'ignore-missing': ignoreMissing,
|
||||
'ignore-unused': ignoreUnused,
|
||||
'include-config': includeConfig,
|
||||
'ignore-untracked': ignoreUntracked,
|
||||
fix = false,
|
||||
path,
|
||||
},
|
||||
|
@ -50,12 +52,13 @@ run(
|
|||
(ignoreIncompatible !== undefined ||
|
||||
ignoreUnused !== undefined ||
|
||||
ignoreMalformed !== undefined ||
|
||||
ignoreMissing !== undefined)
|
||||
ignoreMissing !== undefined ||
|
||||
ignoreUntracked !== undefined)
|
||||
) {
|
||||
throw createFailError(
|
||||
`${chalk.white.bgRed(
|
||||
' I18N ERROR '
|
||||
)} none of the --ignore-incompatible, --ignore-malformed, --ignore-unused or --ignore-missing is allowed when --fix is set.`
|
||||
)} none of the --ignore-incompatible, --ignore-malformed, --ignore-unused or --ignore-missing, --ignore-untracked is allowed when --fix is set.`
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -83,19 +86,20 @@ run(
|
|||
},
|
||||
{
|
||||
title: 'Checking For Untracked Messages based on .i18nrc.json',
|
||||
skip: skipNoTranslations,
|
||||
enabled: (_) => !ignoreUntracked,
|
||||
skip: skipOnNoTranslations,
|
||||
task: ({ config }) =>
|
||||
new Listr(extractUntrackedMessages(srcPaths), { exitOnError: true }),
|
||||
},
|
||||
{
|
||||
title: 'Validating Default Messages',
|
||||
skip: skipNoTranslations,
|
||||
skip: skipOnNoTranslations,
|
||||
task: ({ config }) =>
|
||||
new Listr(extractDefaultMessages(config, srcPaths), { exitOnError: true }),
|
||||
},
|
||||
{
|
||||
title: 'Compatibility Checks',
|
||||
skip: skipNoTranslations,
|
||||
skip: skipOnNoTranslations,
|
||||
task: ({ config }) =>
|
||||
new Listr(
|
||||
checkCompatibility(
|
||||
|
|
|
@ -4852,7 +4852,6 @@
|
|||
"xpack.apm.jvmsTable.nonHeapMemoryColumnLabel": "非ヒープ領域の平均",
|
||||
"xpack.apm.jvmsTable.threadCountColumnLabel": "最大スレッド数",
|
||||
"xpack.apm.kueryBar.disabledPlaceholder": "ここでは検索は利用できません",
|
||||
"xpack.apm.kueryBar.placeholder": "検索 {event, select,\n トランザクション {transactions}\n メトリック: {metric}\n エラー {errors}\n その他 {transactions, errors and metrics}\n } (E.g. {queryExample})",
|
||||
"xpack.apm.license.betaBadge": "ベータ",
|
||||
"xpack.apm.license.betaTooltipMessage": "現在、この機能はベータです。不具合を見つけた場合やご意見がある場合、サポートに問い合わせるか、またはディスカッションフォーラムにご報告ください。",
|
||||
"xpack.apm.license.button": "トライアルを開始",
|
||||
|
|
Loading…
Reference in a new issue