Skip to content

feat(spa): chrome i18n message-catalog infrastructure (#630) + 1.7.0#649

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/spa-i18n-catalog
May 31, 2026
Merged

feat(spa): chrome i18n message-catalog infrastructure (#630) + 1.7.0#649
MartinCastroAlvarez merged 1 commit into
mainfrom
feat/spa-i18n-catalog

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

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

  1. Server: SpaIndexView resolves the active language via translation.get_language() — falls back to settings.LANGUAGE_CODE when LocaleMiddleware isn't in the stack. Rendered as <meta name="dar-language">.

  2. SPA boot: main.tsx reads the meta tag and calls loadCatalog(language) BEFORE the React root mounts — first paint carries translated strings (no FOUC, no fetch state).

  3. Component use: import t from @dar/ui:

    import { t } from '@dar/ui';
    <Button>{t('Save')}</Button>

    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/)

Locale Keys Notes
en placeholder English round-trips via source-as-key
es 40 Spanish
pt 40 Portuguese (also matches pt-br via stem fallback)
fr 40 French

Cover 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 in i18n.ts, ship.

Tests

  • i18n.test.ts (14 cases) — source-as-key fallback, exact + stem language matching (es-ARes), 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, follows translation.activate(), falls back to LANGUAGE_CODE when no locale active.

Coverage strategy

t(…) wired into ShuttleSelect (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.

Verification

  • poetry run pytest -q64 / 64 ✓ (up from 61; +3 new)
  • pnpm test216 / 216 ✓ (up from 202; +14 new)
  • pnpm -r typecheck
  • pnpm lint
  • pnpm -w build

🤖 Generated with Claude Code

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>
@MartinCastroAlvarez MartinCastroAlvarez merged commit 5c293d9 into main May 31, 2026
6 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the feat/spa-i18n-catalog branch May 31, 2026 12:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[audit] LANGUAGE_CODE / gettext / Accept-Language not honored — SPA UI and translated verbose_name / action descriptions stay English

2 participants