feat(i18n): add browser-language localization with Jinja2 _() and locale middleware
Some checks failed
ci/woodpecker/pr/pipeline Pipeline failed
Some checks failed
ci/woodpecker/pr/pipeline Pipeline failed
Add i18n support to the blog web UI with 4 languages (en/ru/fr/de),
80 translation keys, automatic Accept-Language detection, persistent
locale cookie, and a language switcher dropdown in the header.
- Infrastructure: TranslationService, translation dicts, convenience _()
- Presentation: locale middleware, /web/lang/{locale} switcher route
- Templates: all 9 templates use {{ _(key, current_locale) }}
- Tests: 26 tests across TranslationService, locale detection helpers
- Docs: TEST_MODEL.md and FEATURE_INFRASTRUCTURE.md updated with TC-UNIT-811-821
This commit is contained in:
78
app/infrastructure/i18n/translator.py
Normal file
78
app/infrastructure/i18n/translator.py
Normal file
@@ -0,0 +1,78 @@
|
||||
"""Translation service for i18n support.
|
||||
|
||||
This module provides the translation service that resolves translation keys
|
||||
to localized strings using in-memory translation dictionaries. Falls back
|
||||
from requested locale through English to the raw key.
|
||||
"""
|
||||
|
||||
from app.infrastructure.i18n.translations import TRANSLATIONS
|
||||
|
||||
SUPPORTED_LOCALES = frozenset({"en", "ru", "fr", "de"})
|
||||
DEFAULT_LOCALE = "en"
|
||||
|
||||
|
||||
class TranslationService:
|
||||
"""Service for resolving translation keys to localized strings.
|
||||
|
||||
Provides a singleton-like interface for translating UI strings
|
||||
across the application. Falls back through requested locale to
|
||||
English and finally to the raw key if no translation exists.
|
||||
|
||||
Attributes:
|
||||
translations: Dictionary of locale to key to string mappings.
|
||||
"""
|
||||
|
||||
_instance: "TranslationService | None" = None
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize translation service with translation data."""
|
||||
self.translations = TRANSLATIONS
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls) -> "TranslationService":
|
||||
"""Get or create the singleton instance.
|
||||
|
||||
Returns:
|
||||
The shared TranslationService instance.
|
||||
"""
|
||||
if cls._instance is None:
|
||||
cls._instance = cls()
|
||||
return cls._instance
|
||||
|
||||
def get_text(self, key: str, locale: str = DEFAULT_LOCALE) -> str:
|
||||
"""Get translated text for a given key and locale.
|
||||
|
||||
Resolves the key through the locale chain: requested locale,
|
||||
then English fallback, then the raw key itself.
|
||||
|
||||
Args:
|
||||
key: Translation key (e.g. ``nav.home``).
|
||||
locale: Target locale code (e.g. ``en``, ``ru``, ``fr``, ``de``).
|
||||
|
||||
Returns:
|
||||
Translated string if found, otherwise the English version
|
||||
or the key itself as last resort.
|
||||
"""
|
||||
locale_translations = self.translations.get(locale)
|
||||
if locale_translations is not None and key in locale_translations:
|
||||
return locale_translations[key]
|
||||
|
||||
if locale != DEFAULT_LOCALE:
|
||||
fallback = self.translations.get(DEFAULT_LOCALE, {}).get(key)
|
||||
if fallback is not None:
|
||||
return fallback
|
||||
|
||||
return key
|
||||
|
||||
|
||||
def _(key: str, locale: str = DEFAULT_LOCALE) -> str:
|
||||
"""Convenience function for translating a single key.
|
||||
|
||||
Args:
|
||||
key: Translation key to look up.
|
||||
locale: Target locale code.
|
||||
|
||||
Returns:
|
||||
Translated string or the key itself if no translation found.
|
||||
"""
|
||||
return TranslationService.get_instance().get_text(key, locale)
|
||||
Reference in New Issue
Block a user