Skip to content

Commit c97e857

Browse files
authored
[Tools] Validate values in nested ICU messages (#25378) (#25586)
1 parent 8e934e9 commit c97e857

File tree

3 files changed

+59
-8
lines changed

3 files changed

+59
-8
lines changed

src/dev/i18n/__snapshots__/utils.test.js.snap

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,8 @@ exports[`i18n utils should throw if some key is missing in "values" 1`] = `
3737
"some properties are missing in \\"values\\" object (\\"namespace.message.id\\"):
3838
[password]."
3939
`;
40+
41+
exports[`i18n utils should throw on wrong nested ICU message 1`] = `
42+
"\\"values\\" object contains unused properties (\\"namespace.message.id\\"):
43+
[third]."
44+
`;

src/dev/i18n/utils.js

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import { createFailError } from '../run';
3939
const ESCAPE_LINE_BREAK_REGEX = /(?<!\\)\\\n/g;
4040
const HTML_LINE_BREAK_REGEX = /[\s]*\n[\s]*/g;
4141

42+
const ARGUMENT_ELEMENT_TYPE = 'argumentElement';
43+
4244
export const readFileAsync = promisify(fs.readFile);
4345
export const writeFileAsync = promisify(fs.writeFile);
4446
export const globAsync = promisify(glob);
@@ -126,6 +128,37 @@ export function createParserErrorMessage(content, error) {
126128
return `${error.message}:\n${context}`;
127129
}
128130

131+
/**
132+
* Recursively extracts all references from ICU message ast.
133+
*
134+
* Example: `'Removed tag {tag} from {assignmentsLength, plural, one {beat {beatName}} other {# beats}}.'`
135+
*
136+
* @param {any} node
137+
* @param {Set<string>} keys
138+
*/
139+
function extractValueReferencesFromIcuAst(node, keys = new Set()) {
140+
if (Array.isArray(node.elements)) {
141+
for (const element of node.elements) {
142+
if (element.type !== ARGUMENT_ELEMENT_TYPE) {
143+
continue;
144+
}
145+
146+
keys.add(element.id);
147+
148+
// format contains all specific parameters for complex argumentElements
149+
if (element.format && Array.isArray(element.format.options)) {
150+
for (const option of element.format.options) {
151+
extractValueReferencesFromIcuAst(option, keys);
152+
}
153+
}
154+
}
155+
} else if (node.value) {
156+
extractValueReferencesFromIcuAst(node.value, keys);
157+
}
158+
159+
return [...keys];
160+
}
161+
129162
/**
130163
* Checks whether values from "values" and "defaultMessage" correspond to each other.
131164
*
@@ -162,19 +195,12 @@ export function checkValuesProperty(valuesKeys, defaultMessage, messageId) {
162195
throw error;
163196
}
164197

165-
const ARGUMENT_ELEMENT_TYPE = 'argumentElement';
166-
167198
// skip validation if intl-messageformat-parser didn't return an AST with nonempty elements array
168199
if (!defaultMessageAst || !defaultMessageAst.elements || !defaultMessageAst.elements.length) {
169200
return;
170201
}
171202

172-
const defaultMessageValueReferences = defaultMessageAst.elements.reduce((keys, element) => {
173-
if (element.type === ARGUMENT_ELEMENT_TYPE) {
174-
keys.push(element.id);
175-
}
176-
return keys;
177-
}, []);
203+
const defaultMessageValueReferences = extractValueReferencesFromIcuAst(defaultMessageAst);
178204

179205
const missingValuesKeys = difference(defaultMessageValueReferences, valuesKeys);
180206
if (missingValuesKeys.length) {

src/dev/i18n/utils.test.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,26 @@ describe('i18n utils', () => {
155155
).toThrowErrorMatchingSnapshot();
156156
});
157157

158+
test('should parse nested ICU message', () => {
159+
const valuesKeys = ['first', 'second', 'third'];
160+
const defaultMessage = 'Test message {first, plural, one {{second}} other {{third}}}';
161+
const messageId = 'namespace.message.id';
162+
163+
expect(() =>
164+
checkValuesProperty(valuesKeys, defaultMessage, messageId)
165+
).not.toThrow();
166+
});
167+
168+
test(`should throw on wrong nested ICU message`, () => {
169+
const valuesKeys = ['first', 'second', 'third'];
170+
const defaultMessage = 'Test message {first, plural, one {{second}} other {other}}';
171+
const messageId = 'namespace.message.id';
172+
173+
expect(() =>
174+
checkValuesProperty(valuesKeys, defaultMessage, messageId)
175+
).toThrowErrorMatchingSnapshot();
176+
});
177+
158178
test(`should parse string concatenation`, () => {
159179
const source = `
160180
i18n('namespace.id', {

0 commit comments

Comments
 (0)