#!/usr/bin/env python3 """ Translate missing strings in Localizable.xcstrings to all target languages. Uses Google Translate (free, no API key needed). Usage: pip install deep-translator python3 scripts/translate_xcstrings.py """ import json import re import sys import time from pathlib import Path try: from deep_translator import GoogleTranslator except ImportError: print("Please install deep-translator: pip install deep-translator") sys.exit(1) XCSTRINGS_PATH = Path(__file__).parent.parent / "Feels" / "Localizable.xcstrings" TARGET_LANGUAGES = { "de": "german", "es": "spanish", "fr": "french", "ja": "japanese", "ko": "korean", "pt-BR": "portuguese" } LANG_DISPLAY = { "de": "German", "es": "Spanish", "fr": "French", "ja": "Japanese", "ko": "Korean", "pt-BR": "Brazilian Portuguese" } # Strings that should not be translated SKIP_PATTERNS = [ "", " ", " ", " ", "-", "—", "·", "•", ">", "§", "12", "17", "20", ] def get_english_value(string_data: dict, key: str) -> str | None: """Extract the English value for a string.""" localizations = string_data.get("localizations", {}) if "en" in localizations: en_loc = localizations["en"] if "stringUnit" in en_loc: return en_loc["stringUnit"].get("value") return key def needs_translation(string_data: dict, lang: str) -> bool: """Check if a string needs translation for a given language.""" localizations = string_data.get("localizations", {}) if lang not in localizations: return True lang_data = localizations[lang] if "stringUnit" not in lang_data: return True state = lang_data["stringUnit"].get("state", "") return state not in ["translated"] def should_skip(key: str, english_value: str) -> bool: """Check if a string should be skipped.""" if not key.strip() or not english_value.strip(): return True if key in SKIP_PATTERNS or english_value in SKIP_PATTERNS: return True stripped = english_value.strip() if stripped.startswith("%") and len(stripped) <= 10 and " " not in stripped: return True if "http://" in english_value or "https://" in english_value: return True return False def protect_format_specifiers(text: str) -> tuple[str, dict]: """Replace format specifiers with placeholders to protect them during translation.""" placeholders = {} counter = [0] def replace(match): placeholder = f"__FMT{counter[0]}__" placeholders[placeholder] = match.group(0) counter[0] += 1 return placeholder # Match format specifiers: %@, %lld, %1$@, %2$lld, %.0f%%, etc. pattern = r'%(\d+\$)?[-+0 #]*(\d+)?(\.\d+)?(lld|ld|d|@|f|s|%%)' protected = re.sub(pattern, replace, text) return protected, placeholders def restore_format_specifiers(text: str, placeholders: dict) -> str: """Restore format specifiers from placeholders.""" for placeholder, original in placeholders.items(): text = text.replace(placeholder, original) return text def translate_string(text: str, target_lang: str) -> str | None: """Translate a single string, preserving format specifiers.""" if not text.strip(): return text # Protect format specifiers protected, placeholders = protect_format_specifiers(text) try: translator = GoogleTranslator(source='en', target=target_lang) translated = translator.translate(protected) if translated: # Restore format specifiers result = restore_format_specifiers(translated, placeholders) return result return None except Exception as e: print(f" Translation error: {e}") return None def main(): print(f"Loading {XCSTRINGS_PATH}...") with open(XCSTRINGS_PATH, "r") as f: data = json.load(f) strings = data.get("strings", {}) print(f"Found {len(strings)} total strings") # Find strings needing translation needs_work = [] for key, string_data in strings.items(): english_value = get_english_value(string_data, key) if not english_value or should_skip(key, english_value): continue missing_langs = [ lang for lang in TARGET_LANGUAGES.keys() if needs_translation(string_data, lang) ] if missing_langs: needs_work.append((key, english_value, missing_langs)) print(f"Found {len(needs_work)} strings needing translation") total_translations = sum(len(ml) for _, _, ml in needs_work) print(f"Total translations needed: {total_translations}") if not needs_work: print("All strings are already translated!") return total_translated = 0 for i, (key, english, missing_langs) in enumerate(needs_work): print(f"\n[{i+1}/{len(needs_work)}] \"{english[:50]}{'...' if len(english) > 50 else ''}\"") if "localizations" not in strings[key]: strings[key]["localizations"] = {} for lang in missing_langs: google_lang = TARGET_LANGUAGES[lang] translated = translate_string(english, google_lang) if translated: strings[key]["localizations"][lang] = { "stringUnit": { "state": "translated", "value": translated } } total_translated += 1 print(f" {lang}: {translated[:60]}{'...' if len(translated) > 60 else ''}") else: print(f" {lang}: FAILED") # Rate limiting to avoid getting blocked time.sleep(0.3) # Save progress every 20 strings if (i + 1) % 20 == 0: print(f"\n Saving progress ({total_translated} translations)...") with open(XCSTRINGS_PATH, "w") as f: json.dump(data, f, indent=2, ensure_ascii=False) # Final save print(f"\nSaving {total_translated} translations...") with open(XCSTRINGS_PATH, "w") as f: json.dump(data, f, indent=2, ensure_ascii=False) print("\n=== Summary ===") for lang, name in LANG_DISPLAY.items(): count = sum( 1 for s in strings.values() if s.get("localizations", {}).get(lang, {}).get("stringUnit", {}).get("state") == "translated" ) total = len([k for k, v in strings.items() if not should_skip(k, get_english_value(v, k) or "")]) pct = (count / total * 100) if total > 0 else 0 print(f" {name}: {count}/{total} ({pct:.0f}%)") print("\nDone!") if __name__ == "__main__": main()