Files
Rohit Ghumare d86fedaa51 fix: dark-mode mermaid fills, Apple Silicon Docker build, fnm unzip, ROADMAP rows, site chrome translations (#477)
* fix: dark-mode mermaid fills, Apple Silicon Docker build, fnm unzip, ROADMAP rows, site chrome translations

- site/lesson.html: mermaidPreprocess handles 3-digit hex fills so fill:#dfd stays legible in dark mode (#433)
- 07-docker-for-ai: pin FROM to linux/amd64 and document why the cu124 layer fails on Apple Silicon (#476)
- 01-dev-environment: add unzip to the apt line, fnm's installer needs it (#473)
- ROADMAP.md: add Phase 11 lessons 16 and 17, 521 rows to 523
- site: translate interface strings for the eight CI languages via ui-i18n.js + ui-strings.js, picker dispatches aifs:lang, drift guarded by test_ui_i18n.js in CI (#469, #404)

* docs: describe fnm's unzip check and scope the CUDA image to x86_64 hosts

- 01-dev-environment: the installer checks for unzip up front and exits with its own message; macOS goes through Homebrew
- 07-docker-for-ai: the linux/amd64 pin means the image is for x86_64 Linux hosts with an NVIDIA GPU

* feat(i18n): generate site chrome translations in the translate pipeline

The hand dictionary becomes site/ui-strings.json: an English key list plus
per-language overrides. scripts/translate_ui_strings.py fills every other key
through the lesson translator's provider layer, reuses what is already
published, protects file names, paths, and commands behind the same
placeholders, and writes i18n/<lang>/ui.json. A new ui-strings job in
translate.yml runs it per CI language and publishes to the translations
branch with the same race-safe worktree loop, so a new label or a new
language needs no translation written by hand.

site/ui-i18n.js now fetches i18n/<lang>/ui.json from the translations branch
at runtime, caches it per language, and falls back to English until a language
is published. Tests cover source consistency, precedence, placeholder
protection, the page drift guard, and the fetch path.

* fix(i18n): retranslate keys whose override was removed and keep failed dictionary loads retryable

- ui.json now carries {strings, pinned}; a key that was pinned in the last publication but has no override now is translated again instead of reusing the old pin
- the runtime no longer caches a failed fetch, so a language published later is picked up on the next switch without a reload

* fix(i18n): refresh flat ui.json values once when pin provenance is missing

A flat published file predates the {strings, pinned} shape and cannot say
which values came from overrides, so every value in it counts as pinned and
is retranslated on the first run instead of being reused blindly.
2026-09-24 09:22:11 +05:30

158 lines
6.9 KiB
Python

#!/usr/bin/env python3
"""Regression checks for the interface-string translator and its source file."""
import json
import sys
import tempfile
import unittest
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent))
import translate_ui_strings as ui # noqa: E402
FIXED_TOKENS = ("SKILL.md", "README.md", "phases/", "start-learning", "@rohitg00", "(⌘K)")
class SourceFileTest(unittest.TestCase):
def test_source_file_is_consistent(self):
keys, overrides = ui.load_source()
self.assertTrue(keys)
self.assertEqual(ui.check_source(keys, overrides), [])
def test_check_source_reports_problems(self):
problems = ui.check_source(["A", "A", " B"], {"zh": {"C": "x", "A": " "}})
self.assertEqual(len(problems), 4)
class BuildLanguageTest(unittest.TestCase):
def test_override_then_existing_then_translate(self):
calls = []
def translate(text):
calls.append(text)
return text.upper()
strings, count = ui.build_language(
["Contents", "Catalog", "About"], {"Contents": "目录"}, {"Catalog": "cached"}, translate
)
self.assertEqual(strings, {"Contents": "目录", "Catalog": "cached", "About": "ABOUT"})
self.assertEqual(count, 1)
self.assertEqual(calls, ["About"])
def test_removed_override_is_retranslated_instead_of_reused(self):
calls = []
def translate(text):
calls.append(text)
return text.upper()
strings, count = ui.build_language(
["Build", "Run"], {}, {"Build": "old pin", "Run": "machine"}, translate, pinned_before={"Build"}
)
self.assertEqual(strings, {"Build": "BUILD", "Run": "machine"})
self.assertEqual(count, 1)
self.assertEqual(calls, ["Build"])
def test_load_published_accepts_flat_and_wrapped_files(self):
with tempfile.TemporaryDirectory() as tmp:
path = Path(tmp) / "ui.json"
self.assertEqual(ui.load_published(path), ({}, set()))
path.write_text(json.dumps({"A": "a"}), encoding="utf-8")
self.assertEqual(ui.load_published(path), ({"A": "a"}, {"A"}))
path.write_text(json.dumps({"strings": {"A": "a"}, "pinned": ["A"]}), encoding="utf-8")
self.assertEqual(ui.load_published(path), ({"A": "a"}, {"A"}))
def test_flat_file_values_are_refreshed_once_on_migration(self):
with tempfile.TemporaryDirectory() as tmp:
source = Path(tmp) / "ui-strings.json"
source.write_text(json.dumps({"keys": ["Build"], "overrides": {}}), encoding="utf-8")
out = Path(tmp) / "es" / "ui.json"
out.parent.mkdir()
out.write_text(json.dumps({"Build": "old pin"}), encoding="utf-8")
ui.main(["--lang", "es", "--provider", "echo", "--out", tmp, "--source", str(source)])
self.assertEqual(json.loads(out.read_text(encoding="utf-8")), {"strings": {"Build": "Build"}, "pinned": []})
def test_dropped_keys_disappear_from_output(self):
strings, _ = ui.build_language(["A"], {}, {"A": "x", "Gone": "y"}, lambda s: s)
self.assertEqual(list(strings), ["A"])
def test_dry_run_counts_without_translating(self):
strings, count = ui.build_language(["A", "B"], {"A": "a"}, {}, None)
self.assertEqual(strings, {"A": "a"})
self.assertEqual(count, 1)
def test_fixed_tokens_never_reach_the_model(self):
samples = [
"Open SKILL.md",
"Use start-learning to begin the course.",
"Follow @rohitg00",
"Search (⌘K)",
"Requires a local clone. Run copied commands from the repository root, the directory containing README.md and phases/.",
]
seen = []
def translate(text):
seen.append(text)
return text.upper()
strings, _ = ui.build_language(samples, {}, {}, translate)
for text in seen:
for token in FIXED_TOKENS:
self.assertNotIn(token, text)
for sample in samples:
for token in FIXED_TOKENS:
if token in sample:
self.assertIn(token, strings[sample])
self.assertEqual(strings["Open SKILL.md"], "OPEN SKILL.md")
self.assertEqual(strings["Follow @rohitg00"], "FOLLOW @rohitg00")
def test_placeholder_loss_leaves_the_key_english(self):
strings, count = ui.build_language(["Open SKILL.md"], {}, {}, lambda s: "opened")
self.assertEqual(strings, {})
self.assertEqual(count, 0)
class EndToEndTest(unittest.TestCase):
def test_echo_provider_writes_every_language(self):
keys, overrides = ui.load_source()
with tempfile.TemporaryDirectory() as tmp:
ui.main(["--lang", "zh", "--lang", "tr", "--provider", "echo", "--out", tmp])
for lang in ("zh", "tr"):
data = json.loads((Path(tmp) / lang / "ui.json").read_text(encoding="utf-8"))
self.assertEqual(list(data["strings"]), keys)
self.assertEqual(data["pinned"], [key for key in keys if key in overrides.get(lang, {})])
for key, value in overrides.get(lang, {}).items():
self.assertEqual(data["strings"][key], value)
def test_second_run_reuses_published_translations_until_a_pin_is_removed(self):
with tempfile.TemporaryDirectory() as tmp:
source = Path(tmp) / "ui-strings.json"
source.write_text(
json.dumps({"keys": ["Contents", "Catalog"], "overrides": {"es": {"Contents": "Contenido"}}}),
encoding="utf-8",
)
args = ["--lang", "es", "--provider", "echo", "--out", tmp, "--source", str(source)]
ui.main(args)
out = Path(tmp) / "es" / "ui.json"
data = json.loads(out.read_text(encoding="utf-8"))
self.assertEqual(data, {"strings": {"Contents": "Contenido", "Catalog": "Catalog"}, "pinned": ["Contents"]})
data["strings"]["Catalog"] = "kept"
out.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
ui.main(args)
self.assertEqual(json.loads(out.read_text(encoding="utf-8"))["strings"], {"Contents": "Contenido", "Catalog": "kept"})
source.write_text(json.dumps({"keys": ["Contents", "Catalog"], "overrides": {}}), encoding="utf-8")
ui.main(args)
self.assertEqual(json.loads(out.read_text(encoding="utf-8")), {"strings": {"Contents": "Contents", "Catalog": "kept"}, "pinned": []})
ui.main(args + ["--force"])
self.assertEqual(json.loads(out.read_text(encoding="utf-8"))["strings"]["Catalog"], "Catalog")
def test_unknown_language_fails_loudly(self):
with tempfile.TemporaryDirectory() as tmp:
with self.assertRaises(SystemExit):
ui.main(["--lang", "xx", "--provider", "echo", "--out", tmp])
if __name__ == "__main__":
unittest.main()