KB Hungarian translation + HU/EN toggle (i18n) — design

Add a site-wide HU/EN language toggle to the car-repair knowledgebase. Hungarian becomes the default; English stays the fallback/original. Everything data-driven that is currently English — the UI chrome, the cluster labels, and the generated consolidated articles — gets a Hungarian version, switchable from one control. Source manuals stay English (they fall back automatically).

Related: 2026-07-24-knowledgebase, 2026-07-24-kb-semantics-phase2-complete, 2026-07-25-kb-consolidated-gen-phase3-complete, telep-mainframe, SESSION-HANDOVER

Scope

In scope (bilingual, toggle flips between HU and EN):

  • UI chrome — Flask-rendered pages (/semantics, /drafts, generation/job pages) + Material’s static-site chrome (search, nav, footer).
  • Cluster labels — the label shown per cluster in /semantics.
  • Generated consolidated articles — the phase-3 claude -p how-to articles under docs/consolidated/<slug>/.

Out of scope:

  • Source manuals — stay English (bge-large-en corpus; not translated). They render under either language via English fallback.
  • Semantic search itself stays English (embeddings are English-only) — a HU query box is a later, separate concern.

Key decisions

DecisionChoice
ApproachA — native mkdocs-static-i18n + Material language selector for the static site; shared lang cookie for the Flask dynamic pages
Default languageHungarian (hu); English (en) is fallback + original content language
Single source of truth for languagea lang cookie (hu/en), readable by both Flask and the static site
UI chrome stringshand-authored HU/EN string table (small, finite) — not machine-translated
Cluster labelslabel_hu added to clusters.json via a one-shot claude -p opus batch pass; /semantics picks by lang
Article translationon-demand “Fordítás” action → claude -p opus translates the published EN article → HU draft → human review → publish as a .hu.md sibling
Trust modelreuse the existing draft → review → publish gate for all machine-translated content (never auto-publish a translation)
Fallbackany missing HU (manuals, untranslated articles/labels) → show English
Deploymentfold into the existing KB Flask app + build_site() atomic swap; no new always-on service (power-flaky box)
Modelclaude -p --model opus, serialized single worker, thread-capped; fail loud, never publish empty

Architecture

Two surfaces share one lang cookie:

                     ┌─────────────── lang cookie (hu|en) ───────────────┐
                     │                                                    │
   Static mkdocs-material site                         Flask dynamic pages
   (manual library + published articles)               (/semantics, /drafts, gen)
   - mkdocs-static-i18n plugin: /en/ + /hu/ trees      - lang read from cookie
   - Material language selector (dropdown)             - Jinja i18n via STRINGS[lang]
   - manuals: EN only → auto English fallback          - cluster labels: label_hu|label
   - consolidated: draft.md (EN) + draft.hu.md (HU)    - toggle control sets the cookie

Static site (mkdocs) — ~/knowledgebase/

  • Add mkdocs-static-i18n to the KB venv; configure two languages in mkdocs.yml (en default build, hu), and enable Material’s language selector (extra.alternate or the plugin’s nav_translations/language switch). Material ships Hungarian chrome translations, so search/nav localize for free.
  • Build produces /en/… and /hu/…; the plugin falls back to the default language for any page missing a .hu.md — so every English-only manual just renders under both.
  • Consolidated articles: docs/consolidated/<slug>/index.md (EN) gains an optional index.hu.md (HU) produced by the translation flow.

Dynamic pages (Flask) — ~/knowledgebase/app.py

  • A tiny lang helper: read the lang cookie (default hu); expose t(key) + current lang to Jinja templates.
  • i18n.py (new): STRINGS = {"hu": {...}, "en": {...}} — every hand-written chrome string keyed (existing Hungarian strings become the hu values; add en counterparts). t(key, lang) returns the string, falling back to hu then the key.
  • Toggle control in the shared header POSTs/links to a GET /lang/<hu|en> route that sets the cookie and redirects back (Referer/next). The Material selector on static pages links to /hu/…/en/…; a small snippet syncs the cookie so returning to a Flask page keeps the choice.
  • /semantics renders each cluster’s label as cluster.get("label_hu") if lang=="hu" else cluster["label"], falling back to label.

Cluster labels — ~/kb-vectors/

  • New translate_labels.py (pure prompt build + parse) + a kbclust translate-labels subcommand: batch the existing English labels to claude -p --model opus, get {index: label_hu}, write label_hu back into each cluster in clusters.json (snapshot clusters.pre-hu.json first). Idempotent; re-runnable. Fail-safe: on claude error/parse-fail, leave label_hu absent (UI falls back to English).

Article translation — ~/knowledgebase/

  • Reuse the phase-3 pipeline shape (genprompt.py/gen.py/kbgen.py). Add:
    • transprompt.py (pure): build a translate prompt from a published EN article — rules: translate prose to natural Hungarian, but reproduce all numbers/units/torque/part-numbers/wire-colors and [<manual_id>] source tags verbatim (never convert/reword); keep image refs; keep markdown structure.
    • a POST /translate/<slug> route → background job → claude -p --model opus → writes drafts/<slug>-hu/draft.hu.md + meta → runs speccheck (the existing number-token guard: every number in the HU draft must trace to the EN source) → draft state.
    • review UI: reuse /drafts/<slug> render with an “AI-fordítás — ellenőrizd” banner + speccheck warnings; publish moves draft.hu.md next to the EN article as index.hu.md, then build_site().
  • A “Fordítás” button appears on a published consolidated article that lacks a HU sibling.

Data flow — language resolution

request → lang = cookie["lang"] or "hu"
  static page:  served from /hu/ or /en/ tree (Material selector switches; cookie synced)
  flask page:   t(key, lang) for chrome; label_hu|label for clusters
  article body: /hu/consolidated/<slug>/ shows index.hu.md if present else EN (plugin fallback)

Failure / ops notes

  • English is always the safe fallback: missing plugin translation, missing label_hu, missing .hu.md → English renders. Nothing 500s on a missing translation.
  • claude -p translation shares the power-flaky box → single serialized worker, --model opus explicit, fail loud (no draft on empty/error), re-runnable by slug.
  • speccheck on translations is the accuracy guard: flags any HU-draft number absent from the EN source (catches a mangled torque value); surfaced in review, never auto-blocks.
  • Publish stays human-gated + atomic (build_site() temp-dir → os.rename swap); a broken translation never takes the live site down.
  • Path-traversal guard (slugs.safe_slug + realpath containment) applies to the -hu draft/publish paths.
  • No new service: label pass is a CLI; article translation reuses the existing generation worker; the static-i18n build is part of build_site().

Testing / verification

  1. i18n string table unit: t(key,"hu")/t(key,"en") return correct strings; unknown key falls back; every hu key has an en counterpart (test asserts parity).
  2. lang cookie/route: GET /lang/en sets cookie + redirects back; /semantics then renders English chrome; default (no cookie) = Hungarian.
  3. cluster labels: after kbclust translate-labels, clusters have label_hu; /semantics?lang=hu shows Hungarian labels, en shows English; a cluster missing label_hu falls back to English (no crash). clusters.pre-hu.json snapshot exists.
  4. transprompt/speccheck unit: prompt carries the verbatim-numbers + keep-source-tags rules; speccheck flags an injected bad number, passes a faithful translation.
  5. article translate e2e (real): translate the published engine-cooling article → HU draft cites the same [manual_id] tags, same torque/spec numbers verbatim, speccheck clean; review page renders with the AI-translation banner; publish creates index.hu.md; /hu/consolidated/engine-cooling/ renders in Hungarian and /en/… in English.
  6. static fallback: an English-only manual renders under both /hu/ and /en/ (plugin fallback); Material chrome shows Hungarian on /hu/.
  7. No regression: existing 26 KB tests pass; source manuals untouched; /semantics, /drafts, generation still work.

Risks

RiskMitigation
Mistranslated spec (wrong torque/part no.)verbatim-numbers prompt rule + speccheck per-number flag + human review gate + linked EN original
Toggle desync between static + Flask surfacessingle lang cookie is source of truth; Material selector syncs the cookie
mkdocs-static-i18n doubles site/ size / build timeacceptable on this box; build stays atomic; manuals aren’t duplicated in content, only routed
Heavy claude -p translation destabilises boxsingle serialized worker, opus explicit, one at a time, re-runnable
HU article mistaken for a manualAI-translation banner (draft) + provenance header + source links (published), same as phase-3
Model unavailable / silent downgrade--model opus explicit; fail loud, never publish empty