Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
5 changes: 5 additions & 0 deletions src/dev/i18n/__snapshots__/utils.test.js.snap
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,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]."
`;
37 changes: 29 additions & 8 deletions src/dev/i18n/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ import { createFailError } from '../run';
const ESCAPE_LINE_BREAK_REGEX = /(?<!\\)\\\n/g;
const HTML_LINE_BREAK_REGEX = /[\s]*\n[\s]*/g;

const ARGUMENT_ELEMENT_TYPE = 'argumentElement';

export const readFileAsync = promisify(fs.readFile);
export const writeFileAsync = promisify(fs.writeFile);
export const globAsync = promisify(glob);
Expand Down Expand Up @@ -125,6 +127,32 @@ export function createParserErrorMessage(content, error) {
return `${error.message}:\n${context}`;
}

/**
* Recursively extracts all references from ICU message ast.
*
* Example: `'Removed tag {tag} from {assignmentsLength, plural, one {beat {beatName}} other {# beats}}.'`
*
* @param {any} node
* @param {Set<string>} keys
*/
function extractValueReferencesFromIcuAst(node, keys = new Set()) {
if (Array.isArray(node.elements)) {
for (const element of node.elements.filter(element => element.type === ARGUMENT_ELEMENT_TYPE)) {
keys.add(element.id);

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.
*
Expand Down Expand Up @@ -161,19 +189,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) {
Expand Down
20 changes: 20 additions & 0 deletions src/dev/i18n/utils.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,24 @@ describe('i18n utils', () => {
checkValuesProperty(valuesKeys, defaultMessage, messageId)
).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();
});
});