docs(readme): LocaleMiddleware section for translated payload fields (partial #630)#648
Merged
Merged
Conversation
…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
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>
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.
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:gettext_lazy-wrapped strings verbatim through the JSON payload.LocaleMiddlewaremust be inMIDDLEWARE(it's NOT in Django'sstartprojecttemplate by default).With
LocaleMiddlewareenabled, a consumer withverbose_name = _("Cuenta")+LANGUAGE_CODE = "es"(orAccept-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
LocaleMiddlewareactivates the locale for, so thegettext_lazyproxies stringify against the active translation at render time. Confirmed by readingdjango.middleware.locale.LocaleMiddleware.process_request— it callstranslation.activate(language)afterget_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