Check translation contracts offline, add Swedish to the installer and enable the website languages

- Offline checks for terminal and website catalogs reject translations that change commands, paths, identifiers or placeholders
- Terminal and documentation texts that broke those contracts are corrected in every language
- Swedish can be chosen in the stable and beta installers
- The website routes, language switcher and exported pages cover the eight languages, with the shared website texts in each of them
- Website documentation translations are generated only when requested
- From PR #475 by @jcastro
This commit is contained in:
MacRimi
2026-10-18 12:58:25 +02:00
parent f363ef4395
commit f8cabb4700
54 changed files with 2149 additions and 173 deletions
@@ -60,6 +60,124 @@ class WebDocsI18nTests(unittest.TestCase):
target = {"title": "Actualizaciones", "url": "https://example.com"}
self.assertEqual(MODULE.pending_leaves(source, target), [])
def test_rendering_metadata_is_copied_but_visible_labels_are_translated(self):
for key, value in (("tone", "amber"), ("variant", "source"),
("accent", "border-blue-300 bg-blue-50"),
("iconBg", "bg-blue-100 text-blue-700"),
("color", "green")):
with self.subTest(key=key):
self.assertTrue(MODULE.should_copy(value, ("steps", 0, key)))
self.assertFalse(MODULE.should_copy(value, ("label",)))
self.assertEqual(MODULE.merge_tree({key: value}, {key: "translated"}, {}), {key: value})
self.assertFalse(MODULE.should_copy("Translation", ("type",)))
self.assertFalse(MODULE.should_copy("English", ("lang",)))
for value in ("loader", "dedicated"):
self.assertTrue(MODULE.should_copy(value, ("cards", 0, "flow")))
self.assertFalse(MODULE.should_copy(value, ("label",)))
def test_literal_only_examples_and_api_routes_are_not_translation_gaps(self):
for source in ("<code>apt update</code>",
"<code>net0</code> / <code>net1</code>.",
"/api/task-log/&lt;upid&gt;", "/api/vms/<vmid>/config"):
with self.subTest(source=source):
self.assertTrue(MODULE.should_copy(source, ("cell",)))
self.assertFalse(MODULE.should_copy("Run <code>apt update</code> first.", ("body",)))
self.assertFalse(MODULE.should_copy("After running the command, check its output.", ("afterCode",)))
self.assertFalse(MODULE.should_copy("Before running the command, read this.", ("beforeCode",)))
self.assertTrue(MODULE.should_copy("curl http://localhost:8008/api/health", ("curlCode",)))
def test_inline_identifiers_and_encoded_delimiters_round_trip(self):
source = "Set RS_SKIP_PATHS, call _rs_apply_hot and keep localStorage at &lt;host&gt;."
protected, mapping = MODULE.protect_text(source)
for literal in ("RS_SKIP_PATHS", "_rs_apply_hot", "localStorage", "&lt;", "&gt;"):
self.assertIn(literal, mapping.values())
self.assertNotIn(literal, protected)
self.assertEqual(MODULE.restore_text(protected, mapping), source)
def test_custom_code_spans_keep_commands_literal(self):
for tag in ("codeNw", "codeXs"):
source = f"Run <{tag}>pct config 100</{tag}>."
with self.subTest(tag=tag):
protected, mapping = MODULE.protect_rich_tags(source)
self.assertEqual(protected, source)
self.assertEqual(mapping, {})
with self.assertRaisesRegex(ValueError, "literal code changed"):
MODULE.validate_contract(source, source.replace("config", "configuration"))
with self.assertRaisesRegex(ValueError, "numeric residue"):
MODULE.validate_contract(source, source.replace(f"</{tag}>", f"</{tag}>0"))
self.assertTrue(MODULE.should_copy(f"<{tag}>pct config 100</{tag}>", ("example",)))
def test_navigation_identifiers_are_distinct_from_visible_labels(self):
values = {
"categorySlug": ["system", "network"],
"menuCmd": ["menu"],
"branch": ["main", "develop", "feature/*"],
"kind": ["internal", "external"],
"key": ["nas", "linux", "windows"],
}
for key, items in values.items():
for value in items:
with self.subTest(key=key, value=value):
self.assertTrue(MODULE.should_copy(value, ("cards", 0, key)))
self.assertFalse(MODULE.should_copy(value, ("label",)))
self.assertFalse(MODULE.should_copy("Translation", ("kind",)))
self.assertFalse(MODULE.should_copy("Storage", ("branch",)))
def test_ui_breadcrumb_is_prose_while_paths_remain_literal(self):
source = "In the UI: <strong>Datacenter → Backup → Retention</strong>:"
self.assertFalse(MODULE.should_copy(source, ("retention", "uiPath")))
self.assertTrue(MODULE.should_copy("/etc/pve/storage.cfg", ("uiPath",)))
self.assertTrue(MODULE.should_copy("/etc/pve/storage.cfg", ("configPath",)))
def test_bare_sentinel_with_added_digit_is_rejected(self):
with mock.patch.object(MODULE, "BARE_SENTINELS", True):
protected, mapping = MODULE.protect_text("ProxMenux collects metrics.")
token = next(iter(mapping))
with self.assertRaisesRegex(ValueError, "changed protected token"):
MODULE.restore_text(protected.replace(token, token + "0"), mapping)
self.assertEqual(MODULE.restore_text(protected, mapping), "ProxMenux collects metrics.")
def test_numeric_residue_outside_intact_literal_is_rejected(self):
examples = [
('Run <code>pct config</code> first.', 'Spustite <code>pct config</code>0 najprv.'),
('<strong><code>Restart=on-failure</code></strong> restarts the service.',
'<strong>0<code>Restart=on-failure</code></strong> reštartuje službu.'),
]
for source, target in examples:
with self.subTest(source=source):
with self.assertRaisesRegex(ValueError, 'numeric residue'):
MODULE.validate_contract(source, target)
MODULE.validate_contract('The result is <code>echo</code>0.',
'Výsledok je <code>echo</code>0.')
def test_product_and_kernel_module_names_are_protected(self):
source = "Flask uses SQLite and journald; rclone works with Coral, gasket, apex and Alpine."
protected, mapping = MODULE.protect_text(source)
for name in ("Flask", "SQLite", "journald", "rclone", "Coral", "gasket", "apex", "Alpine"):
self.assertIn(name, mapping.values())
self.assertNotIn(name, protected)
self.assertEqual(MODULE.restore_text(protected, mapping), source)
def test_literal_only_changes_sync_without_retranslating_human_prose(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source = root / "messages/en/common.json"
target = root / "messages/de/common.json"
source.parent.mkdir(parents=True)
target.parent.mkdir(parents=True)
source.write_text(json.dumps({"title": "Updates", "code": "tool --version=2", "tone": "blue"}))
target.write_text(json.dumps({"title": "Aktualisierungen", "code": "tool --version=1", "tone": "blau"}))
provider = mock.Mock(side_effect=AssertionError("Existing prose must not be retranslated"))
self.assertEqual(self.run_generator(root, provider), 0)
self.assertEqual(json.loads(target.read_text()), {
"title": "Aktualisierungen", "code": "tool --version=2", "tone": "blue",
})
provider.assert_not_called()
before = target.stat().st_mtime_ns
self.assertEqual(self.run_generator(root, provider), 0)
self.assertEqual(target.stat().st_mtime_ns, before)
provider.assert_not_called()
def test_changed_source_text_is_pending_even_with_existing_translation(self):
source = {"title": "Updated installation guidance"}
target = {"title": "Vorherige Installationsanleitung"}