#!/usr/bin/env python3 """Проверка каталога переводов. Только чтение; ненулевой код возврата при ошибке. Запуск: python3 tools/check_translations.py """ import io, json, os, re, sys ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) CATALOG = os.path.join(ROOT, "i18n", "translations.json") CPP_TEXT = re.compile(r'Loc::text\("([^"]+)"\)') CPP_LIST = re.compile(r'Loc::list\("([^"]+)"\)') QML_PATH = re.compile(r'\bLoc\.t((?:\.[A-Za-z_][A-Za-z0-9_]*)+)') STRAGGLER = re.compile(r'\b(tr|qsTr)\("|QCoreApplication::translate') errors, warnings = [], [] def sources(*exts): for name in sorted(os.listdir(ROOT)): if name.endswith(exts): yield name, io.open(os.path.join(ROOT, name), encoding="utf-8").read() doc = json.load(io.open(CATALOG, encoding="utf-8")) languages = doc["_meta"]["languages"] strings = doc["strings"] # 1. Языки: одинаковый набор ключей, ничего не пустует, типы совпадают. for key, entry in strings.items(): missing = [c for c in languages if c not in entry] if missing: errors.append(f"{key}: нет языков {missing}") continue kinds = {c: type(entry[c]).__name__ for c in languages} if len(set(kinds.values())) != 1: errors.append(f"{key}: разный тип значения по языкам — {kinds}") continue if isinstance(entry[languages[0]], list): sizes = {c: len(entry[c]) for c in languages} if len(set(sizes.values())) != 1: errors.append(f"{key}: разная длина списка — {sizes}") for c in languages: if any(not v.strip() for v in entry[c]): errors.append(f"{key}: пустой элемент списка в «{c}»") else: for c in languages: if not entry[c].strip(): errors.append(f"{key}: пустой перевод в «{c}»") # 2. Набор %N обязан совпадать: порядок слов может отличаться, состав — нет. holders = {c: set(re.findall(r"%\d", entry[c])) for c in languages} if len(set(map(frozenset, holders.values()))) != 1: errors.append(f"{key}: разный набор подстановок — {holders}") lists = {k for k, e in strings.items() if isinstance(e[languages[0]], list)} singles = set(strings) - lists # 3. Каждый ключ из C++ существует и совпадает по типу. used = set() for name, src in sources(".cpp", ".h"): for key in CPP_TEXT.findall(src): if key in singles: used.add(key) else: errors.append(f"{name}: Loc::text(\"{key}\") — нет такого ключа-строки") for key in CPP_LIST.findall(src): if key in lists: used.add(key) else: errors.append(f"{name}: Loc::list(\"{key}\") — нет такого ключа-списка") # 4. Каждый путь Loc.t.a.b.c из QML разворачивается в существующий ключ. for name, src in sources(".qml"): for path in QML_PATH.findall(src): key = path.lstrip(".") if key in strings: used.add(key) continue # Loc.t.a.b.slice(0, 2) и подобное: последний сегмент — метод JS. parent = key.rsplit(".", 1)[0] if parent in strings: used.add(parent) continue errors.append(f"{name}: Loc.t.{key} — нет такого ключа") # 5. Ни одного оставшегося tr()/qsTr()/translate. for name, src in sources(".cpp", ".h", ".qml"): for i, line in enumerate(src.splitlines(), 1): if STRAGGLER.search(line): errors.append(f"{name}:{i}: остался вызов старой схемы перевода") # 6. Ключи, которые никто не использует, — предупреждение. for key in sorted(set(strings) - used): warnings.append(f"ключ {key} нигде не используется") print(f"ключей: {len(strings)} языки: {languages} использовано: {len(used)}") for w in warnings: print(" предупреждение:", w) for e in errors: print(" ОШИБКА:", e) print("ОШИБОК НЕТ" if not errors else f"\nошибок: {len(errors)}") sys.exit(1 if errors else 0)