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
from framework_m_core.interfaces import t
# In a controller or service
async def greet_user(i18n: I18nProtocol, name: str, locale: str = "en"):
greeting = await i18n.translate(
t("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 and titles in DocTypes support i18n out-of-the-box. You can define them as raw string literals. The framework's static translation extraction tool automatically scans for calls to Field(description="..."), Field(title="..."), and class Meta: label = "..." to add them to your catalogs:
class Invoice(BaseDocType):
class Meta:
label = "Invoice Document" # Automatically extracted
customer: str = Field(
description="Customer name", # Automatically extracted
title="Customer Name", # Automatically extracted
)
The Meta API automatically translates these descriptions (which represent field labels on the frontend) at runtime based on the resolved request locale.
Modular Translations & Namespaces
To prevent translation catalogs from growing into single monolithic files, Framework M supports namespaced translations. This allows libraries and packages to encapsulate their own translation keys.
Namespace Resolution in the Frontend (React)
You can load keys from a specific namespace (e.g. desk or ui) using the useTranslation hook:
import { useTranslation } from "react-i18next";
export function SaveButton() {
// Use the "ui" namespace
const { t } = useTranslation("ui");
return <button>{t("Save")}</button>; // Resolves to ui:Save
}
By default, standard components loaded by standard UI views utilize their respective namespace (e.g., standard form elements use "ui", workspace management uses "desk", and other custom code uses "common").
Namespace Resolution in the Backend (Python)
On the backend, you can specify the target namespace as a keyword argument in translate() calls:
from framework_m_core.interfaces import I18nProtocol, t
async def validate_data(i18n: I18nProtocol, locale: str):
# Resolves translation specifically in the "desk" namespace
error_msg = await i18n.translate(
t("Invalid workspace structure"),
locale=locale,
namespace="desk"
)
return error_msg