Files
Termind/Scripts/Tests/test_localization.py
T
Kaede Akino 0eba936203 feat(localization): complete German, French, and Korean support
Update AgentKit to 0.19.2, align the maintenance gitlink and resolved package revision, and unify localization validation.
2026-10-14 13:05:48 +08:00

247 lines
13 KiB
Python

from __future__ import annotations
import importlib.util
from pathlib import Path
import unittest
SPEC = importlib.util.spec_from_file_location(
"check_localization", Path(__file__).resolve().parents[1] / "check-localization.py"
)
assert SPEC is not None and SPEC.loader is not None
CHECK = importlib.util.module_from_spec(SPEC)
SPEC.loader.exec_module(CHECK)
def catalog(key: str, value: str, state: str = "translated") -> dict:
return {
"sourceLanguage": "en",
"strings": {key: {"localizations": {"ja": {"stringUnit": {"state": state, "value": value}}}}},
}
def japanese_problems(data):
return CHECK.problems(data, languages=("ja",))
class JapaneseLocalizationTests(unittest.TestCase):
def test_rejects_spaces_in_both_directions_for_all_japanese_scripts(self):
for value in ["日本語 API", "API 日本語", "ひらがな SSH", "SSH ひらがな", "サーバ API", "API サーバ",
"サーバ\tSSH", "SSH\u00a0サーバ", "サーバ\u3000SSH", "3 台", "サーバ 3"]:
with self.subTest(value=value):
self.assertTrue(any("spacing" in p for p in japanese_problems(catalog("Label", value))))
def test_retains_english_protocol_spaces_and_japanese_label_spacing(self):
for key, value in [("Label", "ssh user@hostで接続"), ("Label", "種類 不明"), ("Label", "SSHサーバ"),
("Type %@", "種類 %@"), ("Type %@", "種類 %1$@")]:
with self.subTest(value=value):
self.assertEqual(japanese_problems(catalog(key, value)), [])
def test_type_exception_is_narrow(self):
self.assertTrue(japanese_problems(catalog("Label %@", "種類 %@")))
self.assertTrue(japanese_problems(catalog("Type %@", "種類 %@")))
self.assertTrue(japanese_problems(catalog("Type %@", "種類 %@の SSH鍵")))
def test_reordered_arguments_keep_their_original_types(self):
self.assertEqual(japanese_problems(catalog("%lld on %@", "%2$@で%1$lld件")), [])
self.assertTrue(japanese_problems(catalog("%lld on %@", "%1$@で%2$lld件")))
self.assertTrue(japanese_problems(catalog("%lld on %@", "%@で%lld件")))
def test_rejects_lost_arguments_and_literal_percent(self):
self.assertTrue(japanese_problems(catalog("%lld%% complete", "%lld完了")))
self.assertTrue(japanese_problems(catalog("Host %@", "ホスト")))
self.assertEqual(japanese_problems(catalog("%lld%% complete", "%lld%%完了")), [])
def test_uses_localized_english_value_for_metadata_keys(self):
data = catalog("NSCameraUsageDescription", "カメラを使用します。")
data["strings"]["NSCameraUsageDescription"]["localizations"]["en"] = {
"stringUnit": {"state": "translated", "value": "Use the camera."}
}
self.assertEqual(japanese_problems(data), [])
def test_rejects_missing_unfinished_and_empty_translations(self):
data = catalog("Settings", "設定")
data["strings"]["Settings"]["localizations"] = {}
self.assertTrue(japanese_problems(data))
self.assertTrue(japanese_problems(catalog("Settings", "設定", "needs_review")))
self.assertTrue(japanese_problems(catalog("Settings", "")))
self.assertEqual(japanese_problems(catalog("", "")), [])
def test_checks_plural_units_and_rejects_non_japanese_categories(self):
data = catalog("%lld files", "%lld個のファイル")
localizations = data["strings"]["%lld files"]["localizations"]
unit = localizations["ja"]
localizations["en"] = {"variations": {"plural": {
"one": {"stringUnit": {"state": "translated", "value": "%lld file"}},
"other": {"stringUnit": {"state": "translated", "value": "%lld files"}},
}}}
self.assertTrue(japanese_problems(data))
localizations["ja"] = {"variations": {"plural": {"other": unit}}}
self.assertEqual(japanese_problems(data), [])
unit["stringUnit"]["value"] = "%lld ファイル"
self.assertTrue(japanese_problems(data))
unit["stringUnit"]["value"] = "%lld個のファイル"
localizations["ja"]["variations"]["plural"]["one"] = unit
self.assertTrue(japanese_problems(data))
def test_respects_nontranslatable_entries(self):
data = catalog("Identifier", "")
data["strings"]["Identifier"]["shouldTranslate"] = False
self.assertEqual(japanese_problems(data), [])
class ChineseLocalizationTests(unittest.TestCase):
def check(self, key, value, language):
data = catalog(key, value)
localizations = data["strings"][key]["localizations"]
localizations[language] = localizations.pop("ja")
return CHECK.problems(data, languages=(language,))
def test_preserves_chinese_spacing_and_punctuation_checks(self):
for language in ["zh-Hans", "zh-Hant"]:
for value in ["中文 SSH", "SSH 中文", "中文,", "中文: SSH", "SSH :中文"]:
with self.subTest(language=language, value=value):
self.assertTrue(self.check("Label", value, language))
self.assertEqual(self.check("Label", "中文SSH", language), [])
def test_preserves_type_subtitle_exception(self):
for language, label in [("zh-Hans", "类型"), ("zh-Hant", "類型")]:
self.assertEqual(self.check("Type %@", label + " %@", language), [])
self.assertTrue(self.check("Other %@", label + " %@", language))
def test_shared_validation_requires_every_supported_language(self):
data = catalog("Settings", "設定")
failures = CHECK.problems(data)
self.assertEqual(len(failures), len(CHECK.SUPPORTED_LANGUAGES) - 1)
self.assertTrue(any("zh-Hans" in failure for failure in failures))
self.assertTrue(any("zh-Hant" in failure for failure in failures))
class KoreanLocalizationTests(unittest.TestCase):
def make_catalog(self, key, value):
data = catalog(key, value)
localizations = data["strings"][key]["localizations"]
localizations["ko"] = localizations.pop("ja")
return data
def test_allows_korean_word_spacing_and_protocol_names(self):
for value in ["SSH 인증 실패", "파일 3개", "사용자 확인 요청 안 함", "암호 구문"]:
with self.subTest(value=value):
self.assertEqual(CHECK.problems(self.make_catalog("Label", value), languages=("ko",)), [])
def test_reordered_arguments_preserve_positions_and_types(self):
data = self.make_catalog("Command failed with exit code %lld after %@.",
"명령이 %2$@ 후 종료 코드 %1$lld로 실패했습니다.")
self.assertEqual(CHECK.problems(data, languages=("ko",)), [])
unit = data["strings"]["Command failed with exit code %lld after %@."]["localizations"]["ko"]["stringUnit"]
unit["value"] = "명령이 %@ 후 종료 코드 %lld로 실패했습니다."
self.assertTrue(any("format arguments" in p for p in CHECK.problems(data, languages=("ko",))))
def test_requires_only_other_for_counted_resources(self):
data = self.make_catalog("%lld files", "파일 %lld개")
localizations = data["strings"]["%lld files"]["localizations"]
unit = localizations["ko"]
localizations["en"] = {"variations": {"plural": {
"one": {"stringUnit": {"state": "translated", "value": "%lld file"}},
"other": {"stringUnit": {"state": "translated", "value": "%lld files"}},
}}}
self.assertTrue(CHECK.problems(data, languages=("ko",)))
localizations["ko"] = {"variations": {"plural": {"other": unit}}}
self.assertEqual(CHECK.problems(data, languages=("ko",)), [])
localizations["ko"]["variations"]["plural"]["one"] = unit
self.assertTrue(CHECK.problems(data, languages=("ko",)))
def test_rejects_missing_and_unfinished_korean(self):
data = self.make_catalog("Settings", "설정")
unit = data["strings"]["Settings"]["localizations"]["ko"]["stringUnit"]
unit["state"] = "needs_review"
self.assertEqual(CHECK.problems(data, languages=("ko",)), ["'Settings': unfinished ko translation"])
del data["strings"]["Settings"]["localizations"]["ko"]
self.assertEqual(CHECK.problems(data, languages=("ko",)), ["'Settings': missing ko translation"])
class EuropeanLocalizationTests(unittest.TestCase):
def make_catalog(self, key, value, language):
return {
"sourceLanguage": "en",
"strings": {key: {"localizations": {
language: {"stringUnit": {"state": "translated", "value": value}},
}}},
}
def test_checks_coverage_state_and_positional_arguments(self):
for language in ("de", "fr"):
with self.subTest(language=language):
data = self.make_catalog("%lld on %@", "%2$@ : %1$lld", language)
self.assertEqual(CHECK.problems(data, languages=(language,)), [])
unit = data["strings"]["%lld on %@"]["localizations"][language]["stringUnit"]
unit["value"] = "%1$@ : %2$lld"
self.assertTrue(CHECK.problems(data, languages=(language,)))
unit["value"] = "%lld on %@"
unit["state"] = "needs_review"
self.assertTrue(CHECK.problems(data, languages=(language,)))
del data["strings"]["%lld on %@"]["localizations"][language]
self.assertTrue(CHECK.problems(data, languages=(language,)))
def test_requires_singular_and_plural_even_for_target_only_variations(self):
for language in ("de", "fr"):
data = self.make_catalog("%lld files", "%lld fichiers", language)
localizations = data["strings"]["%lld files"]["localizations"]
unit = localizations[language]
localizations[language] = {"variations": {"plural": {"other": unit}}}
self.assertTrue(CHECK.problems(data, languages=(language,)))
forms = localizations[language]["variations"]["plural"]
forms["one"] = unit
self.assertEqual(CHECK.problems(data, languages=(language,)), [])
forms["few"] = unit
self.assertTrue(CHECK.problems(data, languages=(language,)))
del forms["few"]
forms["many"] = unit
failures = CHECK.problems(data, languages=(language,))
self.assertEqual(bool(failures), language == "de")
def test_english_plural_requires_translated_plural_variations(self):
for language in ("de", "fr"):
data = self.make_catalog("%lld files", "%lld fichiers", language)
localizations = data["strings"]["%lld files"]["localizations"]
localizations["en"] = {"variations": {"plural": {
"one": {"stringUnit": {"state": "translated", "value": "%lld file"}},
"other": {"stringUnit": {"state": "translated", "value": "%lld files"}},
}}}
self.assertTrue(CHECK.problems(data, languages=(language,)))
def test_each_plural_branch_preserves_format_arguments(self):
data = self.make_catalog("%lld files", "%lld fichiers", "fr")
localizations = data["strings"]["%lld files"]["localizations"]
localizations["fr"] = {"variations": {"plural": {
"one": {"stringUnit": {"state": "translated", "value": "un fichier"}},
"other": localizations["fr"],
}}}
self.assertTrue(any("format arguments" in p for p in CHECK.problems(data, languages=("fr",))))
def test_preserves_european_typography_without_cjk_spacing_rules(self):
for language, value in [("fr", "Échec\u00a0: «\u00a0clé SSH\u00a0»\u202f?"), ("de", "Schlüssel „SSH“")]:
data = self.make_catalog("Label", value, language)
self.assertEqual(CHECK.problems(data, languages=(language,)), [])
def test_explicit_scope_can_limit_checks_to_german_and_french(self):
data = self.make_catalog("Settings", "Einstellungen", "de")
self.assertEqual(CHECK.problems(data, languages=("de", "fr")), ["'Settings': missing fr translation"])
data["strings"]["Settings"]["localizations"]["fr"] = {
"stringUnit": {"state": "translated", "value": "Réglages"}
}
self.assertEqual(CHECK.problems(data, languages=("de", "fr")), [])
def test_uses_english_metadata_value_and_skips_nontranslatable_entries(self):
data = self.make_catalog("BundleKey", "Datei %@", "de")
data["strings"]["BundleKey"]["localizations"]["en"] = {
"stringUnit": {"state": "translated", "value": "File %@"}
}
self.assertEqual(CHECK.problems(data, languages=("de",)), [])
data["strings"]["Identifier"] = {"shouldTranslate": False}
self.assertEqual(CHECK.problems(data, languages=("de",)), [])
if __name__ == "__main__":
unittest.main()