Update AgentKit to 0.19.2, align the maintenance gitlink and resolved package revision, and unify localization validation.
247 lines
13 KiB
Python
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()
|