yunohost/maintenance/missing_i18n_keys.py
2024-07-11 15:53:34 +02:00

230 lines
8.1 KiB
Python

# -*- coding: utf-8 -*-
import toml
import os
import re
import glob
import json
import yaml
import subprocess
import sys
ROOT = os.path.dirname(__file__) + "/../"
LOCALE_FOLDER = ROOT + "/locales/"
REFERENCE_FILE = LOCALE_FOLDER + "en.json"
###############################################################################
# Find used keys in python code #
###############################################################################
def find_expected_string_keys():
# Try to find :
# m18n.n( "foo"
# YunohostError("foo"
# YunohostValidationError("foo"
# # i18n: foo
p1 = re.compile(r"m18n\.n\(\n*\s*[\"\'](\w+)[\"\']")
p2 = re.compile(r"YunohostError\(\n*\s*[\'\"](\w+)[\'\"]")
p3 = re.compile(r"YunohostValidationError\(\n*\s*[\'\"](\w+)[\'\"]")
p4 = re.compile(r"# i18n: [\'\"]?(\w+)[\'\"]?")
python_files = glob.glob(ROOT + "src/*.py")
python_files.extend(glob.glob(ROOT + "src/utils/*.py"))
python_files.extend(glob.glob(ROOT + "src/migrations/*.py"))
python_files.extend(glob.glob(ROOT + "src/migrations/*.py.disabled"))
python_files.extend(glob.glob(ROOT + "src/authenticators/*.py"))
python_files.extend(glob.glob(ROOT + "src/diagnosers/*.py"))
python_files.append(ROOT + "bin/yunohost")
for python_file in python_files:
content = open(python_file).read()
for m in p1.findall(content):
if m.endswith("_"):
continue
yield m
for m in p2.findall(content):
if m.endswith("_"):
continue
yield m
for m in p3.findall(content):
if m.endswith("_"):
continue
yield m
for m in p4.findall(content):
yield m
# For each diagnosis, try to find strings like "diagnosis_stuff_foo" (c.f. diagnosis summaries)
# Also we expect to have "diagnosis_description_<name>" for each diagnosis
p3 = re.compile(r"[\"\'](diagnosis_[a-z]+_\w+)[\"\']")
for python_file in glob.glob(ROOT + "src/diagnosers/*.py"):
if "__init__.py" in python_file:
continue
content = open(python_file).read()
for m in p3.findall(content):
if m.endswith("_"):
# Ignore some name fragments which are actually concatenated with other stuff..
continue
yield m
yield "diagnosis_description_" + os.path.basename(python_file)[:-3].split("-")[
-1
]
# For each migration, expect to find "migration_description_<name>"
for path in glob.glob(ROOT + "src/migrations/*.py"):
if "__init__" in path:
continue
yield "migration_description_" + os.path.basename(path)[:-3]
# FIXME: to be removed in bookworm branch
yield "migration_description_0027_migrate_to_bookworm"
# For each default service, expect to find "service_description_<name>"
for service, info in yaml.safe_load(
open(ROOT + "conf/yunohost/services.yml")
).items():
if info is None:
continue
yield "service_description_" + service
# For all unit operations, expect to find "log_<name>"
# A unit operation is created either using the @is_unit_operation decorator
# or using OperationLogger(
cmd = f"grep -hr '@is_unit_operation' {ROOT}/src/ -A3 2>/dev/null | grep '^def' | sed -E 's@^def (\\w+)\\(.*@\\1@g'"
for funcname in (
subprocess.check_output(cmd, shell=True).decode("utf-8").strip().split("\n")
):
yield "log_" + funcname
p4 = re.compile(r"OperationLogger\(\n*\s*[\"\'](\w+)[\"\']")
for python_file in python_files:
content = open(python_file).read()
for m in ("log_" + match for match in p4.findall(content)):
yield m
# Keys for the actionmap ...
for category in yaml.safe_load(open(ROOT + "share/actionsmap.yml")).values():
if "actions" not in category.keys():
continue
for action in category["actions"].values():
if "arguments" not in action.keys():
continue
for argument in action["arguments"].values():
extra = argument.get("extra")
if not extra:
continue
if "password" in extra:
yield extra["password"]
if "ask" in extra:
yield extra["ask"]
if "comment" in extra:
yield extra["comment"]
if "pattern" in extra:
yield extra["pattern"][1]
if "help" in extra:
yield extra["help"]
# Hardcoded expected keys ...
yield "admin_password" # Not sure that's actually used nowadays...
for method in ["tar", "copy", "custom"]:
yield "backup_applying_method_%s" % method
yield "backup_method_%s_finished" % method
registrars = toml.load(open(ROOT + "share/registrar_list.toml"))
supported_registrars = ["ovh", "gandi", "godaddy"]
for registrar in supported_registrars:
for key in registrars[registrar].keys():
yield f"domain_config_{key}"
# Domain config panel
domain_config = toml.load(open(ROOT + "share/config_domain.toml"))
for panel in domain_config.values():
if not isinstance(panel, dict):
continue
for section in panel.values():
if not isinstance(section, dict):
continue
for key, values in section.items():
if not isinstance(values, dict):
continue
yield f"domain_config_{key}"
# Global settings
global_config = toml.load(open(ROOT + "share/config_global.toml"))
# Boring hard-coding because there's no simple other way idk
settings_without_help_key = [
"passwordless_sudo",
"smtp_relay_host",
"smtp_relay_password",
"smtp_relay_port",
"smtp_relay_user",
"ssowat_panel_overlay_enabled",
"root_password",
"root_access_explain",
"root_password_confirm",
]
for panel in global_config.values():
if not isinstance(panel, dict):
continue
for section in panel.values():
if not isinstance(section, dict):
continue
for key, values in section.items():
if not isinstance(values, dict):
continue
yield f"global_settings_setting_{key}"
if key not in settings_without_help_key:
yield f"global_settings_setting_{key}_help"
###############################################################################
# Compare keys used and keys defined #
###############################################################################
if len(sys.argv) <= 1 or sys.argv[1] not in ["--check", "--fix"]:
print("Please specify --check or --fix")
sys.exit(1)
expected_string_keys = set(find_expected_string_keys())
keys_defined_for_en = json.loads(open(REFERENCE_FILE).read()).keys()
keys_defined = set(keys_defined_for_en)
unused_keys = keys_defined.difference(expected_string_keys)
unused_keys = sorted(unused_keys)
undefined_keys = expected_string_keys.difference(keys_defined)
undefined_keys = sorted(undefined_keys)
mode = sys.argv[1].strip("-")
if mode == "check":
# Unused keys are not too problematic, will be automatically
# removed by the other autoreformat script,
# but still informative to display them
if unused_keys:
print(
"Those i18n keys appears unused:\n" " - " + "\n - ".join(unused_keys)
)
if undefined_keys:
print(
"Those i18n keys should be defined in en.json:\n"
" - " + "\n - ".join(undefined_keys)
)
sys.exit(1)
elif mode == "fix":
j = json.loads(open(REFERENCE_FILE).read())
for key in undefined_keys:
j[key] = "FIXME"
for key in unused_keys:
del j[key]
json.dump(
j,
open(REFERENCE_FILE, "w"),
indent=4,
ensure_ascii=False,
sort_keys=True,
)