Skip to content

docs(readme): LocaleMiddleware section for translated payload fields (partial #630)#648

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
docs/i18n-localemiddleware
May 31, 2026
Merged

docs(readme): LocaleMiddleware section for translated payload fields (partial #630)#648
MartinCastroAlvarez merged 1 commit into
mainfrom
docs/i18n-localemiddleware

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

Partial closure of #630 — documents the wire-side half of the gap. The SPA chrome catalog stays open as the remaining piece.

What's documented

A new "Translated verbose_name / help_text / action descriptions (LocaleMiddleware)" subsection under "Hardening" / cross-origin guidance, explaining that:

  1. The API package surfaces gettext_lazy-wrapped strings verbatim through the JSON payload.
  2. For those proxies to resolve to the active request's language, Django's LocaleMiddleware must be in MIDDLEWARE (it's NOT in Django's startproject template by default).
  3. The SPA's own chrome strings ("Add", "Search", "Loading…") are still hard-coded English; that's the remaining half of [audit] LANGUAGE_CODE / gettext / Accept-Language not honored — SPA UI and translated verbose_name / action descriptions stay English #630.

With LocaleMiddleware enabled, a consumer with verbose_name = _("Cuenta") + LANGUAGE_CODE = "es" (or Accept-Language: es) sees "Cuenta" in the SPA payload. No code change in this package needed.

Verification

The API package's payload-building runs inside the request-response cycle that LocaleMiddleware activates the locale for, so the gettext_lazy proxies stringify against the active translation at render time. Confirmed by reading django.middleware.locale.LocaleMiddleware.process_request — it calls translation.activate(language) after get_language_from_request(request), before the view runs.

Why this is doc-only

The package shouldn't enforce LocaleMiddleware (it's a consumer's stack decision and Django itself doesn't require it). But it should TELL consumers when it's needed — that's what this section does.

🤖 Generated with Claude Code

…630)

Documents the requirement for ``django.middleware.locale.LocaleMiddleware``
in the consumer's MIDDLEWARE stack to make the API's
``gettext_lazy``-wrapped ``verbose_name`` / ``help_text`` /
``@admin.action(description=…)`` proxies resolve to the active
request locale.

The API package itself has no ``activate()`` call — by design, it
piggybacks on Django's standard ``LocaleMiddleware.process_request``
(which calls ``translation.activate(language)`` after
``get_language_from_request``). With the middleware enabled, the
JSON payload returns ``verbose_name`` etc. in the user's language;
without it, the proxies stay un-resolved and read as English
regardless of ``Accept-Language``.

Django's ``startproject`` template does NOT include
``LocaleMiddleware`` by default, so a stock consumer who adds
this package gets the English-only behaviour and a confusing gap
between Django's own chrome (which IS translated when
``LocaleMiddleware`` is present in other paths) and the SPA's
payloads. Calling this out in the README + showing the exact
``MIDDLEWARE`` line is the smallest doc change that closes the
"why aren't my translated verbose_names showing up?" question.

Half of #630 — the wire-side gap — is now documented. The SPA's
own chrome strings (``"Add"`` / ``"Search"`` / ``"Loading…"``) are
still hard-coded English; the message-catalog work is staged for a
separate session.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@MartinCastroAlvarez MartinCastroAlvarez merged commit 1bc79e9 into main May 31, 2026
6 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the docs/i18n-localemiddleware branch May 31, 2026 12:32
MartinCastroAlvarez added a commit that referenced this pull request May 31, 2026
…649)

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: Martin Castro Laminrs <mcastro@laminr.ai>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.

2 participants