103 lines
4.4 KiB
Python
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)
|