"""Locale files: same keys everywhere, and a real fallback. ``Translator`` returns the key itself when a string is missing, which is silent in production — a label would simply read ``axes.nope``. The studio suite guards this by comparing key sets; the same guard applies here. """ from __future__ import annotations import json from pathlib import Path import pytest from orisys_autocal.preferences import load_preferences LOCALES = Path(__file__).resolve().parents[1] / "config" / "locales" LANGUAGES = ("zh_CN", "en_US") def _load(language: str) -> dict: return json.loads((LOCALES / f"{language}.json").read_text(encoding="utf-8")) def _flatten(node: dict, prefix: str = "") -> set[str]: keys: set[str] = set() for key, value in node.items(): path = f"{prefix}{key}" if isinstance(value, dict): keys |= _flatten(value, f"{path}.") else: keys.add(path) return keys def test_every_locale_file_exists(): for language in LANGUAGES: assert (LOCALES / f"{language}.json").is_file(), language def test_locales_have_identical_key_sets(): reference = _flatten(_load("zh_CN")) for language in LANGUAGES[1:]: other = _flatten(_load(language)) assert other == reference, { f"{language}_only": sorted(other - reference), "zh_CN_only": sorted(reference - other), } def test_no_translation_is_empty_where_a_label_is_expected(): """A blank string renders as an empty widget, which looks like a bug.""" for language in LANGUAGES: for key, value in _walk(_load(language)): assert str(value).strip(), f"{language}: {key} is blank" def test_the_product_name_is_localised(): """The window title is Chinese in zh_CN and English in en_US.""" assert _load("zh_CN")["app"]["title"] != _load("en_US")["app"]["title"] @pytest.mark.parametrize("language", LANGUAGES) def test_every_locale_loads_through_the_translator(language): from orisys_app_shared.i18n import Translator translator = Translator(LOCALES, locale=language) for key in sorted(_flatten(_load(language))): assert translator.t(key) != key, f"{language}: {key} did not resolve" def test_the_default_preferences_file_matches_the_schema_shape(): """A shipped default that names a key the app does not have is dead weight.""" shipped = json.loads( (LOCALES.parent / "preferences.default.json").read_text(encoding="utf-8") ) schema = load_preferences() for key in shipped: assert key in schema, f"preferences.default.json has an unknown key: {key}" def test_the_shipped_defaults_carry_the_controller_task_number(): """Re-initialisation names a task; if this key is missing every re-init is wrong.""" from orisys_autocal.motion import DEFAULT_BAS_TASK shipped = json.loads( (LOCALES.parent / "preferences.default.json").read_text(encoding="utf-8") ) assert shipped["bas_task"] == DEFAULT_BAS_TASK == 4 def test_a_stored_block_without_a_new_limit_still_gets_it(): """The upgrade path for a real preference file, which is merged one level deep. A stored ``safety_limits`` replaces the whole default block, so a limit added later would read as "not set" — and motion is refused for a limit the operator cannot see anywhere in the file. Filling the block key by key is what stops that. """ from orisys_app_shared.paths import preference_path from orisys_autocal.preferences import load_preferences path = preference_path() path.parent.mkdir(parents=True, exist_ok=True) # A file written before the stage limits existed, with a chosen travel limit. path.write_text( json.dumps( { "safety_limits": {"max_travel_mm": 42.0}, "sensor_setup": {"press_axis": 4}, "axis_roles": {"x": 0, "y": 1, "pitch": 2, "yaw": 3}, } ), encoding="utf-8", ) prefs = load_preferences() limits = prefs["safety_limits"] assert limits["max_travel_mm"] == 42.0, "a chosen limit was overwritten" assert limits["max_yaw_deg"] > 0 and limits["max_pitch_deg"] > 0 assert limits["contact_search_mm"] > 0 # The old duplicate press axis moves into the mapping, and the key goes. assert prefs["axis_roles"]["z"] == 4 assert "press_axis" not in prefs["sensor_setup"] def _walk(node, prefix: str = ""): for key, value in node.items(): path = f"{prefix}{key}" if isinstance(value, dict): yield from _walk(value, f"{path}.") else: yield path, value