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