From 5f8820d41f52a208fe6d5ccfb52bc48045b10aa5 Mon Sep 17 00:00:00 2001 From: Fallout <228031339+Fallout-rtg@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:44:18 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9F=D0=B5=D1=80=D0=B5=D0=B2=D0=BE=D0=B4=20se?= =?UTF-8?q?cret=20=D0=B8=20donate=20=D0=BD=D0=B0=20=D1=80=D1=83=D1=81?= =?UTF-8?q?=D1=81=D0=BA=D0=B8=D0=B9=20=D1=8F=D0=B7=D1=8B=D0=BA=20(#1419)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/test_i18n.py | 70 +++++++++++++++++++++++++++++++++++++++++++++ ui/i18n/en.json | 1 + ui/i18n/ru.json | 9 +++--- ui/settings_form.py | 2 +- 4 files changed, 77 insertions(+), 5 deletions(-) create mode 100644 tests/test_i18n.py diff --git a/tests/test_i18n.py b/tests/test_i18n.py new file mode 100644 index 0000000..8df5c88 --- /dev/null +++ b/tests/test_i18n.py @@ -0,0 +1,70 @@ +import json +import unittest +from pathlib import Path + +LOCALES_DIR = Path(__file__).parents[1] / "ui" / "i18n" + +# Values that are intentionally identical across locales +# (brand names, technical terms, format-only strings). +# Note: settings.language is deliberately "Language" in ru.json — +# upstream keeps the language selector label in English on purpose. +ALLOWED_UNTRANSLATED = frozenset({ + "app.name", + "button.test_loading", + "connectivity.cfworker_title", + "connectivity.error_line", + "first_run.manual_mtproto", + "language.en", + "language.ru", + "section.cfproxy", + "section.cfworker", + "settings.language", +}) + + +def _load(code: str) -> dict: + with open(LOCALES_DIR / f"{code}.json", encoding="utf-8") as f: + return json.load(f) + + +class I18nParityTest(unittest.TestCase): + def test_en_ru_key_sets_match(self): + en = _load("en") + ru = _load("ru") + self.assertEqual( + set(en), set(ru), + f"Missing in ru: {sorted(set(en) - set(ru))}; " + f"missing in en: {sorted(set(ru) - set(en))}", + ) + + def test_ru_has_no_untouched_english_leftovers(self): + en = _load("en") + ru = _load("ru") + leftovers = sorted( + key for key in set(en) & set(ru) + if en[key] == ru[key] and key not in ALLOWED_UNTRANSLATED + ) + self.assertEqual(leftovers, []) + + def test_format_placeholders_match(self): + import string + + en = _load("en") + ru = _load("ru") + for key in set(en) & set(ru): + en_fields = { + name for _, name, _, _ in string.Formatter().parse(en[key]) + if name + } + ru_fields = { + name for _, name, _, _ in string.Formatter().parse(ru[key]) + if name + } + self.assertEqual( + en_fields, ru_fields, + f"Placeholder mismatch for key {key!r}", + ) + + +if __name__ == "__main__": + unittest.main() diff --git a/ui/i18n/en.json b/ui/i18n/en.json index 7dbda52..3ef5859 100644 --- a/ui/i18n/en.json +++ b/ui/i18n/en.json @@ -69,6 +69,7 @@ "button.update": "Update", "button.page": "Page", "button.close": "Close", + "button.donate": "Donate ♥", "validation.bad_host": "Invalid IP address.", "validation.bad_port": "Port must be a number between 1 and 65535", diff --git a/ui/i18n/ru.json b/ui/i18n/ru.json index e58c0f5..bf1ad67 100644 --- a/ui/i18n/ru.json +++ b/ui/i18n/ru.json @@ -28,7 +28,7 @@ "label.host": "IP-адрес", "label.port": "Порт", - "label.secret": "Secret", + "label.secret": "Ключ", "label.dc_hint": "По одному правилу на строку, формат: номер:IP", "label.cf_enable": "Включить CF-прокси", "label.cf_custom_domain": "Свой домен", @@ -69,11 +69,12 @@ "button.update": "Обновить", "button.page": "Страница", "button.close": "Закрыть", + "button.donate": "Поддержать ♥", "validation.bad_host": "Некорректный IP-адрес.", "validation.bad_port": "Порт должен быть числом 1-65535", - "validation.bad_secret_len": "Secret должен содержать ровно 32 hex-символа (16 байт).", - "validation.bad_secret_hex": "Secret должен состоять только из hex-символов (0-9, a-f).", + "validation.bad_secret_len": "Ключ должен содержать ровно 32 hex-символа (16 байт).", + "validation.bad_secret_hex": "Ключ должен состоять только из hex-символов (0-9, a-f).", "validation.dc_format": "Неверный формат DC:IP: {entry}", "validation.dc_invalid": "Неверная запись DC:IP: {entry}", @@ -109,7 +110,7 @@ "first_run.manual": " Вручную:", "first_run.manual_path": " Настройки → Продвинутые → Тип подключения → Прокси", "first_run.manual_mtproto": " MTProto → {host} : {port}", - "first_run.manual_secret": " Secret: dd{secret}", + "first_run.manual_secret": " Ключ: dd{secret}", "first_run.open_now": "Открыть прокси в Telegram сейчас", "tray.open_telegram": "Открыть в Telegram ({host}:{port})", diff --git a/ui/settings_form.py b/ui/settings_form.py index 67fd349..e50d559 100644 --- a/ui/settings_form.py +++ b/ui/settings_form.py @@ -135,7 +135,7 @@ def _create_header(ctk, frame, theme): ).pack(side="right", padx=(4, 0)) ctk.CTkButton( - header, text="Donate ♥", width=90, height=28, + header, text=t("button.donate"), width=90, height=28, font=(theme.ui_font_family, 13, "bold"), corner_radius=8, fg_color="#22c55e", hover_color="#16a34a", text_color="#ffffff", border_width=0,