""" Internationalization (i18n) support for TTSFM Web Application This module provides multi-language support for the Flask web application, including language detection, translation management, and template functions. """ import json import os from typing import Dict, Any, Optional from flask import request, session, current_app class LanguageManager: """Manages language detection, translation loading, and text translation.""" def __init__(self, app=None, translations_dir: str = "translations"): """ Initialize the LanguageManager. Args: app: Flask application instance translations_dir: Directory containing translation files """ self.translations_dir = translations_dir self.translations: Dict[str, Dict[str, Any]] = {} self.supported_languages = ['en', 'zh'] self.default_language = 'en' if app is not None: self.init_app(app) def init_app(self, app): """Initialize the Flask application with i18n support.""" app.config.setdefault('LANGUAGES', self.supported_languages) app.config.setdefault('DEFAULT_LANGUAGE', self.default_language) # Load translations self.load_translations() # Register template functions app.jinja_env.globals['_'] = self.translate app.jinja_env.globals['get_locale'] = self.get_locale app.jinja_env.globals['get_supported_languages'] = self.get_supported_languages # Store reference to this instance app.language_manager = self def load_translations(self): """Load all translation files from the translations directory.""" translations_path = os.path.join( os.path.dirname(__file__), self.translations_dir ) if not os.path.exists(translations_path): print(f"Warning: Translations directory not found: {translations_path}") return for lang_code in self.supported_languages: file_path = os.path.join(translations_path, f"{lang_code}.json") if os.path.exists(file_path): try: with open(file_path, 'r', encoding='utf-8') as f: self.translations[lang_code] = json.load(f) print(f"Info: Loaded translations for language: {lang_code}") except Exception as e: print(f"Error: Failed to load translations for {lang_code}: {e}") else: print(f"Warning: Translation file not found: {file_path}") def get_locale(self) -> str: """ Get the current locale based on user preference, session, or browser settings. Returns: Language code (e.g., 'en', 'zh') """ # 1. Check URL parameter (for language switching) if 'lang' in request.args: lang = request.args.get('lang') if lang in self.supported_languages: session['language'] = lang return lang # 2. Check session (user's previous choice) if 'language' in session: lang = session['language'] if lang in self.supported_languages: return lang # 3. Check browser's Accept-Language header if request.headers.get('Accept-Language'): browser_langs = request.headers.get('Accept-Language').split(',') for browser_lang in browser_langs: # Extract language code (e.g., 'zh-CN' -> 'zh') lang_code = browser_lang.split(';')[0].split('-')[0].strip().lower() if lang_code in self.supported_languages: session['language'] = lang_code return lang_code # 4. Fall back to default language return self.default_language def set_locale(self, lang_code: str) -> bool: """ Set the current locale. Args: lang_code: Language code to set Returns: True if successful, False if language not supported """ if lang_code in self.supported_languages: session['language'] = lang_code return True return False def translate(self, key: str, **kwargs) -> str: """ Translate a text key to the current locale. Args: key: Translation key in dot notation (e.g., 'nav.home') **kwargs: Variables for string formatting Returns: Translated text or the key if translation not found """ locale = self.get_locale() # Get translation for current locale translation = self._get_nested_value( self.translations.get(locale, {}), key ) # Fall back to default language if not found if translation is None and locale != self.default_language: translation = self._get_nested_value( self.translations.get(self.default_language, {}), key ) # Fall back to key if still not found if translation is None: translation = key # Format with variables if provided if kwargs and isinstance(translation, str): try: translation = translation.format(**kwargs) except (KeyError, ValueError): pass # Ignore formatting errors return translation def _get_nested_value(self, data: Dict[str, Any], key: str) -> Optional[str]: """ Get a nested value from a dictionary using dot notation. Args: data: Dictionary to search in key: Dot-separated key (e.g., 'nav.home') Returns: Value if found, None otherwise """ keys = key.split('.') current = data for k in keys: if isinstance(current, dict) and k in current: current = current[k] else: return None return current if isinstance(current, str) else None def get_supported_languages(self) -> Dict[str, str]: """ Get a dictionary of supported languages with their display names. Returns: Dictionary mapping language codes to display names """ return { 'en': 'English', 'zh': '中文' } def get_language_info(self, lang_code: str) -> Dict[str, str]: """ Get information about a specific language. Args: lang_code: Language code Returns: Dictionary with language information """ language_names = { 'en': {'name': 'English', 'native': 'English'}, 'zh': {'name': 'Chinese', 'native': '中文'} } return language_names.get(lang_code, { 'name': lang_code.upper(), 'native': lang_code.upper() }) # Global instance language_manager = LanguageManager() def init_i18n(app): """Initialize i18n support for the Flask application.""" language_manager.init_app(app) return language_manager # Template helper functions def _(key: str, **kwargs) -> str: """Shorthand translation function for use in templates and code.""" return language_manager.translate(key, **kwargs) def get_locale() -> str: """Get the current locale.""" return language_manager.get_locale() def set_locale(lang_code: str) -> bool: """Set the current locale.""" return language_manager.set_locale(lang_code)