refactor(i18n): make the English text the message id

Merged
SA
2mo ago
+182479-409986

What & why

Follows #7091 (merged). Moves every surface to Lingui's default mode, where the English text is the message id, and deletes the machinery explicit ids required.

With explicit ids, editing English under a stable id left every translation in place and non-empty, so compile --strict saw nothing wrong and the old wording shipped. A custom stale-translation audit (plus an exemptions file and a full-history checkout in CI) existed only to catch that. With the text as the id, an English edit creates a new entry that is empty in every locale and the strict compile catches it natively. Identical English also collapses into one entry: 6,883 catalog entries become 4,894.

What changed:

  • Codemod over 971 source files. <Trans id="…"> → <Trans>; t({ id, message }) and msg({ id, message }) drop the id; runtime i18n._({ id, message }) becomes i18n._(msg({ message })), with values spread into the descriptor where present, since Lingui 6 types _ as taking a descriptor alone. Three hand-built descriptor objects that were never extracted become msg() calls and are translated for the first time. Descriptors whose message is a variable keep their runtime id; they were never in the catalog.
  • Catalogs re-keyed by English text. A carry-over script mapped every old id to its text and voted where several ids shared one text: 78,096 translations carried over, 703 votes had disagreeing old translations (mostly the same word translated inconsistently, e.g. German "Home" as "Start" under five ids and "Home" under one), ties preferring a translation that is not just the English. 13 strings that first entered the catalog here were translated by hand across all 16 locales.
  • Contexts where one English word meant two things. "View" (app menu vs. button), "Plan" (billing vs. agent mode), "Exploring" (roadmap status vs. agent activity), "Open" (pull-request status vs. action), "Home" (directory vs. navigation), and "Unknown" (a person vs. a branch or task, which French genders differently) carry a context and keep their separate translations.
  • Every build runs the macro. The renderer, the Next apps, and mobile already did. The Electron main process gets linguiMacroPlugin in apps/desktop/vite/helpers.ts; the bun-compiled CLI, host-service, and desktop bundle:cli get a Bun bundler plugin exported from @superset/i18n/bun-plugin (forwarded through a new plugins option in cli-framework); the API's Next config gets the same SWC plugin its siblings use. apps/desktop/scripts/verify-single-flight.ts mocks the macro alongside its electron mocks.
  • Deleted: check-stale-translations.ts and its test, locales/en-only-changes.txt, the fetch-depth: 0 checkout in CI's Lint job. check is now extract + strict compile.
  • One test shim for @lingui/core/macro. bun test runs uncompiled source, where the macro entry point throws. packages/i18n/test-setup.ts derives ids with Lingui's own generateMessageId, so tests that load a real catalog get real translations; shared, cli, marketing, and mobile preload it by relative path. Desktop and ui keep their existing React-macro shims. @lingui/core is declared where the macro is now imported (packages/shared, apps/docs).
  • Catalog integrity test. packages/i18n/test/catalog-integrity.test.ts checks every translation keeps its {placeholders}, <0>…</0> tag markers, and ICU plural/select shape. Strict compile only checks that an entry exists; this is the validation the deleted translator used to do for machine fills, now applied to hand-written ones. It found two messages whose Japanese, Chinese, and French translations had lost their second line on main (the merge confirmation's "This merges into {2}…" and the sign-in legal text's Terms and Privacy links); those are fixed here, plus the German versions that were still English.
  • Docs: AGENTS.md and packages/i18n/README.md describe the text-as-id workflow and when to add a context.

The 120k-line catalog diff is the re-keying itself; the compiled messages.ts files keep the same message texts under hashed ids.

How I tested it

  • bun run check:i18n: strict compile passes with zero missing entries across 16 locales, and a second run leaves the catalogs unchanged.
  • bun run typecheck across the monorepo: 0 errors. bun run lint: clean. bun run test (turbo): all packages pass, including desktop (3,286), shared (920), ui (60), i18n (100).
  • The main-process plugin run directly over src/main/lib/menu.ts emits 21 hashed ids and leaves no msg() calls.
  • German spot checks after carry-over: plain "View" → Ansehen, menu "View" → Ansicht; plain "Plan" → Plan, billing "Plan" → Tarif; "Restart" → Neu starten (the tie-break no longer picks the untranslated copy).

Checklist

  • PR title follows conventional commits (type(scope): subject)
  • bun run lint and bun run typecheck pass (CI fails on lint warnings too)
  • "Allow edits from maintainers" is checked on fork PRs

https://claude.ai/code/session_013UiC9jnCiE3bpgg6YzPje1