From f8ae8f419dd273171e4fb9ce964a90ca6f8f918f Mon Sep 17 00:00:00 2001 From: sledgehammer999 Date: Tue, 12 Mar 2024 02:02:42 +0200 Subject: [PATCH] Add helper scripts to manage WebUI translations --- src/webui/www/split_merge_json.py | 128 +++++++++++++++++++++++ src/webui/www/translations_qt2i18next.py | 101 ++++++++++++++++++ 2 files changed, 229 insertions(+) create mode 100755 src/webui/www/split_merge_json.py create mode 100755 src/webui/www/translations_qt2i18next.py diff --git a/src/webui/www/split_merge_json.py b/src/webui/www/split_merge_json.py new file mode 100755 index 000000000..4fb27742a --- /dev/null +++ b/src/webui/www/split_merge_json.py @@ -0,0 +1,128 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# split_merge_json.py - script for splitting the JSON files from +# Transifex into their public/private parts and to merging the +# private/public en.json files to form the source file for Transifex +# +# Copyright (C) 2024 sledgehammer999 +# +# License: Public domain. + +import argparse +import glob +import json +import os +from pathlib import PurePath +import sys + +def updateJson(json_file: str, source: dict[str, str]) -> None: + trimmed_path: str = json_file + path_parts = PurePath(json_file).parts + if len(path_parts) >= 3: + trimmed_path = f"{path_parts[-3]}/{path_parts[-2]}/{path_parts[-1]}" + + if not os.path.exists(json_file): + print(f"\tWARNIG: {trimmed_path} doesn't exist") + return + + print(f"\tUpdating {trimmed_path}") + with open(json_file, mode="r+", encoding='utf-8') as file: + target: dict[str, str] = json.load(file) + if not isinstance(target, dict): + print(f"\tWARNIG: {trimmed_path} is malormed") + return + + for key in target.keys(): + value = source.get(key) + if value: + target[key] = value + file.seek(0) + json.dump(target, file, ensure_ascii=False, indent=2, sort_keys=True) + file.write("\n") + file.truncate() + +def splitJson(transifex_dir: str, json_public_dir: str, json_private_dir: str) -> None: + locales: list[str] = glob.glob("*.json", root_dir=transifex_dir) + locales = [x for x in locales if x != "en.json"] + for locale in locales: + print(f"Processing lang {locale}") + transifex_file: str = f"{transifex_dir}/{locale}" + public_file: str = f"{json_public_dir}/{locale}" + private_file: str = f"{json_private_dir}/{locale}" + + transifex_json: dict[str, str] = {} + with open(transifex_file, mode="r", encoding='utf-8') as file: + transifex_json = json.load(file) + if not isinstance(transifex_json, dict): + trimmed_path: str = transifex_file + path_parts = PurePath(transifex_file).parts + if len(path_parts) >= 2: + trimmed_path = f"{path_parts[-2]}/{path_parts[-1]}" + print(f"\tERROR: {trimmed_path} is malformed.") + continue + + updateJson(public_file, transifex_json) + updateJson(private_file, transifex_json) + +def mergeJson(transifex_dir: str, json_public_dir: str, json_private_dir: str) -> None: + transifex_en_file: str = f"{transifex_dir}/en.json" + public_en_file: str = f"{json_public_dir}/en.json" + private_en_file: str = f"{json_private_dir}/en.json" + + transifex_en_json: dict[str, str] = {} + public_en_json: dict[str, str] = {} + private_en_json: dict[str, str] = {} + + print("Merging source en.json file") + if os.path.exists(public_en_file): + with open(public_en_file, mode="r", encoding='utf-8') as file: + public_en_json = json.load(file) + if not isinstance(public_en_json, dict): + public_en_json = {} + + if os.path.exists(private_en_file): + with open(private_en_file, mode="r", encoding='utf-8') as file: + private_en_json = json.load(file) + if not isinstance(private_en_json, dict): + private_en_json = {} + + transifex_en_json = public_en_json | private_en_json + with open(transifex_en_file, mode="w", encoding='utf-8') as file: + json.dump(transifex_en_json, file, ensure_ascii=False, indent=2, sort_keys=True) + file.write("\n") + +def main() -> int: + script_path: str = os.path.dirname(os.path.realpath(__file__)) + transifex_dir: str = f"{script_path}/transifex" + json_public_dir: str = f"{script_path}/public/lang" + json_private_dir: str = f"{script_path}/private/lang" + + parser = argparse.ArgumentParser() + parser.add_argument("--mode", required=True, choices=["split", "merge"], help="SPLIT the translations files from Transifex into their public/private JSON counterpart. MERGE to merge the public and private en.json files into the 'transifex/en.json' file") + parser.add_argument("--transifex-dir", default=transifex_dir, nargs=1, help=f"directory of WebUI transifex file (default: '{transifex_dir}')") + parser.add_argument("--json-public-dir", default=json_public_dir, nargs=1, help=f"directory of WebUI public JSON translation files(default: '{json_public_dir}')") + parser.add_argument("--json-private-dir", default=json_private_dir, nargs=1, help=f"directory of WebUI private JSON translation files(default: '{json_private_dir}')") + + args = parser.parse_args() + + if not os.path.isdir(args.transifex_dir): + raise RuntimeError(f"'{args.transifex_dir}' isn't a directory") + + if not os.path.isdir(args.json_public_dir): + raise RuntimeError(f"'{args.json_public_dir}' isn't a directory") + + if not os.path.isdir(args.json_private_dir): + raise RuntimeError(f"'{args.json_private_dir}' isn't a directory") + + if args.mode == "merge": + mergeJson(transifex_dir, json_public_dir, json_private_dir) + else: + splitJson(transifex_dir, json_public_dir, json_private_dir) + + print("Done.") + + return 0 + +if __name__ == '__main__': + sys.exit(main()) diff --git a/src/webui/www/translations_qt2i18next.py b/src/webui/www/translations_qt2i18next.py new file mode 100755 index 000000000..c685bd8ab --- /dev/null +++ b/src/webui/www/translations_qt2i18next.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +# translations_qt2i18next.py - script for migrating translations from Qt's .ts files +# to i18next compatible JSON files. +# Copyright (C) 2024 sledgehammer999 +# +# License: Public domain. + +import argparse +import glob +import json +import os +import sys +import xml.etree.ElementTree as ET + +def getTsStrings(ts_file: str, key: str) -> list[str]: + tr_strings: list[str] = [] + tree = ET.parse(ts_file) + root = tree.getroot() + + for context in root.findall('context'): + for message in context.findall('message'): + source = message.find('source').text + if key != source: + continue + + translation = message.find('translation').text + if not translation: + continue + if translation not in tr_strings: + tr_strings.append(translation) + + return tr_strings + +def migrate2Json(ts_dir: str, json_dir: str, locale: str) -> None: + ts_file: str = f"{ts_dir}/webui_{locale}.ts" + js_file: str = f"{json_dir}/{locale}.json" + + print(f"Processing lang {locale}") + if not os.path.exists(ts_file): + print(f"\tERROR: {ts_file} doesn't exist.") + return + + with open(js_file, mode="r+", encoding='utf-8') as file: + translations = json.load(file) + if not isinstance(translations, dict): + print(f"\tERROR: {js_file} is malformed.") + return + + for key, value in translations.items(): + if not (isinstance(key, str) and isinstance(value, str) and not value): + continue + + ts_strings: list[str] = getTsStrings(ts_file, key) + ts_len: int = len(ts_strings) + if ts_len == 0: + print(f"\tWARNING: Translation for '{key}' not found.") + continue + + if ts_len > 1: + print(f"\tWARNING: Multiple translations for '{key}' found.") + continue + + translations[key] = ts_strings[0] + + file.seek(0) + json.dump(translations, file, ensure_ascii=False, indent=2, sort_keys=True) + file.write("\n") + file.truncate() + + print("\tFinished.") + +def main() -> int: + script_path: str = os.path.dirname(os.path.realpath(__file__)) + ts_dir: str = f"{script_path}/translations" + json_dir: str = f"{script_path}/public/lang" + + parser = argparse.ArgumentParser() + parser.add_argument("--ts-dir", default=ts_dir, nargs=1, help=f"directory of WebUI .ts translation files (default: '{ts_dir}')") + parser.add_argument("--json-dir", default=json_dir, nargs=1, help=f"directory of WebUI .json translation files(default: '{json_dir}')") + + args = parser.parse_args() + + if not os.path.isdir(args.ts_dir): + raise RuntimeError(f"'{args.ts_dir}' isn't a directory") + + if not os.path.isdir(args.json_dir): + raise RuntimeError(f"'{args.json_dir}' isn't a directory") + + locales: list[str] = glob.glob("*.json", root_dir=json_dir) + locales = [x.removesuffix(".json") for x in locales if x != "en.json"] + for locale in locales: + migrate2Json(ts_dir, json_dir, locale) + + print("Done.") + + return 0 + +if __name__ == '__main__': + sys.exit(main())