Preserve ICU arguments and selectors during pseudo-localization

Signed-off-by: Rudy Celekli <47457359+rudycelekli@users.noreply.github.com>
This commit is contained in:
Rudy Celekli
2026-10-05 11:59:07 -04:00
parent 83294689da
commit fa9fb98548
+20 -8
View File
@@ -126,16 +126,28 @@ new Intl.ListFormat(locale, { type: 'conjunction' }).format(['Ana', 'Luis', 'Mei
### Pseudo-Localization in CI: Catch It Before Translators Do
```javascript
// Pseudo-locale transform: "Save changes" → "[!!! Šàvé çhàñĝéš one two !!!]"
// - Accented chars expose encoding bugs
// - +40% padding exposes truncation and fixed-width layouts
// - Brackets expose concatenation (fragments render as separate bracketed chunks)
// - Untransformed text on screen = hardcoded string, fail the check
// Transform literal AST nodes, never ICU arguments, selectors or skeletons.
import { parse, TYPE } from '@formatjs/icu-messageformat-parser';
import { printAST } from '@formatjs/icu-messageformat-parser/printer.js';
export function pseudoLocalize(message) {
const map = { a: 'à', e: 'é', i: 'î', o: 'ö', u: 'ü', c: 'ç', n: 'ñ', s: 'š', g: 'ĝ' };
const swapped = message.replace(/[aeioucnsg]/g, (ch) => map[ch] ?? ch);
const padding = ' one two three'.slice(0, Math.ceil(message.length * 0.4));
return `[!!! ${swapped}${padding} !!!]`;
function transform(elements) {
for (const element of elements) {
if (element.type === TYPE.literal) {
const swapped = element.value.replace(/[aeioucnsg]/g, (ch) => map[ch] ?? ch);
const extra = Math.ceil(element.value.length * 0.4);
element.value = swapped + ' ~'.repeat(Math.ceil(extra / 2)).slice(0, extra);
} else if (element.type === TYPE.select || element.type === TYPE.plural) {
for (const option of Object.values(element.options)) transform(option.value);
} else if (element.type === TYPE.tag) {
transform(element.children);
}
}
}
const ast = parse(message);
transform(ast);
return `[!!! ${printAST(ast)} !!!]`;
}
```