Repository navigation
test(i18n): catch locale dict dumps and check explicit ids against English #9191
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| import { describe, expect, test } from "bun:test"; | ||
| import { readFileSync } from "node:fs"; | ||
| import { join } from "node:path"; | ||
| import { compileMessageOrThrow } from "@lingui/message-utils/compileMessage"; | ||
| import { SUPPORTED_LOCALES } from "../src/locales"; | ||
|
|
||
| // A translation may reword anything except the parts the runtime substitutes: | ||
|
|
@@ -55,30 +56,82 @@ const icu = (t: string) => | |
| .sort() | ||
| .join(); | ||
|
|
||
| function strayBraces(t: string): boolean { | ||
| let found = false; | ||
| try { | ||
| compileMessageOrThrow(t, (text) => { | ||
| found ||= /[{}]/.test(text); | ||
| return text; | ||
| }); | ||
| } catch { | ||
| return true; | ||
| } | ||
| return found; | ||
| } | ||
|
|
||
| function integrityError(source: string, translation: string): string | null { | ||
| for (const name of placeholders(source)) { | ||
| if (!placeholders(translation).has(name)) return `missing {${name}}`; | ||
| } | ||
| if (tags(source) !== tags(translation)) return "tag markers differ"; | ||
| if (icu(source) !== icu(translation)) return "ICU structure differs"; | ||
| if (strayBraces(translation) && !strayBraces(source)) | ||
| return "braces the source does not have"; | ||
| return null; | ||
| } | ||
|
|
||
| const readCatalog = (locale: string) => | ||
| parsePo(readFileSync(join(LOCALES_DIR, locale, "messages.po"), "utf8")); | ||
|
|
||
| // An explicit-id message keeps its English in the source catalog's msgstr. | ||
| const english = new Map( | ||
| readCatalog(SOURCE_LOCALE).map((entry) => [ | ||
| entry.msgid, | ||
| entry.msgstr || entry.msgid, | ||
| ]), | ||
| ); | ||
| const sourceText = (msgid: string) => english.get(msgid) ?? msgid; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use If the English Use the English
|
||
|
|
||
| describe("catalog integrity", () => { | ||
| for (const locale of SUPPORTED_LOCALES) { | ||
| if (locale === SOURCE_LOCALE) continue; | ||
| test(`${locale} keeps placeholders, tags, and ICU shape`, () => { | ||
| const broken = parsePo( | ||
| readFileSync(join(LOCALES_DIR, locale, "messages.po"), "utf8"), | ||
| ) | ||
| const broken = readCatalog(locale) | ||
| .filter((entry) => entry.msgstr !== "") | ||
| .flatMap((entry) => { | ||
| const problem = integrityError(entry.msgid, entry.msgstr); | ||
| const problem = integrityError(sourceText(entry.msgid), entry.msgstr); | ||
| return problem | ||
| ? [`${JSON.stringify(entry.msgid.slice(0, 70))}: ${problem}`] | ||
| : []; | ||
| }); | ||
| expect(broken).toEqual([]); | ||
| }); | ||
| } | ||
|
|
||
| test("rejects a dict dump and a dropped placeholder behind an explicit id", () => { | ||
| expect( | ||
| integrityError( | ||
| "Hello {name}", | ||
| '{"en": "Hello {name}", "fr": "Bonjour {name}"}', | ||
| ), | ||
| ).toBe("braces the source does not have"); | ||
| expect( | ||
| integrityError( | ||
| sourceText("serverError.plugins.marketplaceBuiltinRemove"), | ||
| "Вбудований, тому його не можна видалити", | ||
| ), | ||
| ).toBe("missing {name}"); | ||
| expect( | ||
| integrityError( | ||
| "{count, plural, one {# in {name}} other {# in {name}}}", | ||
| '{count, plural, one {{"en": "# in {name}"}} other {# у {name}}}', | ||
| ), | ||
| ).toBe("braces the source does not have"); | ||
| expect( | ||
| integrityError( | ||
| "{count, plural, one {# file} other {# files}}", | ||
| "{count, plural, one {# файл} other {# файлів}", | ||
| ), | ||
| ).toBe("braces the source does not have"); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Check formatted arguments as well as text.
For source
Helloand translationBonjour {price, number}, this check returns no error. The compiler passes only text tokens to the callback, and the other checks do not inspectnumberarguments. The previous brace scan rejected this translation. At runtime, the missingpricerenders asNaN. Compare formatted argument tokens against the source too. (raw.githubusercontent.com)