KB Hungarian i18n + HU/EN toggle — Implementation Plan

For agentic workers: execute task-by-task via subagent-driven development. Each task ends with a runnable unittest check. NO git commits (house rule) and NO code comments (house rule). Steps use - [ ].

Goal: A site-wide HU/EN toggle; Hungarian default; UI chrome + cluster labels + generated articles get Hungarian, with English fallback. Source manuals stay English.

Architecture: Shared lang cookie is the single source of truth. Flask dynamic pages localize via a string table + label_hu; the static mkdocs-material library toggles via mkdocs-static-i18n + Material’s language selector; article bodies get .hu.md siblings produced through the existing draft→review→publish gate.

Design spec: 2026-07-28-kb-hungarian-i18n-design. Related: 2026-07-24-knowledgebase, telep-mainframe.

Global Constraints

  • All work on telep-mainframe as levander; /usr/bin/ssh only. App restart: sudo systemctl restart knowledgebase.
  • venv: /home/levander/knowledgebase/venv/. Tests: cd /home/levander/knowledgebase && venv/bin/python -m unittest <module> (pytest not installed). kb-vectors tests: cd /home/levander/kb-vectors && venv/bin/python -m unittest (or system python as that pkg uses).
  • claude -p --model opus only, serialized, fail-loud, never publish empty. Binary: /home/levander/.local/bin/claude.
  • No git commit. No code comments. Back up any file before first edit (.bak-i18n).
  • English is always the safe fallback — a missing HU value must never 500.

Files:

  • Create: /home/levander/knowledgebase/i18n.py
  • Modify: /home/levander/knowledgebase/app.py (SEMANTICS_HTML, semantics(), add /lang/<code> route + current_lang() helper)
  • Test: /home/levander/knowledgebase/test_i18n.py (new) + update test_semantics.py

Interfaces produced:

  • i18n.STRINGS: dict[str, dict[str,str]] with keys "hu" and "en"; every key present in both.
  • i18n.strings(lang) -> dict — returns STRINGS.get(lang, STRINGS["hu"]).
  • app.current_lang() -> strrequest.cookies.get("lang") if in ("hu","en") else "hu".

Build:

  • i18n.py: STRINGS = {"hu": {...}, "en": {...}}. Seed keys from the existing hardcoded Hungarian in app.py (their current Hungarian becomes the hu value; add English en). At minimum for this task: semantics_title (“Szemantika — kapcsolódó szakaszok” / “Semantics — related sections”), sources_badge (“forrás”/“sources”), generate_btn (“Generál összefoglalót”/“Generate summary”), no_clusters (“Nincs klaszter. Futtasd:”/“No clusters. Run:”), back_kb (”← Vissza a tudásbázishoz”/”← Back to the knowledgebase”), summary_fmt (“%d klaszter, ebből %d több kézikönyvet érint” / “%d clusters, %d span multiple manuals”), plus lang_toggle_other_label/href.
  • app.py: add import i18n; current_lang(); GET /lang/<code> → validate code in ("hu","en"), resp = redirect(request.referrer or "/"), resp.set_cookie("lang", code, max_age=31536000, samesite="Lax"), return resp. Edit SEMANTICS_HTML: replace hardcoded Hungarian with {{S.key}}; render label as {{ (c.label_hu if lang=='hu' else c.label) or c.label }}; add a header toggle <a href="/lang/en">EN</a> | <a href="/lang/hu">HU</a>. In semantics(): lang = current_lang(); S = i18n.strings(lang); summary = S["summary_fmt"] % (len(clusters), cross); pass S=S, lang=lang to render_template_string.

Steps:

  • Back up app.pyapp.py.bak-i18n.
  • Write test_i18n.py: assert set(STRINGS["hu"]) == set(STRINGS["en"]) (parity); strings("en")["generate_btn"] is English; strings("zz") falls back to hu.
  • Run: venv/bin/python -m unittest test_i18n → FAIL (no i18n.py).
  • Create i18n.py; rerun → PASS.
  • Edit app.py (i18n import, current_lang, /lang/<code>, SEMANTICS_HTML, semantics()).
  • Update test_semantics.py: default request → Hungarian title present; request with Cookie: lang=en (via test_client().get("/semantics", headers={"Cookie":"lang=en"})) → English title; cluster label falls back to label when label_hu absent (no 500).
  • Run: venv/bin/python -m unittest test_semantics test_i18n → PASS.
  • sudo systemctl restart knowledgebase; curl /semantics and /semantics with -b lang=en; confirm HU vs EN chrome. /lang/en sets cookie (check -D-).

Deliverable: /semantics toggles HU/EN chrome; labels still English (until Task 2); default HU.


Task 2: Cluster label Hungarian translation (kbclust translate-labels)

Files:

  • Create: /home/levander/kb-vectors/translate_labels.py
  • Modify: /home/levander/kb-vectors/kbclust.py (add translate-labels subcommand)
  • Test: /home/levander/kb-vectors/test_translate_labels.py

Interfaces produced: each cluster in clusters.json gains top-level label_hu (string). Snapshot clusters.pre-hu.json written before overwrite.

Build (mirror judge.py):

  • translate_labels.py: build_prompt(labels: list[str]) -> str (instruct: translate each English automotive topic label to natural Hungarian; return ONLY a JSON array of {index, label_hu}; keep it a concise noun phrase); parse_reply(text) -> list[dict] (extract JSON array, tolerate code fences, raise on failure); _run_claude(prompt) -> str (copy judge.py’s idiom: subprocess.run(["/home/levander/.local/bin/claude","-p","--model","opus"], input=prompt, capture_output=True, text=True, timeout=300), raise on rc!=0/empty); main(batch_size=20): load clusters, snapshot to clusters.pre-hu.json, batch labels, call claude, map label_hu back by index, cluster.write_clusters(clusters). Fail-safe: a failed/unparseable batch leaves those clusters’ label_hu absent (UI falls back to English) — report count.
  • kbclust.py: add translate-labels subparser (--batch, default 20) → import translate_labels; translate_labels.main(batch_size=args.batch).

Steps:

  • Write test_translate_labels.py: parse_reply extracts a JSON array with/without fences and raises on junk; a stubbed batch maps label_hu onto the right cluster by index; a failed batch leaves label_hu absent (no crash).
  • Run unittest → FAIL; implement; rerun → PASS.
  • Live run: python kbclust.py translate-labels; confirm clusters.pre-hu.json snapshot exists and clusters now have label_hu (spot-check 3 for sane Hungarian). Report raw→translated count.
  • Verify /semantics?lang=hu now shows Hungarian labels and lang=en English (Task 1 already wired the fallback).

Deliverable: cluster labels bilingual end-to-end on /semantics.


Task 3: Static library toggle (mkdocs-static-i18n + Material selector)

Files:

  • Modify: /home/levander/knowledgebase/mkdocs.yml
  • Modify (venv): pip install mkdocs-static-i18n
  • Verify: build.py build_site() still succeeds; / renders.

Build:

  • venv/bin/pip install mkdocs-static-i18n.
  • mkdocs.yml: back up first. Add to plugins: the i18n plugin with docs_structure: suffix, languages: [{locale: en, default: true, name: English}, {locale: hu, name: Magyar, build: true}], fallback_to_default: true. Add Material language selector via extra.alternate (link / for hu-default) OR rely on the plugin’s built-in selector — pick whichever the installed version supports (check pip show mkdocs-static-i18n version and its README behavior). Ensure search still listed.
  • Confirm gen_index.py-generated index.md still builds under both locales (English structure is fine; it’s the fallback default).

Steps:

  • pip install; pip show mkdocs-static-i18n (record version).
  • Back up mkdocs.yml.bak-i18n; edit.
  • cd /home/levander/knowledgebase && venv/bin/python -c "import build; ok,e=build.build_site(); print(ok,e[:400])" → expect True.
  • Confirm site/ now has the language trees; curl / and an English-only manual page → renders (English fallback) under default; Material language selector present in header.
  • sudo systemctl restart knowledgebase not needed (static), but confirm site() route serves the new tree.

Deliverable: the manual library + Material chrome carry a HU/EN selector; English-only manuals fall back cleanly.

Risk gate: if build_site() fails or the selector misbehaves, restore mkdocs.yml.bak-i18n, rebuild, report BLOCKED — do not leave the live site/ broken.


Task 4: Article Hungarian translation (draft→review→publish as .hu.md)

Files:

  • Create: /home/levander/knowledgebase/transprompt.py
  • Modify: /home/levander/knowledgebase/gen.py (translate_article, submit/worker reuse), kbgen.py (add translate <slug> subcommand), app.py (optional POST /translate/<slug> + a trigger in /drafts or /semantics)
  • Test: /home/levander/knowledgebase/test_transprompt.py, update test_gen_routes.py if a route is added.

Build (mirror gen.generate_draft/publish):

  • transprompt.py: build_prompt(article_md) -> str — rules: translate all prose to natural Hungarian; reproduce EVERY number/unit/torque/part-number/wire-color and every [<manual_id>] source tag and every ![](img) ref verbatim; keep markdown structure and the # <title>; output only the translated markdown.
  • gen.py: translate_article(slug) -> hu_slug — read the published EN article md (docs/consolidated/<slug>/001-<slug>.md), strip the ASCII banner, claude -p --model opus (same subprocess idiom, timeout 900, fail loud), write drafts/<slug>-hu/draft.hu.md + copy the EN article’s images + meta.json ({slug, lang:"hu", source_slug:slug, state:"draft", speccheck:[...]}); run speccheck.check(hu_md, en_md) (numbers must survive). Reuse the worker/queue via a submit_translate(slug).
  • Publish path: extend gen.publish (or add publish_translation) so a -hu draft writes docs/consolidated/<slug>/001-<slug>.hu.md (sibling of the EN 001-<slug>.md, so mkdocs-static-i18n serves it under /hu/), copy images if missing, build_site(), delete draft. Prepend the HU banner (“AI-forditas. Ellenorizd a forras-kezikonyvek alapjan.”).
  • kbgen.py: translate <slug>, publish <slug>-hu already covered by existing publish.
  • app.py (optional MVP+): POST /translate/<slug> enqueues gen.submit_translate; a “Fordítás” link where consolidated articles are listed (or drive from CLI for v1 — YAGNI the static-page button).

Steps:

  • Write test_transprompt.py: prompt contains the verbatim-numbers + keep-[manual_id]-tags + keep-image-refs rules and the source markdown.
  • Run unittest → FAIL; implement transprompt.py; rerun → PASS.
  • Implement gen.translate_article + publish-translation; add kbgen translate.
  • Live e2e: python kbgen.py translate engine-cooling → draft drafts/engine-cooling-hu/draft.hu.md; read it against the EN: same [manual_id] tags, same torque/spec numbers verbatim, natural Hungarian prose; speccheck clean. Report an honest faithfulness read.
  • Publish it; confirm docs/consolidated/engine-cooling/001-engine-cooling.hu.md exists, build_site() ok, and /hu/consolidated/engine-cooling/… renders Hungarian while /en/… stays English.
  • venv/bin/python -m unittest (full suite) → all pass; source manuals untouched.

Deliverable: consolidated articles translatable to Hungarian through the review gate; served under /hu/.


Self-review notes

  • Spec coverage: Task1=UI chrome+toggle+cookie; Task2=labels; Task3=static library+Material chrome; Task4=article bodies. Manuals = English fallback (Task 3). All spec scope covered.
  • Type consistency: label_hu (Task2) consumed by Task1’s /semantics; current_lang()/i18n.strings names stable across tasks.
  • Ordering safest-first: 1 (pure Flask, reversible) → 2 (claude label pass, snapshot) → 3 (static build, risk-gated) → 4 (claude article pass, review-gated).