Files
minecraft-launcher/tools/check_translations.py
T

103 lines
4.4 KiB
Python

#!/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)