feat(spa): chrome i18n message-catalog infrastructure (#630) + 1.7.0#649
Merged
Conversation
Closes #630. The wire-side half landed in 1.6.0 via PR #648 (documenting LocaleMiddleware so the API's `gettext_lazy`-wrapped `verbose_name` / `help_text` resolve per the active locale). This PR ships the SPA-side half: the chrome's own strings ("Add", "Save", "Search", "Loading…") now flow through a message catalog that hydrates from the request's active locale. ## How it works 1. **Server**: ``SpaIndexView`` resolves the active language via ``translation.get_language()`` — falls back to ``settings.LANGUAGE_CODE`` when ``LocaleMiddleware`` isn't in the stack. The shell template renders it as ``<meta name="dar-language">``. 2. **SPA boot**: ``main.tsx`` reads the meta tag and calls ``loadCatalog(language)`` BEFORE the React root mounts — so the first paint already carries translated strings (no FOUC, no fetch / loading state). 3. **Component use**: any package imports ``t`` from ``@dar/ui`` and wraps user-visible strings: ```tsx import { t } from '@dar/ui'; <Button>{t('Save')}</Button> ``` ``t(en)`` looks the English source up in the active catalog; missing key → returns ``en`` itself. Source-as-key (gettext convention) so refactoring a JSX string doesn't require a parallel catalog-key change, and gradual coverage degrades gracefully. ## Catalogs shipped JSON catalogs bundled in the SPA build under ``packages/ui/src/i18n/``: - ``en.json`` — placeholder; English round-trips via source-as-key. - ``es.json`` — Spanish (40 keys). - ``pt.json`` — Portuguese / pt-br (40 keys; ``pt-br`` aliased). - ``fr.json`` — French (40 keys). The set covers the high-visibility chrome strings — Add, Save, Edit, Delete, Cancel, Refresh, Loading…, Clear all, Customize columns, Layout, Sign in / out, History, the shuttle widget's Available / Chosen / Choose all / Remove all / Filter, the list-action redirect banner, the raw_id lookup link, etc. Adding a new language: drop a JSON file, import it in ``i18n.ts``, ship. ## Tests - ``frontend/packages/ui/src/i18n.test.ts`` — 14 cases pinning source-as-key fallback, exact + stem language matching (``es-AR`` → ``es``), case-insensitive code, unknown-code no-op, sanity-check that ES / PT / FR actually translate the expected keys, English source unchanged. - ``tests/test_active_language.py`` — 3 cases pinning that the ``<meta name="dar-language">`` tag is emitted, that it follows ``translation.activate()``, and that it falls back to ``LANGUAGE_CODE`` when no locale is active. ## Coverage strategy Wired ``t(…)`` into ``ShuttleSelect`` (the high-traffic new widget from #627) as the proof-of-life. Remaining JSX strings stay English until reached — each ``t(en)`` migration is independent and ships incrementally. The README's i18n section (1.6.0) already explains the pattern for consumers. ## Verification - ``poetry run pytest -q`` — **64 / 64 ✓** (up from 61; +3 new in ``test_active_language.py``) - ``pnpm test`` — **216 / 216 ✓** (up from 202; +14 new in ``i18n.test.ts``) - ``pnpm -r typecheck`` ✓ - ``pnpm lint`` ✓ - ``pnpm -w build`` ✓ ## Minor bump rationale ``1.6.0`` → ``1.7.0``. New user-visible capability (chrome translation) per SemVer's "additive features" guideline. Pure additive — English shops see no behaviour change. Closes #630. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #630. The wire-side half landed in 1.6.0 via PR #648 (LocaleMiddleware docs). This PR ships the SPA-side half: chrome strings ("Add", "Save", "Search", "Loading…") flow through a message catalog hydrated from the request's active locale.
How it works
Server:
SpaIndexViewresolves the active language viatranslation.get_language()— falls back tosettings.LANGUAGE_CODEwhenLocaleMiddlewareisn't in the stack. Rendered as<meta name="dar-language">.SPA boot:
main.tsxreads the meta tag and callsloadCatalog(language)BEFORE the React root mounts — first paint carries translated strings (no FOUC, no fetch state).Component use: import
tfrom@dar/ui:Source-as-key (gettext convention). Missing key → returns the English source. Refactoring a JSX string doesn't require a parallel catalog-key change; gradual coverage degrades gracefully.
Catalogs shipped (
frontend/packages/ui/src/i18n/)enesptpt-brvia stem fallback)frCover the high-visibility chrome strings — Add, Save, Edit, Delete, Cancel, Refresh, Loading…, Clear all, Customize columns, Layout, Sign in / out, History, the shuttle widget's Available / Chosen / Choose all / Remove all / Filter, the list-action redirect banner, the raw_id lookup link.
Adding a new language: drop a JSON file under
packages/ui/src/i18n/, import ini18n.ts, ship.Tests
i18n.test.ts(14 cases) — source-as-key fallback, exact + stem language matching (es-AR→es), case-insensitive code, unknown-code no-op, sanity ES/PT/FR translate expected keys, English source unchanged.test_active_language.py(3 cases) — meta tag emitted, followstranslation.activate(), falls back toLANGUAGE_CODEwhen no locale active.Coverage strategy
t(…)wired intoShuttleSelect(high-traffic new widget from #627) as the proof-of-life. Remaining JSX strings stay English until reached — eacht(en)migration is independent and ships incrementally.Verification
poetry run pytest -q— 64 / 64 ✓ (up from 61; +3 new)pnpm test— 216 / 216 ✓ (up from 202; +14 new)pnpm -r typecheck✓pnpm lint✓pnpm -w build✓🤖 Generated with Claude Code