diff --git a/src/dev/i18n/__snapshots__/utils.test.js.snap b/src/dev/i18n/__snapshots__/utils.test.js.snap index b7fa8e2deaa8b..7118c1e772fd8 100644 --- a/src/dev/i18n/__snapshots__/utils.test.js.snap +++ b/src/dev/i18n/__snapshots__/utils.test.js.snap @@ -37,3 +37,8 @@ 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]." +`; diff --git a/src/dev/i18n/utils.js b/src/dev/i18n/utils.js index 041dccb333585..7357eca538254 100644 --- a/src/dev/i18n/utils.js +++ b/src/dev/i18n/utils.js @@ -39,6 +39,8 @@ import { createFailError } from '../run'; const ESCAPE_LINE_BREAK_REGEX = /(?} keys + */ +function extractValueReferencesFromIcuAst(node, keys = new Set()) { + if (Array.isArray(node.elements)) { + for (const element of node.elements) { + if (element.type !== ARGUMENT_ELEMENT_TYPE) { + continue; + } + + keys.add(element.id); + + // format contains all specific parameters for complex argumentElements + if (element.format && Array.isArray(element.format.options)) { + for (const option of element.format.options) { + extractValueReferencesFromIcuAst(option, keys); + } + } + } + } else if (node.value) { + extractValueReferencesFromIcuAst(node.value, keys); + } + + return [...keys]; +} + /** * Checks whether values from "values" and "defaultMessage" correspond to each other. * @@ -162,19 +195,12 @@ export function checkValuesProperty(valuesKeys, defaultMessage, messageId) { throw error; } - const ARGUMENT_ELEMENT_TYPE = 'argumentElement'; - // skip validation if intl-messageformat-parser didn't return an AST with nonempty elements array if (!defaultMessageAst || !defaultMessageAst.elements || !defaultMessageAst.elements.length) { return; } - const defaultMessageValueReferences = defaultMessageAst.elements.reduce((keys, element) => { - if (element.type === ARGUMENT_ELEMENT_TYPE) { - keys.push(element.id); - } - return keys; - }, []); + const defaultMessageValueReferences = extractValueReferencesFromIcuAst(defaultMessageAst); const missingValuesKeys = difference(defaultMessageValueReferences, valuesKeys); if (missingValuesKeys.length) { diff --git a/src/dev/i18n/utils.test.js b/src/dev/i18n/utils.test.js index 4d24f79aa5596..a6df4fa1d346d 100644 --- a/src/dev/i18n/utils.test.js +++ b/src/dev/i18n/utils.test.js @@ -155,6 +155,26 @@ describe('i18n utils', () => { ).toThrowErrorMatchingSnapshot(); }); + test('should parse nested ICU message', () => { + const valuesKeys = ['first', 'second', 'third']; + const defaultMessage = 'Test message {first, plural, one {{second}} other {{third}}}'; + const messageId = 'namespace.message.id'; + + expect(() => + checkValuesProperty(valuesKeys, defaultMessage, messageId) + ).not.toThrow(); + }); + + test(`should throw on wrong nested ICU message`, () => { + const valuesKeys = ['first', 'second', 'third']; + const defaultMessage = 'Test message {first, plural, one {{second}} other {other}}'; + const messageId = 'namespace.message.id'; + + expect(() => + checkValuesProperty(valuesKeys, defaultMessage, messageId) + ).toThrowErrorMatchingSnapshot(); + }); + test(`should parse string concatenation`, () => { const source = ` i18n('namespace.id', {