immich/mobile/scripts/check_i18n_keys.py
2025-04-14 15:17:20 -05:00

372 lines
13 KiB
Python

#!/usr/bin/env python3
import json
import subprocess
import os
import re
import glob
from collections import defaultdict
# Mapping from mobile locale codes to root locale codes
LOCALE_MAPPING = {
"en-US": "en",
"fr-FR": "fr",
"fr-CA": "fr",
"de-DE": "de",
"es-ES": "es",
"es-PE": "es",
"it-IT": "it",
"ja-JP": "ja",
"ko-KR": "ko",
"nl-NL": "nl",
"pl-PL": "pl",
"pt-PT": "pt",
"pt-BR": "pt_BR",
"ru-RU": "ru",
"sv-SE": "sv",
"sv-FI": "sv",
"tr-TR": "tr",
"uk-UA": "uk",
"zh-CN": "zh_SIMPLIFIED",
"zh-TW": "zh_Hant",
"id-ID": "id",
"th-TH": "th",
"el-GR": "el",
"ro-RO": "ro",
"hu-HU": "hu",
"sk-SK": "sk",
"lv-LV": "lv",
"vi-VN": "vi",
"lt-LT": "lt",
"mn-MN": "mn",
"sr-Latn": "sr_Latn",
"da-DK": "da",
"nb-NO": "nb_NO",
"ar-JO": "ar",
"ca": "ca",
"cs-CZ": "cs",
"gl-ES": "gl",
"sr-Cyrl": "sr_Cyrl",
"sl-SI": "sl",
"he-IL": "he",
"fi-FI": "fi",
"es-MX": "es",
"hi-IN": "hi",
# Add more mappings as needed
}
# Regex pattern to find translation keys in Dart code
TRANSLATION_KEY_PATTERN = r'["\']([\w_\.]+)["\']\.tr\(\)'
def find_mobile_i18n_files():
mobile_i18n_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "i18n")
if not os.path.exists(mobile_i18n_dir):
print(f"Mobile i18n directory not found: {mobile_i18n_dir}")
return []
return [f for f in os.listdir(mobile_i18n_dir) if f.endswith('.json')]
def find_root_i18n_dir():
current_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
root_dir = os.path.dirname(current_dir)
i18n_dir = os.path.join(root_dir, "i18n")
if not os.path.exists(i18n_dir):
print(f"Root i18n directory not found: {i18n_dir}")
return None
return i18n_dir
def shorten_key(key):
"""Try to shorten translation keys by removing common prefixes"""
# Define prefixes to remove
prefixes = [
"action_common_",
"album_info_card_",
"album_viewer_",
"asset_list_",
"backup_controller_page_",
"cache_settings_",
"control_bottom_app_bar_",
"search_page_",
"theme_setting_"
]
# Check if key starts with any of the prefixes
for prefix in prefixes:
if key.startswith(prefix):
return key[len(prefix):]
return key
def find_all_dart_files():
"""Find all Dart files in the mobile app"""
mobile_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
lib_dir = os.path.join(mobile_dir, "lib")
if not os.path.exists(lib_dir):
print(f"Mobile lib directory not found: {lib_dir}")
return []
return glob.glob(os.path.join(lib_dir, "**", "*.dart"), recursive=True)
def extract_translation_keys_from_dart_files():
"""Extract all translation keys used in the Dart code"""
dart_files = find_all_dart_files()
translation_keys = set()
for dart_file in dart_files:
with open(dart_file, 'r') as f:
content = f.read()
matches = re.findall(TRANSLATION_KEY_PATTERN, content)
translation_keys.update(matches)
return translation_keys
def check_for_unused_keys():
"""Check for unused translation keys in the mobile app and returns a list of them."""
mobile_i18n_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "i18n")
en_us_path = os.path.join(mobile_i18n_dir, "en-US.json")
with open(en_us_path, 'r') as f:
data = json.load(f)
keys_to_delete = []
for k in data.keys():
sp = subprocess.run(['sh', '-c', f'grep -q -r --include="*.dart" "{k}" {os.path.dirname(mobile_i18n_dir)}/lib'], capture_output=True)
if sp.returncode != 0:
print("Not found in source code, key:", k)
keys_to_delete.append(k)
return keys_to_delete, data
def migrate_translations():
"""Migrate translations from mobile to root i18n directory WITHOUT modifying source files"""
mobile_i18n_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "i18n")
root_i18n_dir = find_root_i18n_dir()
if not root_i18n_dir:
print("Could not find root i18n directory")
return {}
# Get list of mobile translation files
mobile_files = find_mobile_i18n_files()
if not mobile_files:
print("No mobile translation files found")
return {}
# Dictionary to store key mappings (old_key -> new_key)
key_mapping = {}
# First, find all unused keys (but don't delete them)
unused_keys, _ = check_for_unused_keys()
print(f"Found {len(unused_keys)} unused keys (will be migrated but marked)")
# Process each mobile translation file
for mobile_file in mobile_files:
mobile_locale = mobile_file.split('.')[0] # e.g., "en-US"
if mobile_locale not in LOCALE_MAPPING:
print(f"No mapping found for locale {mobile_locale}, skipping")
continue
root_locale = LOCALE_MAPPING[mobile_locale]
root_file = f"{root_locale}.json"
root_file_path = os.path.join(root_i18n_dir, root_file)
# Load mobile translations
with open(os.path.join(mobile_i18n_dir, mobile_file), 'r') as f:
mobile_translations = json.load(f)
# Load root translations if they exist
if os.path.exists(root_file_path):
with open(root_file_path, 'r') as f:
root_translations = json.load(f)
else:
root_translations = {}
# Keys to migrate (including all keys)
keys_to_migrate = {}
# Migrate all keys from mobile to root
for k, v in mobile_translations.items():
if k not in root_translations: # Only migrate if not already in root
# Try to shorten the key
new_key = shorten_key(k)
# If shortened key already exists in root, use the original key
if new_key != k and new_key in root_translations:
keys_to_migrate[k] = v
key_mapping[k] = k # Map to itself (no change)
else:
keys_to_migrate[new_key] = v
key_mapping[k] = new_key # Map old key to new key
else:
# If the key already exists in root, just record the mapping
key_mapping[k] = k
# If there are keys to migrate, update the root file
if keys_to_migrate:
print(f"Migrating {len(keys_to_migrate)} keys from {mobile_file} to {root_file}")
# Update the root translations with the new keys
root_translations.update(keys_to_migrate)
# Write the updated translations back to the root file
with open(root_file_path, 'w') as f:
json.dump(root_translations, f, indent=2, ensure_ascii=False, sort_keys=True)
else:
print(f"No new keys to migrate from {mobile_file} to {root_file}")
# IMPORTANT: We do NOT modify the source files at all, just return the key mappings
return key_mapping
def generate_migration_report(key_mapping, unused_keys):
"""Generate a detailed report of the migration"""
report_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "translation_migration_report.md")
with open(report_path, 'w') as f:
f.write("# Translation Migration Report\n\n")
f.write("## Summary\n")
f.write(f"- Total keys migrated: {len(key_mapping)}\n")
f.write(f"- Keys shortened: {sum(1 for k, v in key_mapping.items() if k != v)}\n")
f.write(f"- Unused keys identified: {len(unused_keys)}\n\n")
f.write("## Key Mappings\n")
f.write("The following keys were migrated to the root i18n directory:\n\n")
f.write("| Original Key | New Key | Shortened |\n")
f.write("|-------------|---------|----------|\n")
for old_key, new_key in sorted(key_mapping.items()):
shortened = "Yes" if old_key != new_key else "No"
f.write(f"| `{old_key}` | `{new_key}` | {shortened} |\n")
f.write("\n\n## Unused Keys\n")
f.write("The following keys were identified as unused in the codebase:\n\n")
f.write("```\n")
for key in sorted(unused_keys):
f.write(f"{key}\n")
f.write("```\n\n")
f.write("## Next Steps\n")
f.write("1. Review the key mappings to ensure they are correct\n")
f.write("2. Test the application thoroughly to ensure all translations work properly\n")
f.write("3. Consider removing unused keys from the source files if they are no longer needed\n")
print(f"Migration report generated at {report_path}")
return report_path
def update_dart_code_references(key_mapping):
"""Update translation key references in Dart files"""
if not key_mapping:
print("No key mappings provided, skipping Dart code update")
return []
# Get all Dart files
dart_files = find_all_dart_files()
updated_files = []
# Process each Dart file
for dart_file in dart_files:
with open(dart_file, 'r') as f:
content = f.read()
updated_content = content
file_updated = False
file_updates = []
# Replace each old key with the new key
for old_key, new_key in key_mapping.items():
if old_key != new_key: # Only update if the key has changed
# Pattern to match the key used in tr() calls
pattern = f'["\']({re.escape(old_key)})["\']\.tr\(\)'
replacement = f'"{new_key}".tr()'
# Perform the replacement
new_content = re.sub(pattern, replacement, updated_content)
if new_content != updated_content:
file_updates.append(f"{old_key} -> {new_key}")
updated_content = new_content
file_updated = True
# Write the updated content back to the file if changes were made
if file_updated:
with open(dart_file, 'w') as f:
f.write(updated_content)
updated_files.append((dart_file, file_updates))
print(f"Updated {os.path.basename(dart_file)} with new translation keys: {', '.join(file_updates)}")
print(f"Updated {len(updated_files)} Dart files with new translation keys")
return updated_files
def check_for_duplicate_keys():
"""Check for duplicate keys between mobile and root i18n"""
mobile_i18n_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "assets", "i18n")
root_i18n_dir = find_root_i18n_dir()
if not root_i18n_dir:
print("Could not find root i18n directory")
return []
# Load English translations as reference
mobile_en_path = os.path.join(mobile_i18n_dir, "en-US.json")
root_en_path = os.path.join(root_i18n_dir, "en.json")
if not os.path.exists(mobile_en_path) or not os.path.exists(root_en_path):
print("Missing English translation files")
return []
with open(mobile_en_path, 'r') as f:
mobile_en = json.load(f)
with open(root_en_path, 'r') as f:
root_en = json.load(f)
# Find duplicate keys
duplicates = set(mobile_en.keys()) & set(root_en.keys())
if duplicates:
print(f"Found {len(duplicates)} duplicate keys between mobile and root i18n:")
for key in sorted(duplicates):
mobile_val = mobile_en[key]
root_val = root_en[key]
if mobile_val == root_val:
status = "SAME"
else:
status = "DIFFERENT"
print(f" {key}: {status}")
print(f" Mobile: {mobile_val}")
print(f" Root: {root_val}")
else:
print("No duplicate keys found between mobile and root i18n")
return duplicates
def main():
print("Checking for duplicate keys between mobile and root i18n...")
duplicates = check_for_duplicate_keys()
print("\nExtracting translation keys used in Dart code...")
translation_keys = extract_translation_keys_from_dart_files()
print(f"Found {len(translation_keys)} translation keys in use")
# Get unused keys (but don't delete them)
unused_keys, _ = check_for_unused_keys()
print("\nMigrating translations from mobile to root i18n...")
key_mapping = migrate_translations()
print(f"Created mappings for {len(key_mapping)} keys")
print("\nUpdating translation references in Dart code...")
updated_files = update_dart_code_references(key_mapping)
# Generate a detailed report
report_path = generate_migration_report(key_mapping, unused_keys)
print("\nMigration completed! See the report for details:", report_path)
print("\nNote: Source translation files were NOT modified or deleted.")
if __name__ == '__main__':
main()