Files
2026-08-09 07:39:39 +02:00

123 lines
3.9 KiB
Python

"""Merge new translation keys into every locale file.
Adds a nested patch of keys to all locales under src/i18n/locales/ so no locale
is ever left with a missing key. Existing values are never overwritten, so this
is safe to re-run.
The English text is written into every locale as a temporary placeholder. Those
placeholders must then be translated -- see i18n_apply_translations.py. A key
still holding English in a non-English locale is unfinished work, not a valid
default.
Usage:
python3 .agents/skills/i18n/i18n_add_keys.py <patch.json>
python3 .agents/skills/i18n/i18n_add_keys.py /tmp/new-keys.json --dry-run
The patch file mirrors the locale file structure, with values in English:
{
"address_book_ui": {
"unnamed_contact": "unnamed contact",
"save_without_name": "save without a registered name"
}
}
Key order is preserved: new keys are appended to the end of the block that owns
them, which is how core.json has always grown. The file is topic-grouped rather
than alphabetical, so this script does NOT re-sort on write (unlike its Qortal
Hub original). Pass --sort only if the project deliberately switches convention.
"""
import argparse
import json
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[3]
LOCALES = REPO_ROOT / 'src' / 'i18n' / 'locales'
NAMESPACE = 'core'
def languages():
"""Every locale directory that exists, so a new language needs no edit here."""
return sorted(d.name for d in LOCALES.iterdir() if d.is_dir())
def sort_node(node):
if isinstance(node, dict):
return {k: sort_node(node[k]) for k in sorted(node, key=str.lower)}
if isinstance(node, list):
return [sort_node(item) for item in node]
return node
def merge(dst, src, added, path=''):
"""Deep-merge src into dst without overwriting. Records added key paths."""
for key, value in src.items():
full = f'{path}.{key}' if path else key
if isinstance(value, dict):
if not isinstance(dst.get(key), dict):
dst[key] = {}
merge(dst[key], value, added, full)
elif key not in dst:
dst[key] = value
added.append(full)
def main():
parser = argparse.ArgumentParser(description=__doc__.split('\n')[0])
parser.add_argument('patch', help='JSON file of new keys, values in English')
parser.add_argument(
'--namespace',
default=NAMESPACE,
help=f'locale file to patch, without .json (default: {NAMESPACE})',
)
parser.add_argument(
'--dry-run',
action='store_true',
help='report what would change without writing',
)
parser.add_argument(
'--sort',
action='store_true',
help='alphabetize the whole file on write (NOT this repo\'s convention)',
)
args = parser.parse_args()
patch = json.loads(Path(args.patch).read_text(encoding='utf-8'))
langs = languages()
added_en = []
for lang in langs:
path = LOCALES / lang / f'{args.namespace}.json'
if not path.exists():
sys.exit(f'missing locale file: {path}')
data = json.loads(path.read_text(encoding='utf-8'))
added = []
merge(data, patch, added)
if lang == 'en':
added_en = added
if added and not args.dry_run:
out = sort_node(data) if args.sort else data
path.write_text(
json.dumps(out, ensure_ascii=False, indent=2) + '\n',
encoding='utf-8',
)
for key in added_en:
print(f' + {args.namespace}:{key}')
verb = 'would be added' if args.dry_run else 'added'
print(
f'\n{len(added_en)} keys {verb} to {args.namespace}.json '
f'across {len(langs)} locales'
)
if added_en and not args.dry_run:
print('Next: translate them with i18n_apply_translations.py')
if __name__ == '__main__':
main()