Files
deflock-app/scripts/localize.py
T

222 lines
7.1 KiB
Python
Executable File

#!/usr/bin/env python3
"""
localize.py — small helper for managing lib/localizations/*.json translation files.
Usage (normally invoked via scripts/localize.sh):
localize.sh --check
Validate that every locale file is valid JSON, has a language.name key,
and has exactly the same set of leaf keys as en.json (the reference).
Exits non-zero (and prints details) if anything doesn't match.
localize.sh --list [--lang xx]
Print all dot-notation leaf keys. Defaults to en.json if --lang is
omitted. Useful for seeing the full key structure at a glance.
localize.sh --get --lang xx --key some.nested.key
Print the value of a single key from lib/localizations/xx.json.
localize.sh --write --lang xx --key some.nested.key "New value"
Set (or add) the value of a key in lib/localizations/xx.json. Creates
intermediate objects as needed. Preserves key order/formatting via
Python's json module (2-space indent, matching existing files).
Examples:
./scripts/localize.sh --check
./scripts/localize.sh --list --lang de
./scripts/localize.sh --get --lang en --key node.title
./scripts/localize.sh --write --lang es --key node.checkingNodeStatusTitle "Comprobando estado"
"""
import argparse
import json
import sys
from pathlib import Path
LOCALIZATIONS_DIR = Path(__file__).resolve().parent.parent / "lib" / "localizations"
REFERENCE_LOCALE = "en"
def locale_path(lang: str) -> Path:
return LOCALIZATIONS_DIR / f"{lang}.json"
def load_locale(lang: str) -> dict:
path = locale_path(lang)
if not path.exists():
print(f"error: no such locale file: {path}", file=sys.stderr)
sys.exit(1)
try:
with open(path, "r", encoding="utf-8") as f:
return json.load(f)
except json.JSONDecodeError as e:
print(f"error: {path.name} is not valid JSON: {e}", file=sys.stderr)
sys.exit(1)
def save_locale(lang: str, data: dict) -> None:
path = locale_path(lang)
with open(path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2)
f.write("\n")
def all_locale_files():
return sorted(LOCALIZATIONS_DIR.glob("*.json"))
def extract_leaf_keys(data, prefix=""):
keys = set()
for k, v in data.items():
full_key = f"{prefix}.{k}" if prefix else k
if isinstance(v, dict):
keys |= extract_leaf_keys(v, full_key)
else:
keys.add(full_key)
return keys
def get_nested(data: dict, dotted_key: str):
parts = dotted_key.split(".")
node = data
for part in parts:
if not isinstance(node, dict) or part not in node:
return None
node = node[part]
return node
def set_nested(data: dict, dotted_key: str, value: str) -> None:
parts = dotted_key.split(".")
node = data
for part in parts[:-1]:
if part not in node or not isinstance(node[part], dict):
node[part] = {}
node = node[part]
node[parts[-1]] = value
def cmd_check(_args) -> int:
files = all_locale_files()
if not files:
print(f"error: no locale files found in {LOCALIZATIONS_DIR}", file=sys.stderr)
return 1
ref_path = locale_path(REFERENCE_LOCALE)
if not ref_path.exists():
print(f"error: reference locale '{REFERENCE_LOCALE}.json' not found", file=sys.stderr)
return 1
ok = True
parsed = {}
for path in files:
try:
with open(path, "r", encoding="utf-8") as f:
parsed[path.stem] = json.load(f)
except json.JSONDecodeError as e:
print(f"FAIL {path.name}: invalid JSON: {e}")
ok = False
for lang, data in parsed.items():
lang_name = data.get("language", {}).get("name")
if not isinstance(lang_name, str) or not lang_name:
print(f"FAIL {lang}.json: missing 'language.name' string")
ok = False
if REFERENCE_LOCALE not in parsed:
print(f"error: {REFERENCE_LOCALE}.json failed to parse, cannot compare keys")
return 1
reference_keys = extract_leaf_keys(parsed[REFERENCE_LOCALE])
for lang, data in parsed.items():
if lang == REFERENCE_LOCALE:
continue
keys = extract_leaf_keys(data)
missing = reference_keys - keys
extra = keys - reference_keys
if missing:
print(f"FAIL {lang}.json: missing {len(missing)} key(s):")
for k in sorted(missing):
print(f" - {k}")
ok = False
if extra:
print(f"FAIL {lang}.json: has {len(extra)} extra key(s) not in {REFERENCE_LOCALE}.json:")
for k in sorted(extra):
print(f" + {k}")
ok = False
if ok:
print(f"OK {len(parsed)} locale file(s) valid, all keys match {REFERENCE_LOCALE}.json")
return 0
return 1
def cmd_list(args) -> int:
lang = args.lang or REFERENCE_LOCALE
data = load_locale(lang)
keys = sorted(extract_leaf_keys(data))
for k in keys:
print(k)
return 0
def cmd_get(args) -> int:
if not args.lang or not args.key:
print("error: --get requires --lang and --key", file=sys.stderr)
return 1
data = load_locale(args.lang)
value = get_nested(data, args.key)
if value is None:
print(f"error: key '{args.key}' not found in {args.lang}.json", file=sys.stderr)
return 1
if isinstance(value, dict):
print(f"error: '{args.key}' is not a leaf key (it's an object) in {args.lang}.json", file=sys.stderr)
return 1
print(value)
return 0
def cmd_write(args) -> int:
if not args.lang or not args.key or args.value is None:
print("error: --write requires --lang, --key, and a value argument", file=sys.stderr)
return 1
data = load_locale(args.lang)
set_nested(data, args.key, args.value)
save_locale(args.lang, data)
print(f"Wrote {args.lang}.json: {args.key} = {args.value!r}")
return 0
def main():
parser = argparse.ArgumentParser(
description="Manage DeFlock localization JSON files.",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--check", action="store_true", help="Validate all locale files match en.json's key structure")
group.add_argument("--list", action="store_true", help="List all dot-notation leaf keys for a locale (default: en)")
group.add_argument("--get", action="store_true", help="Print the value of a single key")
group.add_argument("--write", action="store_true", help="Set the value of a single key")
parser.add_argument("--lang", help="Language code, e.g. 'en', 'de', 'es' (filename without .json)")
parser.add_argument("--key", help="Dot-notation key, e.g. 'node.title'")
parser.add_argument("value", nargs="?", help="New value (only used with --write)")
args = parser.parse_args()
if args.check:
sys.exit(cmd_check(args))
elif args.list:
sys.exit(cmd_list(args))
elif args.get:
sys.exit(cmd_get(args))
elif args.write:
sys.exit(cmd_write(args))
if __name__ == "__main__":
main()