i18n: externalize UI chrome + Spanish catalog (LC-188) #235

Merged
nrupard merged 1 commit from feat/lc-188-i18n-externalize into main 2026-05-27 20:19:23 +02:00
Owner

Follow-on to the LC-100 i18n framework (#233): externalize the hardcoded English UI-chrome strings across the server-rendered templates and ship a full Spanish catalog. The i18n_catalog CI gate is green (every template-referenced key exists in English; en and es key sets match exactly).

What lands

  • Areas externalized: auth, two_factor, enclave, room, partials (sidebar / nav / switcher), admin, settings, status, saved, invitations, inbox, reminders, home, search, users, dm, activity, voice, and the root templates (layout, not_found, poll / scheduled modals, admin_layout).
  • Catalogs split per area (server/locales/{en,es}/<area>.ftl); Fluent merges them at load, and the gate's coverage check unions all .ftl per locale. 14 area files per locale, en/es at parity.
  • Filter scope: every view module that owns a #[derive(Template)] now imports crate::i18n::filters, so any template or partial can use {{ "key"|t }} without per-area coordination.

Conventions (docs/i18n.md)

  • Keys kebab-case, area-prefixed, no dots; no-space "key"|t (the spaced form parses as bitwise-or and won't compile).
  • A string embedding a dynamic value mid-sentence is split into adjacent keys with the value emitted inline (the t filter takes no args).
  • Plural cases use the existing {% if n == 1 %} two-key pattern.

Out of scope (per the LC-100 spec)

  • ws/ live-update fragments render outside the request task (no locale task-local) -> fall back to English.
  • Outbound email templates; user-authored content (room descriptions, branding body) stays a single string per row.
  • JS string literals inside <script> blocks are not reachable by the template filter.

Notes

  • routes_bookmarks: the empty-state assertion now matches an escaping-stable substring. Askama HTML-escapes the t filter output, so the apostrophe in "haven't" renders as an entity (the string was previously static literal HTML). This is correct/secure behavior; only the test's literal-match needed updating.

Verification

just test + just test-saas green; cargo fmt --all clean; i18n_catalog gate passes; no spaced-pipe filter forms.

Try it

Settings -> Language -> Español localizes the app; clearing it falls back to Accept-Language then English.

Follow-on to the LC-100 i18n framework (#233): externalize the hardcoded English UI-chrome strings across the server-rendered templates and ship a full **Spanish** catalog. The `i18n_catalog` CI gate is green (every template-referenced key exists in English; en and es key sets match exactly). ## What lands - **Areas externalized:** auth, two_factor, enclave, room, partials (sidebar / nav / switcher), admin, settings, status, saved, invitations, inbox, reminders, home, search, users, dm, activity, voice, and the root templates (layout, not_found, poll / scheduled modals, admin_layout). - **Catalogs split per area** (`server/locales/{en,es}/<area>.ftl`); Fluent merges them at load, and the gate's coverage check unions all `.ftl` per locale. 14 area files per locale, en/es at parity. - **Filter scope:** every view module that owns a `#[derive(Template)]` now imports `crate::i18n::filters`, so any template or partial can use `{{ "key"|t }}` without per-area coordination. ## Conventions (docs/i18n.md) - Keys kebab-case, area-prefixed, no dots; **no-space** `"key"|t` (the spaced form parses as bitwise-or and won't compile). - A string embedding a dynamic value mid-sentence is split into adjacent keys with the value emitted inline (the `t` filter takes no args). - Plural cases use the existing `{% if n == 1 %}` two-key pattern. ## Out of scope (per the LC-100 spec) - `ws/` live-update fragments render outside the request task (no locale task-local) -> fall back to English. - Outbound email templates; user-authored content (room descriptions, branding body) stays a single string per row. - JS string literals inside `<script>` blocks are not reachable by the template filter. ## Notes - `routes_bookmarks`: the empty-state assertion now matches an escaping-stable substring. Askama HTML-escapes the `t` filter output, so the apostrophe in "haven't" renders as an entity (the string was previously static literal HTML). This is correct/secure behavior; only the test's literal-match needed updating. ## Verification `just test` + `just test-saas` green; `cargo fmt --all` clean; `i18n_catalog` gate passes; no spaced-pipe filter forms. ## Try it Settings -> Language -> Español localizes the app; clearing it falls back to `Accept-Language` then English.
i18n: externalize UI chrome across templates + Spanish catalog (LC-188)
All checks were successful
check-secrets / Nosey parker (push) Successful in 3s
check-secrets / Kingfisher (push) Successful in 5s
check-secrets / TruffleHog (push) Successful in 5s
check-secrets / Nosey parker (pull_request) Successful in 4s
check-secrets / TruffleHog (pull_request) Successful in 4s
check-secrets / Kingfisher (pull_request) Successful in 6s
Check / clippy + fmt + tests (pull_request) Successful in 2m11s
Create release / Create release from merged PR (pull_request) Has been skipped
464a1e0c7f
Follow-on to the LC-100 framework: externalize the hardcoded English UI-chrome strings across the server-rendered templates and provide full Spanish coverage. The catalog is split per area (one .ftl per area per locale; Fluent merges them at load), and the i18n_catalog CI gate stays green (every referenced key exists in English; en and es key sets match exactly).

Areas externalized: auth, two_factor, enclave, room, partials (sidebar/nav/switcher), admin, settings, status, saved, invitations, inbox, reminders, home, search, users, dm, activity, voice, plus the root templates (layout, not_found, poll/scheduled modals, admin_layout). Every view module that owns a Template now imports crate::i18n::filters so any template/partial can use the `| t` filter (no per-area coordination).

Conventions (docs/i18n.md): keys are kebab-case, area-prefixed, no dots; the no-space `"key"|t` form (the spaced form parses as bitwise-or); strings that embed a dynamic value mid-sentence are split into adjacent keys with the value emitted inline (the `t` filter takes no args); plural cases use the existing `{% if n == 1 %}` two-key pattern.

Out of scope (per the LC-100 spec): ws/ live-update fragments render outside the request task so they fall back to English; outbound email templates; user-authored content (room descriptions, branding body) stays a single string per row. JS string literals inside <script> blocks are not reachable by the template filter and are left as-is.

routes_bookmarks: the empty-state assertion now matches an escaping-stable substring - Askama HTML-escapes the t filter output, so the apostrophe in "haven't" renders as an entity (a string that was previously static literal HTML).

just test + just test-saas green; cargo fmt --all clean; i18n_catalog gate passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
nrupard deleted branch feat/lc-188-i18n-externalize 2026-05-27 20:19:24 +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!235
No description provided.