From 5c7c886454f71ae362f7bde7d21e05c35d78cba3 Mon Sep 17 00:00:00 2001 From: 7000pctAUTO Date: Mon, 2 Feb 2026 03:56:43 +0000 Subject: [PATCH] Add source files --- i18n_key_sync/sync.py | 273 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 273 insertions(+) create mode 100644 i18n_key_sync/sync.py diff --git a/i18n_key_sync/sync.py b/i18n_key_sync/sync.py new file mode 100644 index 0000000..7fea42e --- /dev/null +++ b/i18n_key_sync/sync.py @@ -0,0 +1,273 @@ +"""Synchronization of i18n keys with locale files.""" + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Dict, List, Optional + + +@dataclass +class SyncChange: + """Represents a single sync change.""" + + locale: str + key: str + old_value: Optional[Any] = None + new_value: Any = None + action: str = "add" + + +@dataclass +class SyncResult: + """Result of a sync operation.""" + + changes: List[SyncChange] = field(default_factory=list) + locales_updated: List[str] = field(default_factory=list) + + @property + def total_changes(self) -> int: + """Total number of changes.""" + return len(self.changes) + + +class Syncer: + """Synchronize i18n keys with locale files.""" + + def __init__(self, locale_parser): + """Initialize the syncer. + + Args: + locale_parser: LocaleParser instance. + + """ + self.parser = locale_parser + + def sync_keys( + self, + keys: set, + locale_dir: str, + target_locales: Optional[List[str]] = None, + placeholder_value: str = "TODO: Translate this string", + fill_from_locale: Optional[str] = None, + dry_run: bool = False, + ) -> List[SyncChange]: + """Sync missing keys to locale files. + + Args: + keys: Set of keys extracted from source code. + locale_dir: Directory containing locale files. + target_locales: Specific locales to sync (None for all). + placeholder_value: Value to use for new keys. + fill_from_locale: Locale to copy values from. + dry_run: If True, don't actually modify files. + + Returns: + List of SyncChange objects. + + """ + locale_dir_path = Path(locale_dir) + locale_keys = self.parser.parse_directory(locale_dir, target_locales) + changes = [] + + if fill_from_locale: + source_data = self._get_locale_data(locale_dir, fill_from_locale) + else: + source_data = None + + locales_to_update = set(locale_keys.keys()) if locale_keys else set() + if target_locales: + locales_to_update = set(target_locales) & locales_to_update + + for locale in locales_to_update: + existing_keys = locale_keys.get(locale, set()) + missing_keys = keys - existing_keys + + if missing_keys: + locale_file = locale_dir_path / f"{locale}.json" + if not locale_file.exists(): + locale_file = locale_dir_path / f"{locale}.yaml" + + if locale_file.exists(): + data = self.parser.parse_file(locale_file) + else: + data = {} + + for key in sorted(missing_keys): + if source_data: + new_value = self.parser.get_value(source_data, key) + if new_value is None: + new_value = placeholder_value + else: + new_value = placeholder_value + + old_value = self.parser.get_value(data, key) + + change = SyncChange( + locale=locale, + key=key, + old_value=old_value, + new_value=new_value, + action="add" if old_value is None else "update", + ) + changes.append(change) + + if not dry_run: + self.parser.add_key(data, key, new_value) + + if not dry_run: + format = "yaml" if locale_file.suffix in [".yaml", ".yml"] else "json" + self.parser.write_file(locale_file, data, format) + + return changes + + def _get_locale_data(self, locale_dir: str, locale: str) -> Dict[str, Any]: + """Get data for a specific locale. + + Args: + locale_dir: Directory containing locale files. + locale: Locale name. + + Returns: + Dictionary of locale data. + + """ + locale_dir_path = Path(locale_dir) + locale_file = locale_dir_path / f"{locale}.json" + if not locale_file.exists(): + locale_file = locale_dir_path / f"{locale}.yaml" + + if locale_file.exists(): + return self.parser.parse_file(locale_file) + return {} + + def sync_to_new_locale( + self, + keys: set, + locale_dir: str, + new_locale: str, + source_locale: Optional[str] = None, + placeholder_value: str = "TODO: Translate this string", + dry_run: bool = False, + ) -> List[SyncChange]: + """Create a new locale file with the given keys. + + Args: + keys: Set of keys to include. + locale_dir: Directory for locale files. + new_locale: Name of the new locale. + source_locale: Locale to copy values from. + placeholder_value: Default value for missing translations. + dry_run: If True, don't actually create files. + + Returns: + List of SyncChange objects. + + """ + changes = [] + data: Dict[str, Any] = {} + + if source_locale: + source_data = self._get_locale_data(locale_dir, source_locale) + else: + source_data = None + + for key in sorted(keys): + if source_data: + value = self.parser.get_value(source_data, key) + if value is None: + value = placeholder_value + else: + value = placeholder_value + + change = SyncChange( + locale=new_locale, + key=key, + old_value=None, + new_value=value, + action="add", + ) + changes.append(change) + + if not dry_run: + self.parser.add_key(data, key, value) + + if not dry_run and changes: + locale_file = Path(locale_dir) / f"{new_locale}.json" + self.parser.write_file(locale_file, data, "json") + + return changes + + def remove_unused_keys( + self, + locale_keys: Dict[str, set], + used_keys: set, + locale_dir: str, + dry_run: bool = False, + ) -> List[SyncChange]: + """Remove keys from locale files that are not used in code. + + Args: + locale_keys: Dictionary mapping locales to their key sets. + used_keys: Set of keys used in source code. + locale_dir: Directory containing locale files. + dry_run: If True, don't actually modify files. + + Returns: + List of SyncChange objects. + + """ + changes = [] + locale_dir_path = Path(locale_dir) + + for locale, keys in locale_keys.items(): + unused_keys = keys - used_keys + + if not unused_keys: + continue + + locale_file = locale_dir_path / f"{locale}.json" + if not locale_file.exists(): + locale_file = locale_dir_path / f"{locale}.yaml" + + if locale_file.exists(): + data = self.parser.parse_file(locale_file) + + for key in sorted(unused_keys): + old_value = self.parser.get_value(data, key) + + if old_value is not None: + change = SyncChange( + locale=locale, + key=key, + old_value=old_value, + new_value=None, + action="remove", + ) + changes.append(change) + + if not dry_run: + self._remove_key_from_data(data, key) + + if not dry_run and changes: + format = "yaml" if locale_file.suffix in [".yaml", ".yml"] else "json" + self.parser.write_file(locale_file, data, format) + + return changes + + def _remove_key_from_data(self, data: Dict[str, Any], key: str) -> None: + """Remove a key from nested data structure. + + Args: + data: Nested dictionary. + key: Key in dot notation. + + """ + parts = key.split(".") + current = data + for part in parts[:-1]: + if part in current: + current = current[part] + else: + return + + if parts[-1] in current: + del current[parts[-1]]