diff --git a/i18n_key_sync/locale.py b/i18n_key_sync/locale.py new file mode 100644 index 0000000..2927539 --- /dev/null +++ b/i18n_key_sync/locale.py @@ -0,0 +1,222 @@ +"""Locale file parsing for JSON and YAML format.""" + +import json +from pathlib import Path +from typing import Any, Dict, List, Optional, Set + + +class LocaleParser: + """Parse and manage locale files in JSON and YAML formats.""" + + def __init__(self): + """Initialize the locale parser.""" + self._cache: Dict[str, Dict[str, Any]] = {} + + def parse_file(self, file_path: Path | str) -> Dict[str, Any]: + """Parse a single locale file. + + Args: + file_path: Path to the locale file. + + Returns: + Dictionary of locale keys and values. + + Raises: + ValueError: If the file format is not supported. + FileNotFoundError: If the file doesn't exist. + + """ + path = Path(file_path) + path_str = str(path) + if path_str in self._cache: + return self._cache[path_str] + + content = path.read_text(encoding="utf-8") + suffix = path.suffix.lower() + + if suffix == ".json": + data = json.loads(content) + elif suffix in [".yaml", ".yml"]: + import yaml + + data = yaml.safe_load(content) + else: + raise ValueError(f"Unsupported locale file format: {suffix}") + + if not isinstance(data, dict): + data = {} + + self._cache[str(file_path)] = data + return data + + def get_all_keys(self, data: Dict[str, Any], prefix: str = "") -> Set[str]: + """Get all keys from nested dictionary, including nested keys as dot notation. + + Args: + data: Nested dictionary of locale data. + prefix: Prefix for nested keys. + + Returns: + Set of all keys in dot notation. + + """ + keys = set() + for key, value in data.items(): + full_key = f"{prefix}.{key}" if prefix else key + if isinstance(value, dict): + keys.update(self.get_all_keys(value, full_key)) + else: + keys.add(full_key) + return keys + + def flatten_keys(self, data: Dict[str, Any], prefix: str = "") -> Dict[str, Any]: + """Flatten nested dictionary to dot notation. + + Args: + data: Nested dictionary of locale data. + prefix: Prefix for nested keys. + + Returns: + Flattened dictionary with dot notation keys. + + """ + flattened = {} + for key, value in data.items(): + full_key = f"{prefix}.{key}" if prefix else key + if isinstance(value, dict): + flattened.update(self.flatten_keys(value, full_key)) + else: + flattened[full_key] = value + return flattened + + def unflatten_keys(self, data: Dict[str, Any]) -> Dict[str, Any]: + """Unflatten dot notation keys to nested dictionary. + + Args: + data: Flattened dictionary with dot notation keys. + + Returns: + Nested dictionary. + + """ + nested: Dict[str, Any] = {} + for key, value in data.items(): + parts = key.split(".") + current = nested + for part in parts[:-1]: + if part not in current: + current[part] = {} + current = current[part] + current[parts[-1]] = value + return nested + + def parse_directory( + self, + dir_path: str, + locales: Optional[List[str]] = None, + ) -> Dict[str, Set[str]]: + """Parse all locale files in a directory. + + Args: + dir_path: Path to the locale directory. + locales: Optional list of locale names to include. + + Returns: + Dictionary mapping locale names to their key sets. + + """ + locale_dir = Path(dir_path) + results: Dict[str, Set[str]] = {} + + if not locale_dir.exists(): + return results + + for locale_file in locale_dir.iterdir(): + if not locale_file.is_file(): + continue + + locale_name = locale_file.stem + if locales and locale_name not in locales: + continue + + try: + data = self.parse_file(locale_file) + keys = self.get_all_keys(data) + results[locale_name] = keys + except (ValueError, json.JSONDecodeError) as e: + if locales: + raise ValueError(f"Error parsing {locale_file}: {e}") + + return results + + def write_file( + self, + file_path: Path, + data: Dict[str, Any], + format: str = "json", + ) -> None: + """Write locale data to a file. + + Args: + file_path: Path to the output file. + data: Dictionary of locale data. + format: Output format (json or yaml). + + """ + content: str + if format == "json": + content = json.dumps(data, indent=2, ensure_ascii=False) + elif format in ["yaml", "yml"]: + import yaml + + content = yaml.dump(data, default_flow_style=False, allow_unicode=True) + else: + raise ValueError(f"Unsupported format: {format}") + + file_path.write_text(content, encoding="utf-8") + + def add_key( + self, + data: Dict[str, Any], + key: str, + value: Any, + ) -> Dict[str, Any]: + """Add a key to locale data, creating nested structure if needed. + + Args: + data: Existing locale data. + key: Key in dot notation. + value: Value to set. + + Returns: + Updated locale data. + + """ + parts = key.split(".") + current = data + for part in parts[:-1]: + if part not in current: + current[part] = {} + current = current[part] + current[parts[-1]] = value + return data + + def get_value(self, data: Dict[str, Any], key: str) -> Optional[Any]: + """Get a value from locale data using dot notation. + + Args: + data: Locale data dictionary. + key: Key in dot notation. + + Returns: + The value if found, None otherwise. + + """ + parts = key.split(".") + current = data + for part in parts: + if isinstance(current, dict) and part in current: + current = current[part] + else: + return None + return current