Source code for i18n

"""
Internationalization (i18n) module for the RegressionLab application.

This module provides language support for the application, allowing all UI messages,
logs, and errors to be displayed in different languages based on the LANGUAGE
environment variable.

Supported languages:

    - 'es' or 'espaĂąol': Spanish (default)
    - 'en' or 'english': English
    - 'de' or 'german': German

Usage::

    from i18n import t

    # In UI code
    messagebox.showerror(t('error.title'), t('error.fitting_failed'))

    # In logger code
    logger.info(t('log.application_starting'))
"""

# Standard library
import json
import os
from pathlib import Path
from typing import Any, Dict, Optional

from config import (
    DEFAULT_LANGUAGE,
    LANGUAGE_ALIASES,
    SUPPORTED_LANGUAGE_CODES,
)

# Current loaded language
_current_language: str = DEFAULT_LANGUAGE
_translations: Dict[str, Any] = {}
# Cache loaded files per language to avoid re-reading when switching language
_translation_cache: Dict[str, Dict[str, Any]] = {}
# Cache resolved keys for current language to avoid repeated nested lookups
_key_cache: Dict[str, str] = {}

__all__ = ["initialize_i18n", "t", "DEFAULT_LANGUAGE"]


def _normalize_language(language: str) -> str:
    """
    Normalize language code to standard format.

    Args:
        language: Language name or code

    Returns:
        Normalized language code (one of SUPPORTED_LANGUAGE_CODES).
    """
    lang = language.lower()
    if lang in SUPPORTED_LANGUAGE_CODES:
        return lang
    return LANGUAGE_ALIASES.get(lang, DEFAULT_LANGUAGE)


def _get_language_from_env() -> str:
    """
    Get the language from the LANGUAGE environment variable.

    Returns:
        Language code ('es', 'en', or 'de')
    """
    lang = os.getenv("LANGUAGE", DEFAULT_LANGUAGE).lower()

    return _normalize_language(lang)


def _load_translations(language: str) -> Dict[str, Any]:
    """
    Load translation file for the specified language.
    Uses an in-memory cache so each language file is read at most once.

    Args:
        language: Language code ('es', 'en', or 'de')

    Returns:
        Dictionary with translations

    Raises:
        FileNotFoundError: If translation file doesn't exist
    """
    if language in _translation_cache:
        return _translation_cache[language]

    locales_dir = Path(__file__).parent / "locales"
    translation_file = locales_dir / f"{language}.json"

    if not translation_file.exists():
        raise FileNotFoundError(f"Translation file not found: {translation_file}")

    with open(translation_file, "r", encoding="utf-8") as f:
        data = json.load(f)

    _translation_cache[language] = data
    return data


[docs] def initialize_i18n(language: Optional[str] = None) -> None: """ Initialize the internationalization system. This function should be called once at application startup. If no language is specified, it reads from the LANGUAGE environment variable. Args: language: Optional language code ('es', 'en', or 'de'). If None, reads from env var. """ global _current_language, _translations, _key_cache if language is None: language = _get_language_from_env() else: language = _normalize_language(language) _current_language = language _key_cache.clear() try: _translations = _load_translations(language) except FileNotFoundError as e: # Fallback to default language if specified language not found print(f"Warning: {e}") print(f"Falling back to default language: {DEFAULT_LANGUAGE}") _current_language = DEFAULT_LANGUAGE _translations = _load_translations(DEFAULT_LANGUAGE)
[docs] def t(key: str, **kwargs: Any) -> str: """ Translate a key to the current language. Retrieves the translation for a given key in the current language. The key uses dot notation to navigate nested dictionaries. Args: key: Translation key in dot notation (e.g. ``'menu.welcome'``). **kwargs: Optional format parameters for string interpolation. Returns: Translated string, or the key itself if translation not found. Examples: >>> t('menu.welcome') 'Welcome, scientist. What would you like to do?' >>> t('error.fitting_failed_details', error='Invalid data') 'The fitter was unable to fit the data.\\n\\nDetails: Invalid data\\n\\nPlease try another equation or verify the data.' """ # Ensure translations are loaded if not _translations: initialize_i18n() # Use cached template string when available if key in _key_cache: template = _key_cache[key] if kwargs: try: return template.format(**kwargs) except (KeyError, ValueError): return template return template # Navigate nested dictionaries using dot notation keys = key.split(".") value: Any = _translations for k in keys: if isinstance(value, dict) and k in value: value = value[k] else: return key if isinstance(value, dict): return key template = str(value) _key_cache[key] = template if kwargs: try: return template.format(**kwargs) except (KeyError, ValueError): return template return template