Skip to content

Foundation: multi-language support (i18n) for the HFS UI (en/es/de) #187

Description

@smunini

Summary

Lay the foundation for multi-language support across HFS, so the user
interface can be presented in multiple languages — starting with English,
Spanish, and German
— and so every layer from the browser to the terminology
backend agrees on the requested language.

Today HFS has no UI i18n layer: UI chrome and error prose are English-only, and
there is no locale-negotiation or message-catalog machinery. This issue
establishes the structure, conventions, and rules of the road — plus a
working skeleton and a discussion document — so the first localization PRs set
the right precedent rather than an accidental one. As with the UI foundation
(#186), the goal is not a fully translated product; it is to answer, with a
running structure and a written rationale: "When we add a language to HFS,
where does everything go, and why?"

The one thing HFS already does well here is terminology: HTS imports
multi-language SNOMED CT (and LOINC linguistic variants) and serves the right
term per displayLanguage / Accept-Language. This foundation builds on that
rather than reinventing it.

Motivation

  • Our UI must be usable by Spanish- and German-speaking users, not just English.
  • Localization is not one thing — UI chrome, locale negotiation, API/error
    messages, FHIR content pass-through, terminology display, and formatting are
    separate layers. Without an agreed model they get conflated, producing bugs
    like a Spanish nav bar over English SNOMED terms.
  • Without an agreed structure, the first UI-string PR silently sets precedent
    (catalog format, key naming, fallback behavior, where negotiation lives). We
    should decide that deliberately, up front — the same rationale as Foundation: HTMX-first web UI (POC, structure, and rules of the road) #186.

Approach (proposed — finalized in the discussion doc)

One negotiated locale per request, computed once, threaded to every layer
the UI chrome, the error text, and the terminology displayLanguage all read
the same value.

  • UI strings → Project Fluent catalogs under
    locales/<locale>/main.ftl. Chosen over flat JSON/gettext for
    grammar-correct CLDR plurals/selectors, asymmetric translation, and
    first-class Rust support via fluent-templates,
    which integrates with Askama (the templating engine proposed in Foundation: HTMX-first web UI (POC, structure, and rules of the road) #186).
    Catalogs are embedded at build time (single binary, no runtime CDN —
    air-gap-friendly), consistent with Foundation: HTMX-first web UI (POC, structure, and rules of the road) #186's asset stance.
  • Locale negotiation middleware producing a RequestLocale, with precedence:
    explicit override (?lang= / cookie from the switcher) → saved user preference
    (feat/user-ui-settings) → Accept-Language (RFC 4647 Lookup) → en default.
  • Fallback chain, never a crash or blank: negotiated → base language → en.
    A half-translated locale is always usable.
  • Errors (OperationOutcome.text / issue.details.text) come from the same
    catalogs (error-* keys); machine-facing issue.code / codings stay stable.
  • Terminology reuses HTS as-is — the request locale is passed straight
    through as displayLanguage; we do not build a second UI-local translation
    table for clinical terms.

Full detail — including the front-to-back request-flow diagram, the six
localization layers, and the terminology integration — is in the discussion
document delivered on the branch: docs/multi-language.md.

File placement — rules of the road (ratified in the doc)

Where things go

  • locales/<locale>/main.ftl — all translatable UI text; en is the source
    of truth
    and defines the canonical key set.
  • Locale-negotiation middleware — one RequestLocale per request, in request
    extensions, forwarded to HTS as displayLanguage.
  • A thin template helper resolving keys against the request locale.

Where things must NOT go

  • No hardcoded human-readable strings in Rust or templates — if a user can
    read it, it comes from a catalog (logs/dev errors stay English).
  • No sentence-building by string concatenation — one keyed message with
    placeables and a plural selector.
  • No if lang == "de" branching in handler logic — differences live in the
    catalog, not Rust control flow.
  • No new browser-facing JSON API for translations — localized HTML is
    rendered server-side (consistent with Foundation: HTMX-first web UI (POC, structure, and rules of the road) #186).
  • No duplicate translation table for clinical terms — SNOMED/LOINC
    designations are authoritative; pass the locale through to HTS.
  • Do not localize identifiers, codes, URLs, FHIR element names, UCUM units,
    or FHIR wire date/time formats.

Existing terminology (SNOMED) localization — build on this

  • Multi-language import: the SNOMED RF2 importer
    (crates/hts/src/import/snomed_rf2.rs) imports all active descriptions in
    every language in the archive (incl. national-edition per-language files) as
    concept.designation entries tagged with the RF2 languageCode, and emits
    preferredForLanguage designations from the language reference sets.
  • Scoped import via LanguageFilter (HTS_IMPORT_LANGUAGES / --languages).
  • Runtime selection: $expand / $lookup / $validate-code honor
    displayLanguage and Accept-Language; matching is RFC 4647 §3.4 Lookup in
    crates/hts/src/language.rs, reconciling heterogeneous tags (SNOMED bare de
    vs LOINC de-DE vs browser de-DE).

Deliverables

  1. A branch — feat/i18n-foundation (off main, pushed) containing:
    • Working structure: seeded Fluent UI message catalogs
      locales/{en,es,de}/main.ftl (English source + Spanish + German), with the
      same key set across all three and CLDR plural selectors.
    • locales/README.md — catalog conventions and how to add a language.
  2. A discussion document — docs/multi-language.md — covering:
    • The chosen approach and why (Fluent + server-rendered, per-request
      negotiated locale).
    • The six localization layers and the front-to-back request flow.
    • File-placement rules ("where things go / must not go") as durable convention.
    • Fallback/negotiation policy, error-message localization, FHIR content
      pass-through, formatting, and security/escaping notes.
    • How UI i18n relates to the existing HTS SNOMED multi-language support.
    • The language roadmap and references.

Runtime wiring (negotiation middleware, the Askama/Fluent helper, Cargo.toml)
intentionally rides on the helios-web crate from #186 so the two don't
conflict — this issue establishes the shape and the rules; #186 provides
the crate they plug into.

Acceptance criteria

  • Branch feat/i18n-foundation exists with a working locale structure.
  • Seeded catalogs for English, Spanish, and German with a shared key set.
  • Discussion document on the branch covering approach, guidelines, and rules
    of the road, and referencing the existing HTS SNOMED multi-language support.
  • Approach reviewed/ratified so subsequent UI PRs follow it.

Out of scope

  • A fully translated UI, or translating patient/clinical (FHIR) content —
    we select among existing translations, we do not generate them.
  • The UI crate / templating runtime itself (that is Foundation: HTMX-first web UI (POC, structure, and rules of the road) #186); this issue delivers
    the i18n structure and conventions that plug into it.
  • Additional languages beyond en/es/de (French, Portuguese, … are pure
    translation once the structure holds).

References

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationenhancementNew feature or request

Type

No type

Fields

No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions