From 1158068a311ac6deaa00a5f760f202126d6491ba Mon Sep 17 00:00:00 2001 From: Ahmed Nagi Date: Sat, 26 Apr 2025 13:13:35 +0200 Subject: [PATCH] Add script to auto-translate missing keys in translation files with colored output and parallel processing --- fill_missing_translations.py | 108 +++++++++++++++++++++++++++++++++++ locales/ar.json | 48 +++++++++++++++- 2 files changed, 155 insertions(+), 1 deletion(-) create mode 100644 fill_missing_translations.py diff --git a/fill_missing_translations.py b/fill_missing_translations.py new file mode 100644 index 0000000..b3418c1 --- /dev/null +++ b/fill_missing_translations.py @@ -0,0 +1,108 @@ +""" +Compares two JSON translation files in /locales (e.g., en.json and ar.json). +Finds keys missing in the target file, translates their values using Google Translate (web scraping), +and inserts the translations. Runs in parallel for speed and creates a backup of the target file. +""" +import json +import requests +import sys +from pathlib import Path +import urllib.parse +import re +from concurrent.futures import ThreadPoolExecutor, as_completed +from colorama import init, Fore, Style + +init(autoreset=True) + +# Recursively get all keys in the JSON as dot-separated paths +def get_keys(d, prefix=''): + keys = set() + for k, v in d.items(): + full_key = f"{prefix}.{k}" if prefix else k + if isinstance(v, dict): + keys |= get_keys(v, full_key) + else: + keys.add(full_key) + return keys + +# Get value from nested dict by dot-separated path +def get_by_path(d, path): + for p in path.split('.'): + d = d[p] + return d + +# Set value in nested dict by dot-separated path +def set_by_path(d, path, value): + parts = path.split('.') + for p in parts[:-1]: + if p not in d: + d[p] = {} + d = d[p] + d[parts[-1]] = value + +# Translate text using Google Translate web scraping (mobile version) +def translate(text, source, target): + url = f"https://translate.google.com/m?sl={source}&tl={target}&q={requests.utils.quote(text)}" + headers = {"User-Agent": "Mozilla/5.0"} + response = requests.get(url, headers=headers) + if response.status_code == 200: + m = re.search(r'class=\"result-container\">(.*?)<', response.text) + if m: + return m.group(1) + else: + print(Fore.RED + f"Translation not found for: {text}") + return text + else: + print(Fore.RED + f"Request failed for: {text}") + return text + +# Main logic: compare keys, translate missing, and update file +def main(en_filename, other_filename): + # Always use the /locales directory + en_path = Path("locales") / en_filename + other_path = Path("locales") / other_filename + # Infer language code from filename (before .json) + en_lang = Path(en_filename).stem + other_lang = Path(other_filename).stem + + with open(en_path, encoding='utf-8') as f: + en = json.load(f) + with open(other_path, encoding='utf-8') as f: + other = json.load(f) + + en_keys = get_keys(en) + other_keys = get_keys(other) + + missing = en_keys - other_keys + print(Fore.YELLOW + f"Missing keys: {len(missing)}") + + # Parallel translation using ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=8) as executor: + future_to_key = { + executor.submit(translate, get_by_path(en, key), en_lang, other_lang): key + for key in missing + } + for future in as_completed(future_to_key): + key = future_to_key[future] + value = get_by_path(en, key) + try: + translated = future.result() + print(Fore.CYAN + f"Translated [{key}]: '{value}' -> " + Fore.MAGENTA + f"'{translated}'") + except Exception as exc: + print(Fore.RED + f"Error translating {key}: {exc}") + translated = value + set_by_path(other, key, translated) + + # Save the updated file and create a backup + backup_path = other_path.with_suffix('.bak.json') + other_path.rename(backup_path) + with open(other_path, 'w', encoding='utf-8') as f: + json.dump(other, f, ensure_ascii=False, indent=4) + print(Fore.GREEN + f"File updated. Backup saved to {backup_path}") + +if __name__ == "__main__": + # Example: python3 fill_missing_translations.py en.json ar.json + if len(sys.argv) != 3: + print("Usage: python3 fill_missing_translations.py en.json ar.json") + sys.exit(1) + main(sys.argv[1], sys.argv[2]) \ No newline at end of file diff --git a/locales/ar.json b/locales/ar.json index e0b02b4..b5b2190 100644 --- a/locales/ar.json +++ b/locales/ar.json @@ -32,7 +32,10 @@ "exiting": "جاري الخروج ……", "bypass_version_check": "تجاوز فحص إصدار Cursor", "check_user_authorized": "فحص صلاحية المستخدم", - "bypass_token_limit": "تجاوز حد الرمز المميز (Token)" + "bypass_token_limit": "تجاوز حد الرمز المميز (Token)", + "restore_machine_id": "استعادة معرف الجهاز من النسخ الاحتياطي", + "lang_invalid_choice": "اختيار غير صالح. الرجاء إدخال أحد الخيارات التالية: ({lang_choices})", + "language_config_saved": "تم حفظ تكوين اللغة بنجاح" }, "languages": { "en": "الإنجليزية", @@ -765,5 +768,48 @@ "connection_error": "خطأ في الاتصال بخادم التحديث", "unexpected_error": "خطأ غير متوقع أثناء تحديث الرمز: {error}", "extraction_error": "خطأ في استخراج الرمز: {error}" + }, + "restore": { + "update_failed": "Failed to update storage file: {error}", + "read_backup_failed": "Failed to read backup file: {error}", + "please_enter_number": "Please enter a valid number", + "no_backups_found": "No backup files found", + "title": "Restore Machine ID from Backup", + "sqlite_updated": "SQLite database updated successfully", + "permission_denied": "Permission denied. Please try running as administrator", + "sqlite_not_found": "SQLite database not found", + "backup_creation_failed": "فشل في إنشاء نسخة احتياطية: {error}", + "windows_machine_guid_updated": "تم تحديث GUID Machine Windows بنجاح", + "select_backup": "حدد النسخ الاحتياطي لاستعادة", + "sqm_client_key_not_found": "لم يتم العثور على مفتاح تسجيل SQMClient", + "machine_id_backup_created": "تم إنشاء نسخة احتياطية من ملف الجهاز", + "system_ids_update_failed": "فشل في تحديث معرفات النظام: {error}", + "current_backup_created": "تم إنشاء نسخة احتياطية من ملف التخزين الحالي", + "update_windows_machine_guid_failed": "فشل في تحديث GUID MAVEN", + "updating_pair": "تحديث زوج القيمة الرئيسية", + "press_enter": "اضغط على Enter للمتابعة", + "missing_id": "معرف مفقود: {id}", + "current_file_not_found": "لم يتم العثور على ملف التخزين الحالي", + "sqlite_update_failed": "فشل في تحديث قاعدة بيانات SQLite: {error}", + "success": "تم استعادة معرف الجهاز بنجاح", + "process_error": "استعادة خطأ العملية: {error}", + "update_macos_system_ids_failed": "فشل في تحديث معرفات نظام MacOS: {error}", + "machine_id_update_failed": "فشل في تحديث ملف الماكينة: {error}", + "available_backups": "ملفات النسخ الاحتياطي المتاحة", + "windows_machine_id_updated": "معرف جهاز Windows تم تحديثه بنجاح", + "updating_sqlite": "تحديث قاعدة بيانات SQLite", + "invalid_selection": "اختيار غير صالح", + "update_windows_system_ids_failed": "فشل في تحديث معرفات نظام Windows: {error}", + "macos_platform_uuid_updated": "تم تحديث منصة MacOS UUID بنجاح", + "ids_to_restore": "معرفات الماكينة لاستعادة", + "operation_cancelled": "تم إلغاء العملية", + "machine_id_updated": "تم تحديث ملف الجهاز بنجاح", + "update_windows_machine_id_failed": "فشل في تحديث معرف جهاز Windows: {error}", + "storage_updated": "تم تحديث ملف التخزين بنجاح", + "failed_to_execute_plutil_command": "فشل تنفيذ أمر بلوتيل", + "updating_system_ids": "تحديث معرفات النظام", + "starting": "بدء عملية استعادة معرف الجهاز", + "confirm": "هل أنت متأكد من أنك تريد استعادة هذه المعرفات؟", + "to_cancel": "للإلغاء" } } \ No newline at end of file