feat(i18n): Fluent + Askama localization framework (LC-100) #233

Merged
nrupard merged 1 commit from feat/lc-100-i18n into main 2026-05-27 18:56:23 +02:00
Owner

The i18n framework for LC-100: Fluent message catalogs + an Askama filter, per-user locale, locale-resolution middleware, a CI key-gate, and contributor docs. Validated end-to-end on the auth (login) and settings pages with English + Spanish.

Externalizing the remaining ~150 templates' strings + completing the Spanish catalog is a large mechanical follow-on tracked separately (see the new issue). The CI gate only checks keys that are referenced, so it stays green as externalization proceeds incrementally.

What lands

  • i18n module (server/src/i18n.rs): catalogs embedded at compile time via fluent-templates::static_loader! (single-binary preserved); t / tn(count) Askama filters that read a tokio::task_local! current locale; resolve() picks user pref -> Accept-Language -> English, with unknown-locale fallback (a render never breaks). Catalogs at server/locales/{en,es}/*.ftl - Fluent merges every .ftl per locale, so the catalog can be split by area.
  • Locale middleware (resolve_locale in server/src/auth.rs), layered inside inject_user, scopes CURRENT_LOCALE for the request. Templates localize with {{ "key"|t }} (note: no space before the pipe - Askama parses "x" | t as bitwise-or) without threading a locale through every view struct. Each view module with such a template does use crate::i18n::filters;.
  • Per-user locale: users.locale column (auth migration 0024) threaded through UserRecord/User + set_user_locale; Settings -> Language dropdown + POST /settings/language (empty = use browser language).
  • CI gate (server/tests/i18n_catalog.rs): fails if a template references a key absent from English; fails if any locale's key set differs from English (full-coverage check).
  • Docs: docs/i18n.md - how to add a locale, key conventions, the filter-import rule, v1 scope.

Acceptance criteria (this framework PR)

  • A user can pick their locale in Settings; the choice persists (users.locale).
  • No preference -> falls back to Accept-Language.
  • An unsupported locale falls back to English without breaking the render.
  • Two locales shipped (English + Spanish), validated on the externalized pages.
  • CI fails if a template references a key missing from the English catalog.
  • Contributor docs explain how to add a locale.
  • All template/view strings externalized -> tracked in the follow-up issue.

Verification

cargo fmt --all clean; i18n unit tests + i18n_catalog gate pass; just test + just test-saas green (incl. migration-list drift fix: auth 0024 added to 22 hand-rolled test pools).

The i18n **framework** for LC-100: Fluent message catalogs + an Askama filter, per-user locale, locale-resolution middleware, a CI key-gate, and contributor docs. Validated end-to-end on the auth (login) and settings pages with English + Spanish. Externalizing the remaining ~150 templates' strings + completing the Spanish catalog is a large mechanical follow-on tracked separately (see the new issue). The CI gate only checks keys that are *referenced*, so it stays green as externalization proceeds incrementally. ## What lands - **i18n module** (`server/src/i18n.rs`): catalogs embedded at compile time via `fluent-templates::static_loader!` (single-binary preserved); `t` / `tn(count)` Askama filters that read a `tokio::task_local!` current locale; `resolve()` picks user pref -> `Accept-Language` -> English, with unknown-locale fallback (a render never breaks). Catalogs at `server/locales/{en,es}/*.ftl` - Fluent merges every `.ftl` per locale, so the catalog can be split by area. - **Locale middleware** (`resolve_locale` in `server/src/auth.rs`), layered inside `inject_user`, scopes `CURRENT_LOCALE` for the request. Templates localize with `{{ "key"|t }}` (note: **no space** before the pipe - Askama parses `"x" | t` as bitwise-or) without threading a locale through every view struct. Each view module with such a template does `use crate::i18n::filters;`. - **Per-user locale**: `users.locale` column (auth migration 0024) threaded through `UserRecord`/`User` + `set_user_locale`; **Settings -> Language** dropdown + `POST /settings/language` (empty = use browser language). - **CI gate** (`server/tests/i18n_catalog.rs`): fails if a template references a key absent from English; fails if any locale's key set differs from English (full-coverage check). - **Docs**: `docs/i18n.md` - how to add a locale, key conventions, the filter-import rule, v1 scope. ## Acceptance criteria (this framework PR) - [x] A user can pick their locale in Settings; the choice persists (`users.locale`). - [x] No preference -> falls back to `Accept-Language`. - [x] An unsupported locale falls back to English without breaking the render. - [x] Two locales shipped (English + Spanish), validated on the externalized pages. - [x] CI fails if a template references a key missing from the English catalog. - [x] Contributor docs explain how to add a locale. - [ ] All template/view strings externalized -> tracked in the follow-up issue. ## Verification `cargo fmt --all` clean; i18n unit tests + `i18n_catalog` gate pass; `just test` + `just test-saas` green (incl. migration-list drift fix: auth `0024` added to 22 hand-rolled test pools).
feat(i18n): Fluent + Askama framework, locale middleware, settings picker (LC-100)
Some checks failed
check-secrets / Nosey parker (push) Successful in 4s
check-secrets / TruffleHog (push) Successful in 7s
check-secrets / Kingfisher (push) Successful in 8s
check-secrets / TruffleHog (pull_request) Successful in 8s
check-secrets / Nosey parker (pull_request) Successful in 8s
check-secrets / Kingfisher (pull_request) Successful in 10s
Check / clippy + fmt + tests (pull_request) Failing after 1m27s
Create release / Create release from merged PR (pull_request) Has been skipped
30efeb81bf
Core i18n machinery for the server-rendered UI. String externalization across all templates continues on this branch; this commit lands the framework + first externalized pages (auth login, settings) with the CI gate green.

- i18n module (server/src/i18n.rs): Fluent catalogs embedded via fluent-templates static_loader!; `t` / `tn(count)` Askama filters reading a tokio task-local current locale; resolve() picks user pref -> Accept-Language -> English with unknown-locale fallback. Catalogs at server/locales/{en,es}/*.ftl (Fluent merges all .ftl per locale, so they can be split by area).
- resolve_locale middleware (server/src/auth.rs), layered inside inject_user, scopes CURRENT_LOCALE for the request so templates localize with `{{ "key"|t }}` (no-space pipe; Askama parses `"x" | t` as bitor) without threading a locale through view structs.
- users.locale column (auth migration 0024) threaded through UserRecord/User + set_user_locale; Settings -> Language dropdown + POST /settings/language.
- CI gate (server/tests/i18n_catalog.rs): every template-referenced key must exist in English; every locale must match English's key set exactly.
- docs/i18n.md: contributor guide (add a locale, key conventions, the filter-import requirement).
- Migration-list drift: 0024 added to the 22 hand-rolled auth-pool test files.

just test + just test-saas green; cargo fmt --all + i18n unit/catalog tests pass.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-100-i18n 2026-05-27 18:56:23 +02:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
psa-systems/lets-chat!233
No description provided.