Files
pvemanager/backend/app/i18n.py
markmorado c09c0c5b63 refactor(i18n): move translations to JSON locale files
- Extract 1149 translation keys from i18n.py into locales/ru.json and locales/en.json
- Rewrite i18n.py as a lightweight JSON loader (5287 → 128 lines)
- Add I18nService.reload() for runtime hot-reload without restart
- Add I18nService.available_languages() to list loaded language codes
- Public API unchanged: t(), get(), get_all(), add_translation()
- New languages require only a new *.json file in locales/ — no code changes

Bump version: 1.1.6 → 1.1.7
2026-03-16 04:08:28 +01:00

129 lines
4.2 KiB
Python

"""
Internationalization (i18n) service
Loads translations from locales/*.json files at runtime.
Each file name must match the language code (e.g. ru.json → "ru").
Adding a new language requires only dropping a new JSON file into the
locales/ directory — no code changes needed.
"""
import json
import os
from pathlib import Path
from typing import Dict, Optional
from enum import Enum
# Directory that contains *.json locale files
LOCALES_DIR = Path(__file__).parent / "locales"
class Language(str, Enum):
"""Supported languages"""
RU = "ru" # Russian
EN = "en" # English (US)
class I18nService:
"""Service for handling translations loaded from JSON locale files."""
# {lang_code: {key: translated_string}}
_translations: Dict[str, Dict[str, str]] = {}
_loaded: bool = False
# ---------------------------------------------------------------------------
# Internal helpers
# ---------------------------------------------------------------------------
@classmethod
def _load(cls) -> None:
"""Load all *.json files from LOCALES_DIR into _translations."""
cls._translations = {}
if not LOCALES_DIR.is_dir():
return
for path in sorted(LOCALES_DIR.glob("*.json")):
lang = path.stem # "ru", "en", ...
with open(path, encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict):
cls._translations[lang] = {str(k): str(v) for k, v in data.items()}
cls._loaded = True
@classmethod
def _ensure_loaded(cls) -> None:
if not cls._loaded:
cls._load()
@classmethod
def reload(cls) -> None:
"""Force re-read of all locale files (useful after editing JSON without restart)."""
cls._loaded = False
cls._load()
# ---------------------------------------------------------------------------
# Public API (same signatures as before — no call-sites need to change)
# ---------------------------------------------------------------------------
@classmethod
def get(cls, key: str, lang: str = "ru", **kwargs) -> str:
"""
Return the translated string for *key* in *lang*.
Falls back to Russian, then to the raw key if nothing is found.
Supports str.format(**kwargs) placeholders.
"""
cls._ensure_loaded()
lang_dict = cls._translations.get(lang) or cls._translations.get("ru") or {}
translation = lang_dict.get(key)
if translation is None:
# Last resort: try any available language
for fallback in cls._translations.values():
if key in fallback:
translation = fallback[key]
break
else:
translation = key
if kwargs:
try:
translation = translation.format(**kwargs)
except KeyError:
pass
return translation
@classmethod
def get_all(cls, lang: str = "ru") -> Dict[str, str]:
"""Return all translations for *lang* as a flat {key: value} dict."""
cls._ensure_loaded()
return dict(cls._translations.get(lang) or cls._translations.get("ru") or {})
@classmethod
def add_translation(cls, key: str, ru: str, en: str) -> None:
"""
Register an in-memory translation (does NOT write to disk).
Useful for dynamically generated keys at runtime.
"""
cls._ensure_loaded()
for lang, value in (("ru", ru), ("en", en)):
if lang not in cls._translations:
cls._translations[lang] = {}
cls._translations[lang][key] = value
@classmethod
def available_languages(cls) -> list:
"""Return list of language codes that have locale files."""
cls._ensure_loaded()
return sorted(cls._translations.keys())
# ---------------------------------------------------------------------------
# Convenience shortcut
# ---------------------------------------------------------------------------
def t(key: str, lang: str = "ru", **kwargs) -> str:
"""Shortcut for I18nService.get()"""
return I18nService.get(key, lang, **kwargs)