Skip to content

i18n — Internationalization engine

Tools for bot localization and translation management.

Locales Compiler

Utility for automated collection and merging of .ftl files from feature directories.

compile_locales(features_path)

Discovers and compiles .ftl files from features into an isolated tmp directory.

Algorithm: 1. Scans features_path for any subdirectories named locales. 2. Inside each locales, finds language folders (e.g., ru, en). 3. Merges all *.ftl files for each language into a single messages.ftl. 4. Places results in /tmp/bot_locales_{hash}/{locale}/messages.ftl.

Parameters:

Name Type Description Default
features_path Path

Root path to the features directory (e.g. src/my_bot/features).

required

Returns:

Type Description
str

Path template string for FluentRuntimeCore: "/tmp/bot_locales_{hash}/{locale}".

Source code in src/codex_bot/engine/i18n/locales_compiler.py
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
def compile_locales(features_path: pathlib.Path) -> str:
    """Discovers and compiles .ftl files from features into an isolated tmp directory.

    Algorithm:
    1. Scans ``features_path`` for any subdirectories named ``locales``.
    2. Inside each ``locales``, finds language folders (e.g., ``ru``, ``en``).
    3. Merges all ``*.ftl`` files for each language into a single ``messages.ftl``.
    4. Places results in ``/tmp/bot_locales_{hash}/{locale}/messages.ftl``.

    Args:
        features_path: Root path to the features directory (e.g. src/my_bot/features).

    Returns:
        Path template string for FluentRuntimeCore: ``"/tmp/bot_locales_{hash}/{locale}"``.
    """
    # 1. Prepare unique isolated directory
    path_hash = hashlib.md5(str(features_path.absolute()).encode(), usedforsecurity=False).hexdigest()[:8]
    tmp_root = pathlib.Path(tempfile.gettempdir()) / f"bot_locales_{path_hash}"

    if tmp_root.exists():
        try:
            shutil.rmtree(tmp_root)
        except OSError as e:
            log.warning(f"LocalesCompiler | Clean failed: {e}")

    tmp_root.mkdir(parents=True, exist_ok=True)

    if not features_path.exists():
        log.error(f"LocalesCompiler | Path not found: {features_path}")
        return str(tmp_root / "{locale}")

    # 2. Discovery and Merging
    # Dictionary structure: { "en": ["content1", "content2"], "ru": [...] }
    merged_data: dict[str, list[str]] = {}

    # Find all 'locales' directories inside features
    for locales_dir in features_path.rglob("locales"):
        if not locales_dir.is_dir():
            continue

        # Look for language subdirectories (ru, en, etc.)
        for lang_dir in locales_dir.iterdir():
            if not lang_dir.is_dir():
                continue

            lang = lang_dir.name
            if lang not in merged_data:
                merged_data[lang] = []

            # Read all FTL files in the language directory
            for ftl_file in sorted(lang_dir.glob("*.ftl")):
                content = ftl_file.read_text(encoding="utf-8")
                # Add source info for easier debugging of translations
                merged_data[lang].append(f"\n### Source: {ftl_file.as_posix()} ###\n{content}")

    # 3. Write results
    for lang, contents in merged_data.items():
        lang_tmp_dir = tmp_root / lang
        lang_tmp_dir.mkdir(parents=True, exist_ok=True)

        output_file = lang_tmp_dir / "messages.ftl"
        output_file.write_text("\n".join(contents), encoding="utf-8")
        log.debug(f"LocalesCompiler | Merged {len(contents)} files for '{lang}' -> {output_file}")

    log.info(f"LocalesCompiler | Compiled {len(merged_data)} languages from {features_path}")
    return str(tmp_root / "{locale}")

Middlewares

FSMContextI18nManager

Bases: BaseManager

Language manager via FSM storage (Redis).

Locale determination priority: 1. FSM storage (key "locale") — user explicitly selected a language. 2. Telegram language_code — if it's in the allowed_locales list. 3. default_locale — fallback.

Parameters:

Name Type Description Default
allowed_locales list[str] | None

List of allowed language codes (e.g., ["ru", "en", "de"]).

None
default_locale str

Default language if nothing matches.

'en'
Example
from aiogram_i18n import I18nMiddleware
from aiogram_i18n.cores import FluentRuntimeCore

i18n = I18nMiddleware(
    core=FluentRuntimeCore(path="locales/{locale}"),
    manager=FSMContextI18nManager(allowed_locales=["ru", "en"], default_locale="en"),
    default_locale="en",
)
i18n.setup(dp)
Source code in src/codex_bot/engine/middlewares/i18n.py
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
class FSMContextI18nManager(BaseManager):
    """
    Language manager via FSM storage (Redis).

    Locale determination priority:
    1. FSM storage (key "locale") — user explicitly selected a language.
    2. Telegram language_code — if it's in the allowed_locales list.
    3. default_locale — fallback.

    Args:
        allowed_locales: List of allowed language codes (e.g., ["ru", "en", "de"]).
        default_locale: Default language if nothing matches.

    Example:
        ```python
        from aiogram_i18n import I18nMiddleware
        from aiogram_i18n.cores import FluentRuntimeCore

        i18n = I18nMiddleware(
            core=FluentRuntimeCore(path="locales/{locale}"),
            manager=FSMContextI18nManager(allowed_locales=["ru", "en"], default_locale="en"),
            default_locale="en",
        )
        i18n.setup(dp)
        ```
    """

    def __init__(
        self,
        allowed_locales: list[str] | None = None,
        default_locale: str = "en",
    ) -> None:
        super().__init__(default_locale=default_locale)
        self.allowed_locales: list[str] = allowed_locales or []

    async def get_locale(self, event_from_user: User | None = None, **kwargs: Any) -> str:
        """
        Determines the user's current locale.

        Args:
            event_from_user: Telegram user.
            **kwargs: aiogram-i18n context (includes "state").

        Returns:
            String locale code (e.g., "ru").
        """
        state: FSMContext | None = kwargs.get("state")
        if state:
            locale = await StateHelper.get_value(state, "locale")
            if isinstance(locale, str):
                return locale

        if event_from_user:
            lang = event_from_user.language_code
            if lang and (not self.allowed_locales or lang in self.allowed_locales):
                return lang

        return str(self.default_locale)

    async def set_locale(self, locale: str, **kwargs: Any) -> None:
        """
        Saves the selected locale to FSM.

        Args:
            locale: Language code to save.
            **kwargs: Context (includes "state").
        """
        state: FSMContext | None = kwargs.get("state")
        if state:
            await StateHelper.update_value(state, "locale", locale)

Functions

get_locale(event_from_user=None, **kwargs) async

Determines the user's current locale.

Parameters:

Name Type Description Default
event_from_user User | None

Telegram user.

None
**kwargs Any

aiogram-i18n context (includes "state").

{}

Returns:

Type Description
str

String locale code (e.g., "ru").

Source code in src/codex_bot/engine/middlewares/i18n.py
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
async def get_locale(self, event_from_user: User | None = None, **kwargs: Any) -> str:
    """
    Determines the user's current locale.

    Args:
        event_from_user: Telegram user.
        **kwargs: aiogram-i18n context (includes "state").

    Returns:
        String locale code (e.g., "ru").
    """
    state: FSMContext | None = kwargs.get("state")
    if state:
        locale = await StateHelper.get_value(state, "locale")
        if isinstance(locale, str):
            return locale

    if event_from_user:
        lang = event_from_user.language_code
        if lang and (not self.allowed_locales or lang in self.allowed_locales):
            return lang

    return str(self.default_locale)

set_locale(locale, **kwargs) async

Saves the selected locale to FSM.

Parameters:

Name Type Description Default
locale str

Language code to save.

required
**kwargs Any

Context (includes "state").

{}
Source code in src/codex_bot/engine/middlewares/i18n.py
81
82
83
84
85
86
87
88
89
90
91
async def set_locale(self, locale: str, **kwargs: Any) -> None:
    """
    Saves the selected locale to FSM.

    Args:
        locale: Language code to save.
        **kwargs: Context (includes "state").
    """
    state: FSMContext | None = kwargs.get("state")
    if state:
        await StateHelper.update_value(state, "locale", locale)