| | |
| | | EXPECTED_LANGUAGE_COUNT = 29 |
| | | ALLOWED_ENGLISH_COPY_KEYS = { |
| | | "pro.card.badge", |
| | | "settings.proCompare.pro", |
| | | } |
| | | REQUIRED_PLACEHOLDERS = { |
| | | "pro.notes.remaining": {"%limit%", "%remaining%"}, |
| | | "pro.theme.status.previewCountdown": {"%time%"}, |
| | | } |
| | | REQUIRED_NON_PRO_KEYS = [ |
| | | "settings.proStatus.freeUser", |
| | | "settings.proStatus.proUser", |
| | | "settings.proStatus.lockedListTitle", |
| | | "settings.proStatus.unlockedListTitle", |
| | | "settings.proStatus.lockedFeature", |
| | | "settings.proStatus.unlockedFeature", |
| | | "settings.proStatus.freeNotes", |
| | | "settings.proStatus.unlimitedNotes", |
| | | "settings.proStatus.limitedFeature", |
| | | "settings.proStatus.unsupportedFeature", |
| | | "settings.proStatus.unavailableFeature", |
| | | "settings.proStatus.previewFiveMinutes", |
| | | "settings.proFeature.appNotes", |
| | | "settings.proFeature.dataBackup", |
| | | "settings.proBenefit.general", |
| | | "settings.proBenefit.hotkeys", |
| | | "settings.proBenefit.tags", |
| | | "settings.proBenefit.data", |
| | | "settings.proBenefit.about", |
| | | "settings.proGuide.general", |
| | | "settings.proGuide.hotkeys", |
| | | "settings.proGuide.tags", |
| | | "settings.proGuide.data", |
| | | "settings.proCompare.title", |
| | | "settings.proCompare.free", |
| | | "settings.proCompare.pro", |
| | | "tag.customColor", |
| | | ] |
| | | REQUIRED_VISIBLE_SETTINGS_KEYS = [ |
| | | "settings.bubbleDisplayScope", |
| | | "settings.bubbleAllApps", |
| | | "settings.bubbleUncommonOnly", |
| | | "quickSearch.hotkeys", |
| | | "quickSearch.mainHotkey", |
| | | "quickSearch.mainHotkeyDesc", |
| | | "quickSearch.internalHotkey", |
| | | "quickSearch.internalHotkeyDesc", |
| | | "quickSearch.internalHotkeyStatus", |
| | | "quickSearch.globalHotkey", |
| | | "quickSearch.globalHotkeyDesc", |
| | | "quickSearch.status.Active", |
| | | "quickSearch.status.registrationFailed", |
| | | "quickSearch.status.unavailable", |
| | | "quickSearch.mainHotkeyUnavailableMessage", |
| | | "quickSearch.globalHotkeyUnavailableMessage", |
| | | "quickSearch.spaceDisplay", |
| | | ] |
| | | OLD_NOTE_LIMIT_PATTERNS = [ |
| | | re.compile(r"5[- ]?(note|notes|app notes)", re.I), |
| | | re.compile(r"5\s*个应用备注"), |
| | | re.compile(r"5\s*條備註"), |
| | | re.compile(r"5\s*条备注"), |
| | | ] |
| | | |
| | | |
| | | def fail(message: str) -> None: |
| | |
| | | pro_keys = [key for key in english if key.startswith("pro.")] |
| | | if not pro_keys: |
| | | fail("en.json has no pro.* keys") |
| | | if english.get("settings.proStatus.lockedFeature") != "✅ Supported": |
| | | fail("en.json Pro comparison support status must be '✅ Supported'") |
| | | if english.get("settings.proStatus.unlimitedNotes") != "✅ Unlimited notes": |
| | | fail("en.json Pro comparison unlimited notes status must include the leading checkmark") |
| | | if english.get("settings.proStatus.unlockedFeature") != "✅ Unlocked": |
| | | fail("en.json Pro comparison unlocked status must be '✅ Unlocked'") |
| | | |
| | | zh_hans = load_json(localization_dir / "zh-Hans.json") |
| | | if zh_hans.get("settings.proStatus.lockedFeature") != "✅支持": |
| | | fail("zh-Hans.json Pro comparison support status must be '✅支持'") |
| | | if zh_hans.get("settings.proStatus.unlimitedNotes") != "✅无限备注": |
| | | fail("zh-Hans.json Pro comparison unlimited notes status must be '✅无限备注'") |
| | | if zh_hans.get("settings.proStatus.unlockedFeature") != "✅已解锁": |
| | | fail("zh-Hans.json Pro comparison unlocked status must be '✅已解锁'") |
| | | |
| | | ko = load_json(localization_dir / "ko.json") |
| | | if ko.get("quickSearch.hotkeys") != "키보드 단축키": |
| | | fail("ko.json quickSearch.hotkeys must not fall back to English") |
| | | if ko.get("quickSearch.mainHotkey") != "앱 목록": |
| | | fail("ko.json quickSearch.mainHotkey must not fall back to English") |
| | | if ko.get("quickSearch.status.Active") != "활성": |
| | | fail("ko.json quickSearch.status.Active must not fall back to English") |
| | | if ko.get("settings.bubbleDisplayScope") != "말풍선 팁 표시 대상:": |
| | | fail("ko.json settings.bubbleDisplayScope must not fall back to English") |
| | | |
| | | ar = load_json(localization_dir / "ar.json") |
| | | if ar.get("quickSearch.hotkeys") != "اختصارات لوحة المفاتيح": |
| | | fail("ar.json quickSearch.hotkeys must not fall back to English") |
| | | if ar.get("quickSearch.mainHotkey") != "قائمة التطبيقات": |
| | | fail("ar.json quickSearch.mainHotkey must not fall back to English") |
| | | if ar.get("quickSearch.status.Active") != "مفعّل": |
| | | fail("ar.json quickSearch.status.Active must not fall back to English") |
| | | if ar.get("settings.bubbleDisplayScope") != "إظهار تلميحات الفقاعات لـ:": |
| | | fail("ar.json settings.bubbleDisplayScope must not fall back to English") |
| | | |
| | | for code in language_codes: |
| | | path = localization_dir / f"{code}.json" |
| | | if not path.is_file(): |
| | | fail(f"missing localization file: {path}") |
| | | data = load_json(path) |
| | | required_keys = pro_keys + REQUIRED_NON_PRO_KEYS + REQUIRED_VISIBLE_SETTINGS_KEYS |
| | | missing = [ |
| | | key for key in pro_keys |
| | | key for key in required_keys |
| | | if not isinstance(data.get(key), str) or not data.get(key, "").strip() |
| | | ] |
| | | if missing: |
| | | fail(f"{code}.json is missing pro translations: {', '.join(missing)}") |
| | | fail(f"{code}.json is missing Pro/settings translations: {', '.join(missing)}") |
| | | |
| | | badge = data["pro.card.badge"] |
| | | if data.get("pro.card.title.free") == badge: |
| | | fail(f"{code}.json uses the badge text as the upgrade title for pro.card.title.free") |
| | | |
| | | dirty_keys = [key for key in pro_keys if "__" in data[key] or "###FILE" in data[key]] |
| | | dirty_keys = [key for key in required_keys if "__" in data[key] or "###FILE" in data[key]] |
| | | if dirty_keys: |
| | | fail(f"{code}.json still contains placeholder artifacts: {', '.join(dirty_keys)}") |
| | | |
| | | old_limit_keys = [ |
| | | key for key, value in data.items() |
| | | if isinstance(value, str) |
| | | and any(pattern.search(value) for pattern in OLD_NOTE_LIMIT_PATTERNS) |
| | | ] |
| | | if old_limit_keys: |
| | | fail(f"{code}.json still contains old 5-note copy: {', '.join(old_limit_keys)}") |
| | | |
| | | placeholder_errors = [] |
| | | for key, placeholders in REQUIRED_PLACEHOLDERS.items(): |
| | |
| | | |
| | | if code != "en": |
| | | copied_english = [ |
| | | key for key in pro_keys |
| | | key for key in required_keys |
| | | if key not in ALLOWED_ENGLISH_COPY_KEYS and data.get(key) == english.get(key) |
| | | ] |
| | | if copied_english: |
| | |
| | | |
| | | print( |
| | | "PASS Pro localization QA: " |
| | | f"{len(language_codes)} languages, {len(pro_keys)} keys, " |
| | | f"{len(language_codes)} languages, {len(pro_keys) + len(REQUIRED_NON_PRO_KEYS) + len(REQUIRED_VISIBLE_SETTINGS_KEYS)} Pro/settings keys, " |
| | | "no placeholder artifacts or English fallback copies" |
| | | ) |
| | | PY |