This commit is contained in:
222
i18n_key_sync/locale.py
Normal file
222
i18n_key_sync/locale.py
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user