Skip to main content

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:

  1. Request locale - Accept-Language header
  2. User preference - user.locale setting
  3. Tenant default - tenant.default_locale
  4. 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.