Files
ProxMenux/.github/scripts/tests/test_build_web_docs_i18n.py
T
MacRimi fcfe8da765 refine post-install and hardware GPU docs, Monitor UX and CLI styling
- rewrite the 15 post-install pages and the 3 hardware GPU pages so they reflect the current scripts (reversibility, tracked-tool counts, kernel parameters, per-tool commands, Alpine LXC propagation flow)
- migrate the legacy step-badge helper on post-install/optional and create-vm/synology to the canonical pill component, with the stepLabel key added in each locale
- fix rich-text i18n calls missing helpers across network, automated, optional, security, customization and the post-install landing pages, and escape the `<iface>` placeholder in automated so intl no longer parses it as a tag
- remove the mouse-follow blue overlay from the docs landing layout
- reposition the App-tab Edit button and stack the Search and Register controls vertically on mobile
- move the Bulk update Configure/Edit control into the section header so it behaves the same on desktop and mobile
- show a spinner during the final autoremove/autoclean pass of update-pve-safe so the cleanup step reads as active instead of silent
- restyle the shell spinner and msg_info in a distinctive purple and drop the unused msg_lang duplicate
- add a web-docs i18n build script and its CI workflow, plus tests for the pushover notification channel
2026-08-26 17:23:09 +02:00

222 lines
8.5 KiB
Python

import importlib.util
import json
import sys
import tempfile
import unittest
from pathlib import Path
from unittest import mock
SCRIPT = Path(__file__).parents[1] / "build_web_docs_i18n.py"
SPEC = importlib.util.spec_from_file_location("build_web_docs_i18n", SCRIPT)
MODULE = importlib.util.module_from_spec(SPEC)
assert SPEC and SPEC.loader
sys.modules[SPEC.name] = MODULE
SPEC.loader.exec_module(MODULE)
class WebDocsI18nTests(unittest.TestCase):
def run_generator(self, root, provider):
source_root = root / "messages" / "en"
messages_root = root / "messages"
state_path = messages_root / ".docs-i18n-source-state.json"
argv = [
str(SCRIPT),
"--source-dir",
str(source_root),
"--messages-dir",
str(messages_root),
"--languages",
"de",
"--source-state",
str(state_path),
"--workers",
"1",
]
with mock.patch.object(sys, "argv", argv), mock.patch.object(
MODULE, "provider_function", return_value=provider
):
return MODULE.main()
def test_protected_contract_round_trip(self):
source = (
"Run <code>systemctl restart pveproxy</code> in Proxmox VE, "
"then open <link>Settings</link> at {host}."
)
protected, mapping = MODULE.protect_text(source)
self.assertIn("<code>systemctl restart pveproxy</code>", protected)
self.assertNotIn("Proxmox VE", protected)
self.assertIn("<code>", protected)
self.assertIn("</code>", protected)
self.assertIn("<link>", protected)
self.assertIn("</link>", protected)
self.assertNotIn("<code>", mapping.values())
self.assertNotIn("</code>", mapping.values())
self.assertNotIn("systemctl", mapping.values())
self.assertEqual(MODULE.restore_text(protected, mapping), source)
def test_human_translation_is_not_pending(self):
source = {"title": "Updates", "url": "https://example.com"}
target = {"title": "Actualizaciones", "url": "https://example.com"}
self.assertEqual(MODULE.pending_leaves(source, target, refresh=False), [])
def test_changed_source_text_is_pending_even_with_existing_translation(self):
source = {"title": "Updated installation guidance"}
target = {"title": "Vorherige Installationsanleitung"}
token = MODULE.leaf_token(("title",))
leaves = MODULE.pending_leaves(
source,
target,
refresh=False,
forced_tokens={token},
)
self.assertEqual([leaf.path for leaf in leaves], [("title",)])
def test_source_fingerprints_change_per_leaf(self):
before = MODULE.source_fingerprints({"title": "One", "body": "Same"})
after = MODULE.source_fingerprints({"title": "Two", "body": "Same"})
self.assertNotEqual(
before[MODULE.leaf_token(("title",))],
after[MODULE.leaf_token(("title",))],
)
self.assertEqual(
before[MODULE.leaf_token(("body",))],
after[MODULE.leaf_token(("body",))],
)
def test_schema_mismatch_detects_removed_keys(self):
self.assertFalse(
MODULE.schema_matches(
{"title": "Title"},
{"title": "Título", "removed": "Old"},
)
)
def test_schema_mismatch_detects_changed_structural_values(self):
self.assertFalse(
MODULE.schema_matches(
{"enabled": True, "retries": 2},
{"enabled": False, "retries": 2},
)
)
def test_schema_allows_localized_string_values(self):
self.assertTrue(
MODULE.schema_matches(
{"title": "Updates"},
{"title": "Aktualisierungen"},
)
)
def test_translatable_rich_tags_use_opaque_names(self):
source = (
"Keep <strong>this</strong>, <em>that</em> and "
"<code>systemctl restart pveproxy</code>."
)
protected, mapping = MODULE.protect_rich_tags(source)
self.assertIn("<pmxrich0000>this</pmxrich0000>", protected)
self.assertIn("<pmxrich0001>that</pmxrich0001>", protected)
self.assertIn("<code>systemctl restart pveproxy</code>", protected)
self.assertEqual(MODULE.restore_rich_tags(protected, mapping), source)
def test_source_schema_drives_output(self):
source = {"title": "Title", "items": ["One", "Two"]}
target = {
"title": "Título",
"items": ["Uno", "Dos", "Obsoleto"],
"removed": "Old",
}
translated = {
("items", 0): "Uno",
("items", 1): "Dos",
}
self.assertEqual(
MODULE.merge_tree(source, target, translated),
{"title": "Título", "items": ["Uno", "Dos"]},
)
def test_translation_memory_ignores_conflicts(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source_root = root / "messages" / "en"
target_root = root / "messages" / "es"
source_root.mkdir(parents=True)
target_root.mkdir(parents=True)
(source_root / "one.json").write_text(
json.dumps({"label": "Settings"}), encoding="utf-8"
)
(target_root / "one.json").write_text(
json.dumps({"label": "Ajustes"}), encoding="utf-8"
)
(source_root / "two.json").write_text(
json.dumps({"label": "Settings"}), encoding="utf-8"
)
(target_root / "two.json").write_text(
json.dumps({"label": "Configuración"}), encoding="utf-8"
)
memory = MODULE.collect_memory(source_root, root / "messages", "es")
self.assertNotIn("Settings", memory)
def test_source_state_retranslates_an_existing_changed_leaf(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source_path = root / "messages" / "en" / "page.json"
target_path = root / "messages" / "de" / "page.json"
source_path.parent.mkdir(parents=True)
target_path.parent.mkdir(parents=True)
source_path.write_text(json.dumps({"title": "Original"}), encoding="utf-8")
target_path.write_text(json.dumps({"title": "Ursprünglich"}), encoding="utf-8")
self.assertEqual(
self.run_generator(root, lambda *_: self.fail("provider called during baseline")),
0,
)
self.assertEqual(read_json_file(target_path), {"title": "Ursprünglich"})
source_path.write_text(json.dumps({"title": "Changed"}), encoding="utf-8")
calls = []
def provider(text, language):
calls.append((text, language))
return "Geändert"
self.assertEqual(self.run_generator(root, provider), 0)
self.assertEqual(calls, [("Changed", "de")])
self.assertEqual(read_json_file(target_path), {"title": "Geändert"})
def test_new_leaf_preserves_translation_supplied_with_source_change(self):
with tempfile.TemporaryDirectory() as temporary:
root = Path(temporary)
source_path = root / "messages" / "en" / "page.json"
target_path = root / "messages" / "de" / "page.json"
source_path.parent.mkdir(parents=True)
target_path.parent.mkdir(parents=True)
source_path.write_text(json.dumps({"title": "Original"}), encoding="utf-8")
target_path.write_text(json.dumps({"title": "Ursprünglich"}), encoding="utf-8")
self.assertEqual(self.run_generator(root, lambda *_: "unused"), 0)
source_path.write_text(
json.dumps({"title": "Original", "new": "New text"}),
encoding="utf-8",
)
target_path.write_text(
json.dumps({"title": "Ursprünglich", "new": "Neuer Text"}),
encoding="utf-8",
)
self.assertEqual(
self.run_generator(root, lambda *_: self.fail("manual translation overwritten")),
0,
)
self.assertEqual(
read_json_file(target_path),
{"title": "Ursprünglich", "new": "Neuer Text"},
)
def read_json_file(path):
return json.loads(path.read_text(encoding="utf-8"))
if __name__ == "__main__":
unittest.main()