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
labelshown per cluster in/semantics. - Generated consolidated articles — the phase-3
claude -phow-to articles underdocs/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
| Decision | Choice |
|---|---|
| Approach | A — native mkdocs-static-i18n + Material language selector for the static site; shared lang cookie for the Flask dynamic pages |
| Default language | Hungarian (hu); English (en) is fallback + original content language |
| Single source of truth for language | a lang cookie (hu/en), readable by both Flask and the static site |
| UI chrome strings | hand-authored HU/EN string table (small, finite) — not machine-translated |
| Cluster labels | label_hu added to clusters.json via a one-shot claude -p opus batch pass; /semantics picks by lang |
| Article translation | on-demand “Fordítás” action → claude -p opus translates the published EN article → HU draft → human review → publish as a .hu.md sibling |
| Trust model | reuse the existing draft → review → publish gate for all machine-translated content (never auto-publish a translation) |
| Fallback | any missing HU (manuals, untranslated articles/labels) → show English |
| Deployment | fold into the existing KB Flask app + build_site() atomic swap; no new always-on service (power-flaky box) |
| Model | claude -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-i18nto the KB venv; configure two languages inmkdocs.yml(endefault build,hu), and enable Material’s language selector (extra.alternateor the plugin’snav_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 optionalindex.hu.md(HU) produced by the translation flow.
Dynamic pages (Flask) — ~/knowledgebase/app.py
- A tiny
langhelper: read thelangcookie (defaulthu); exposet(key)+ current lang to Jinja templates. i18n.py(new):STRINGS = {"hu": {...}, "en": {...}}— every hand-written chrome string keyed (existing Hungarian strings become thehuvalues; addencounterparts).t(key, lang)returns the string, falling back tohuthen 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. /semanticsrenders each cluster’s label ascluster.get("label_hu") if lang=="hu" else cluster["label"], falling back tolabel.
Cluster labels — ~/kb-vectors/
- New
translate_labels.py(pure prompt build + parse) + akbclust translate-labelssubcommand: batch the existing Englishlabels toclaude -p --model opus, get{index: label_hu}, writelabel_huback into each cluster inclusters.json(snapshotclusters.pre-hu.jsonfirst). Idempotent; re-runnable. Fail-safe: on claude error/parse-fail, leavelabel_huabsent (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→ writesdrafts/<slug>-hu/draft.hu.md+ meta → runsspeccheck(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 movesdraft.hu.mdnext to the EN article asindex.hu.md, thenbuild_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 -ptranslation shares the power-flaky box → single serialized worker,--model opusexplicit, fail loud (no draft on empty/error), re-runnable by slug.speccheckon 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.renameswap); a broken translation never takes the live site down. - Path-traversal guard (
slugs.safe_slug+ realpath containment) applies to the-hudraft/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
- i18n string table unit:
t(key,"hu")/t(key,"en")return correct strings; unknown key falls back; everyhukey has anencounterpart (test asserts parity). - lang cookie/route:
GET /lang/ensets cookie + redirects back;/semanticsthen renders English chrome; default (no cookie) = Hungarian. - cluster labels: after
kbclust translate-labels, clusters havelabel_hu;/semantics?lang=hushows Hungarian labels,enshows English; a cluster missinglabel_hufalls back to English (no crash).clusters.pre-hu.jsonsnapshot exists. - transprompt/speccheck unit: prompt carries the verbatim-numbers + keep-source-tags rules; speccheck flags an injected bad number, passes a faithful translation.
- article translate e2e (real): translate the published
engine-coolingarticle → HU draft cites the same[manual_id]tags, same torque/spec numbers verbatim, speccheck clean; review page renders with the AI-translation banner; publish createsindex.hu.md;/hu/consolidated/engine-cooling/renders in Hungarian and/en/…in English. - static fallback: an English-only manual renders under both
/hu/and/en/(plugin fallback); Material chrome shows Hungarian on/hu/. - No regression: existing 26 KB tests pass; source manuals untouched;
/semantics,/drafts, generation still work.
Risks
| Risk | Mitigation |
|---|---|
| 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 surfaces | single lang cookie is source of truth; Material selector syncs the cookie |
mkdocs-static-i18n doubles site/ size / build time | acceptable on this box; build stays atomic; manuals aren’t duplicated in content, only routed |
Heavy claude -p translation destabilises box | single serialized worker, opus explicit, one at a time, re-runnable |
| HU article mistaken for a manual | AI-translation banner (draft) + provenance header + source links (published), same as phase-3 |
| Model unavailable / silent downgrade | --model opus explicit; fail loud, never publish empty |