Internationalization (i18n)
Framework M provides built-in internationalization support through the I18nProtocol.
I18nProtocol
The core interface for translations:
from framework_m.core.interfaces.i18n import I18nProtocol
class I18nProtocol(Protocol):
async def translate(
self,
text: str,
locale: str | None = None,
*,
context: dict[str, str] | None = None,
default: str | None = None,
) -> str: ...
async def get_locale(self) -> str: ...
async def set_locale(self, locale: str) -> None: ...
async def get_available_locales(self) -> list[str]: ...
Using Translations
# In a controller or service
async def greet_user(i18n: I18nProtocol, name: str, locale: str = "en"):
greeting = await i18n.translate(
"Hello, {name}!",
locale=locale,
context={"name": name}
)
return greeting
InMemoryI18nAdapter
A simple adapter for testing and development:
from framework_m.adapters.i18n import InMemoryI18nAdapter
i18n = InMemoryI18nAdapter(default_locale="en")
# Add translations
i18n.add_translation("es", "Hello", "Hola")
i18n.add_translation("fr", "Hello", "Bonjour")
# Use translations
text = await i18n.translate("Hello", locale="es")
# Returns: "Hola"
# With interpolation
i18n.add_translation("es", "Hello, {name}!", "¡Hola, {name}!")
text = await i18n.translate(
"Hello, {name}!",
locale="es",
context={"name": "Juan"}
)
# Returns: "¡Hola, Juan!"
Locale Resolution
The framework supports locale resolution in this order:
- Request locale -
Accept-Languageheader - User preference -
user.localesetting - Tenant default -
tenant.default_locale - System default -
settings.DEFAULT_LOCALE
Creating Custom Adapters
Implement I18nProtocol for custom translation backends:
class DatabaseI18nAdapter:
"""Load translations from database."""
def __init__(self, repo: TranslationRepository):
self.repo = repo
self._cache: dict[str, dict[str, str]] = {}
async def translate(
self,
text: str,
locale: str | None = None,
*,
context: dict[str, str] | None = None,
default: str | None = None,
) -> str:
target_locale = locale or await self.get_locale()
# Check cache first
if target_locale in self._cache:
if text in self._cache[target_locale]:
translation = self._cache[target_locale][text]
if context:
for key, value in context.items():
translation = translation.replace(f"{{{key}}}", value)
return translation
# Fallback to original text
return default or text
async def load_translations(self, locale: str) -> None:
"""Load all translations for a locale into cache."""
translations = await self.repo.get_by_locale(locale)
self._cache[locale] = {t.source: t.translated for t in translations}
DocType Labels
Field labels in DocTypes support i18n:
class Invoice(BaseDocType):
customer: str = Field(
description="Customer name", # Can be translated via i18n
)
The Meta API returns translated labels based on request locale.