Skip to main content

src/telega_i18n.erl

-module(telega_i18n).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/telega_i18n.gleam").
-export([new/1, add_locale/3, with_fallback/3, default_locale/1, locales/1, with_command_translations/3, add_toml/3, add_json/3, load_toml_dir/2, load_json_dir/2, translate/4, plural_category/2, translate_count/5, resolve_locale/3, user_language_code/1, enter/2, leave/0, current_locale/0, translate_current/2, t/3, tn/4, middleware/2]).
-export_type([catalog/0, i18n_error/0, state/0]).

-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.

?MODULEDOC(
    " Internationalization (i18n) for the [Telega](https://hexdocs.pm/telega)\n"
    " Telegram Bot Library.\n"
    "\n"
    " Translations live in a [`Catalog`](#Catalog) — one flat table of dotted\n"
    " keys per locale. Load them from TOML or JSON, install the\n"
    " [`middleware`](#middleware) to resolve the active locale per update, then\n"
    " call [`t`](#t) inside handlers.\n"
    "\n"
    " ## Quick start\n"
    "\n"
    " `locales/en.toml`:\n"
    "\n"
    " ```toml\n"
    " greeting = \"Hello, {name}!\"\n"
    "\n"
    " [cart]\n"
    " title = \"Your cart\"\n"
    " ```\n"
    "\n"
    " `locales/ru.toml`:\n"
    "\n"
    " ```toml\n"
    " greeting = \"Привет, {name}!\"\n"
    "\n"
    " [cart]\n"
    " title = \"Ваша корзина\"\n"
    " ```\n"
    "\n"
    " ```gleam\n"
    " import gleam/option.{None}\n"
    " import telega/router\n"
    " import telega_i18n\n"
    "\n"
    " pub fn build_router(catalog) {\n"
    "   // `catalog` loaded once at startup, e.g. with `load_toml_dir`.\n"
    "   router.new(\"bot\")\n"
    "   |> router.use_middleware(telega_i18n.middleware(\n"
    "     catalog:,\n"
    "     // Optional per-user override stored in the session. Return `None`\n"
    "     // to fall back to the user's Telegram `language_code`.\n"
    "     from: fn(_session) { None },\n"
    "   ))\n"
    "   |> router.on_command(\"start\", greet)\n"
    " }\n"
    "\n"
    " fn greet(ctx, _command) {\n"
    "   let msg = telega_i18n.t(ctx, \"greeting\", [#(\"name\", \"Lucy\")])\n"
    "   // -> \"Hello, Lucy!\" or \"Привет, Lucy!\" depending on the user's locale\n"
    "   reply.with_text(ctx, msg)\n"
    " }\n"
    " ```\n"
    "\n"
    " ## Locale resolution\n"
    "\n"
    " For every update the middleware picks the first available of:\n"
    "\n"
    " 1. the session override returned by your `from` resolver,\n"
    " 2. the sender's Telegram `language_code`,\n"
    " 3. the catalog's default locale.\n"
    "\n"
    " The resolved locale is stored in the process dictionary of the chat\n"
    " instance handling the update, so [`t`](#t) needs only the key.\n"
    "\n"
    " ## Fallback chains\n"
    "\n"
    " Lookups walk a chain: the active locale, its base language (`\"en-US\"` →\n"
    " `\"en\"`), any explicit [`with_fallback`](#with_fallback) entries, and finally\n"
    " the default locale. A missing key returns the key itself, so nothing ever\n"
    " crashes on a typo.\n"
    "\n"
    " ## Pluralization\n"
    "\n"
    " [`tn`](#tn) selects a CLDR plural category (`one`/`few`/`many`/`other`) and\n"
    " looks up `\"<key>.<category>\"`. The `count` is injected as `{count}`\n"
    " automatically. English and Russian rules are built in.\n"
    "\n"
    " ```toml\n"
    " [items]\n"
    " one = \"{count} item\"\n"
    " other = \"{count} items\"\n"
    " ```\n"
    "\n"
    " ```gleam\n"
    " telega_i18n.tn(ctx, \"items\", 5, [])\n"
    " // -> \"5 items\"\n"
    " ```\n"
).

-opaque catalog() :: {catalog,
        binary(),
        gleam@dict:dict(binary(), gleam@dict:dict(binary(), binary())),
        gleam@dict:dict(binary(), list(binary()))}.

-type i18n_error() :: {parse_error, binary()} | {file_error, binary()}.

-type state() :: {state, catalog(), binary()}.

-file("src/telega_i18n.gleam", 132).
?DOC(
    " Create an empty catalog with the given default locale. The default is the\n"
    " last link of every fallback chain.\n"
).
-spec new(binary()) -> catalog().
new(Default_locale) ->
    {catalog, Default_locale, maps:new(), maps:new()}.

-file("src/telega_i18n.gleam", 138).
?DOC(
    " Add (or merge into) a locale from an already-flattened map of dotted keys\n"
    " to templates. Later entries win on conflict.\n"
).
-spec add_locale(catalog(), binary(), gleam@dict:dict(binary(), binary())) -> catalog().
add_locale(Catalog, Locale, Translations) ->
    Existing = begin
        _pipe = gleam_stdlib:map_get(erlang:element(3, Catalog), Locale),
        gleam@result:unwrap(_pipe, maps:new())
    end,
    Merged = maps:merge(Existing, Translations),
    {catalog,
        erlang:element(2, Catalog),
        gleam@dict:insert(erlang:element(3, Catalog), Locale, Merged),
        erlang:element(4, Catalog)}.

-file("src/telega_i18n.gleam", 153).
?DOC(
    " Register an explicit fallback chain for a locale. These locales are tried\n"
    " (in order) after the active locale and its base language but before the\n"
    " default locale.\n"
).
-spec with_fallback(catalog(), binary(), list(binary())) -> catalog().
with_fallback(Catalog, Locale, Chain) ->
    {catalog,
        erlang:element(2, Catalog),
        erlang:element(3, Catalog),
        gleam@dict:insert(erlang:element(4, Catalog), Locale, Chain)}.

-file("src/telega_i18n.gleam", 162).
?DOC(" The catalog's default locale.\n").
-spec default_locale(catalog()) -> binary().
default_locale(Catalog) ->
    erlang:element(2, Catalog).

-file("src/telega_i18n.gleam", 167).
?DOC(" The list of locales the catalog knows about.\n").
-spec locales(catalog()) -> list(binary()).
locales(Catalog) ->
    maps:keys(erlang:element(3, Catalog)).

-file("src/telega_i18n.gleam", 364).
-spec interpolate(binary(), list({binary(), binary()})) -> binary().
interpolate(Template, Args) ->
    gleam@list:fold(
        Args,
        Template,
        fun(Acc, Pair) ->
            gleam@string:replace(
                Acc,
                <<<<"{"/utf8, (erlang:element(1, Pair))/binary>>/binary,
                    "}"/utf8>>,
                erlang:element(2, Pair)
            )
        end
    ).

-file("src/telega_i18n.gleam", 348).
-spec locale_chain(catalog(), binary()) -> list(binary()).
locale_chain(Catalog, Locale) ->
    Base = case gleam@string:split_once(Locale, <<"-"/utf8>>) of
        {ok, {Language, _}} ->
            [Language];

        {error, _} ->
            []
    end,
    Extra = begin
        _pipe = gleam_stdlib:map_get(erlang:element(4, Catalog), Locale),
        gleam@result:unwrap(_pipe, [])
    end,
    _pipe@1 = [Locale],
    _pipe@2 = lists:append(_pipe@1, Base),
    _pipe@3 = lists:append(_pipe@2, Extra),
    _pipe@4 = lists:append(_pipe@3, [erlang:element(2, Catalog)]),
    gleam@list:unique(_pipe@4).

-file("src/telega_i18n.gleam", 334).
-spec lookup(catalog(), binary(), binary()) -> {ok, binary()} | {error, nil}.
lookup(Catalog, Locale, Key) ->
    _pipe = locale_chain(Catalog, Locale),
    gleam@list:find_map(
        _pipe,
        fun(Loc) ->
            case gleam_stdlib:map_get(erlang:element(3, Catalog), Loc) of
                {ok, Table} ->
                    gleam_stdlib:map_get(Table, Key);

                {error, _} ->
                    {error, nil}
            end
        end
    ).

-file("src/telega_i18n.gleam", 194).
?DOC(
    " Wire localized command descriptions from this catalog into a `telega`\n"
    " builder. Implies `telega.with_auto_commands`: the bot publishes its commands\n"
    " on start (default language first, then one `setMyCommands(language_code:)`\n"
    " per catalog locale).\n"
    "\n"
    " Each command's description is looked up at `prefix <> command` — command\n"
    " `\"start\"` with `prefix: \"commands.\"` reads catalog key `\"commands.start\"`,\n"
    " honoring the catalog's fallback chains. A missing key keeps the description\n"
    " the command was registered with in the router.\n"
    "\n"
    " ```gleam\n"
    " let catalog =\n"
    "   i18n.new(\"en\")\n"
    "   |> i18n.add_toml(\"en\", en_toml)\n"
    "   |> i18n.add_toml(\"ru\", ru_toml)\n"
    "\n"
    " telega.new_for_polling(api_client:)\n"
    " |> telega.with_router(router)\n"
    " |> i18n.with_command_translations(catalog, prefix: \"commands.\")\n"
    " |> telega.init_for_polling()\n"
    " ```\n"
).
-spec with_command_translations(
    telega:telega_builder(BGGF, BGGG, BGGH),
    catalog(),
    binary()
) -> telega:telega_builder(BGGF, BGGG, BGGH).
with_command_translations(Builder, Catalog, Prefix) ->
    telega:with_command_translations(
        Builder,
        locales(Catalog),
        fun(Command, Locale) ->
            case lookup(Catalog, Locale, <<Prefix/binary, Command/binary>>) of
                {ok, Template} ->
                    {some, interpolate(Template, [])};

                {error, _} ->
                    none
            end
        end
    ).

-file("src/telega_i18n.gleam", 626).
-spec bool_to_string(boolean()) -> binary().
bool_to_string(Value) ->
    case Value of
        true ->
            <<"true"/utf8>>;

        false ->
            <<"false"/utf8>>
    end.

-file("src/telega_i18n.gleam", 570).
-spec toml_leaf(tom:toml()) -> {ok, binary()} | {error, nil}.
toml_leaf(Value) ->
    case tom:as_string(Value) of
        {ok, Text} ->
            {ok, Text};

        {error, _} ->
            case tom:as_int(Value) of
                {ok, I} ->
                    {ok, erlang:integer_to_binary(I)};

                {error, _} ->
                    case tom:as_float(Value) of
                        {ok, F} ->
                            {ok, gleam_stdlib:float_to_string(F)};

                        {error, _} ->
                            case tom:as_bool(Value) of
                                {ok, B} ->
                                    {ok, bool_to_string(B)};

                                {error, _} ->
                                    {error, nil}
                            end
                    end
            end
    end.

-file("src/telega_i18n.gleam", 619).
-spec join_key(binary(), binary()) -> binary().
join_key(Prefix, Key) ->
    case Prefix of
        <<""/utf8>> ->
            Key;

        _ ->
            <<<<Prefix/binary, "."/utf8>>/binary, Key/binary>>
    end.

-file("src/telega_i18n.gleam", 552).
-spec flatten_toml(
    binary(),
    gleam@dict:dict(binary(), tom:toml()),
    list({binary(), binary()})
) -> list({binary(), binary()}).
flatten_toml(Prefix, Table, Acc) ->
    gleam@dict:fold(
        Table,
        Acc,
        fun(Acc@1, Key, Value) ->
            Path = join_key(Prefix, Key),
            case tom:as_table(Value) of
                {ok, Sub} ->
                    flatten_toml(Path, Sub, Acc@1);

                {error, _} ->
                    case toml_leaf(Value) of
                        {ok, Text} ->
                            [{Path, Text} | Acc@1];

                        {error, _} ->
                            Acc@1
                    end
            end
        end
    ).

-file("src/telega_i18n.gleam", 215).
?DOC(
    " Parse a TOML document and merge it into the catalog under `locale`. Nested\n"
    " tables become dotted keys (`[cart] title = \"...\"` → `cart.title`).\n"
).
-spec add_toml(catalog(), binary(), binary()) -> {ok, catalog()} |
    {error, i18n_error()}.
add_toml(Catalog, Locale, Content) ->
    case tom:parse(Content) of
        {ok, Parsed} ->
            Flat = begin
                _pipe = flatten_toml(<<""/utf8>>, Parsed, []),
                maps:from_list(_pipe)
            end,
            {ok, add_locale(Catalog, Locale, Flat)};

        {error, Error} ->
            {error, {parse_error, gleam@string:inspect(Error)}}
    end.

-file("src/telega_i18n.gleam", 589).
-spec flatten_dynamic(
    binary(),
    gleam@dynamic:dynamic_(),
    list({binary(), binary()})
) -> list({binary(), binary()}).
flatten_dynamic(Prefix, Value, Acc) ->
    case gleam@dynamic@decode:run(
        Value,
        {decoder, fun gleam@dynamic@decode:decode_string/1}
    ) of
        {ok, Text} ->
            [{Prefix, Text} | Acc];

        {error, _} ->
            case gleam@dynamic@decode:run(
                Value,
                gleam@dynamic@decode:dict(
                    {decoder, fun gleam@dynamic@decode:decode_string/1},
                    {decoder, fun gleam@dynamic@decode:decode_dynamic/1}
                )
            ) of
                {ok, Sub} ->
                    gleam@dict:fold(
                        Sub,
                        Acc,
                        fun(Acc@1, Key, Value@1) ->
                            flatten_dynamic(
                                join_key(Prefix, Key),
                                Value@1,
                                Acc@1
                            )
                        end
                    );

                {error, _} ->
                    case gleam@dynamic@decode:run(
                        Value,
                        {decoder, fun gleam@dynamic@decode:decode_int/1}
                    ) of
                        {ok, I} ->
                            [{Prefix, erlang:integer_to_binary(I)} | Acc];

                        {error, _} ->
                            case gleam@dynamic@decode:run(
                                Value,
                                {decoder,
                                    fun gleam@dynamic@decode:decode_float/1}
                            ) of
                                {ok, F} ->
                                    [{Prefix, gleam_stdlib:float_to_string(F)} |
                                        Acc];

                                {error, _} ->
                                    case gleam@dynamic@decode:run(
                                        Value,
                                        {decoder,
                                            fun gleam@dynamic@decode:decode_bool/1}
                                    ) of
                                        {ok, B} ->
                                            [{Prefix, bool_to_string(B)} | Acc];

                                        {error, _} ->
                                            Acc
                                    end
                            end
                    end
            end
    end.

-file("src/telega_i18n.gleam", 231).
?DOC(
    " Parse a JSON document and merge it into the catalog under `locale`. Nested\n"
    " objects become dotted keys.\n"
).
-spec add_json(catalog(), binary(), binary()) -> {ok, catalog()} |
    {error, i18n_error()}.
add_json(Catalog, Locale, Content) ->
    case gleam@json:parse(
        Content,
        {decoder, fun gleam@dynamic@decode:decode_dynamic/1}
    ) of
        {ok, Value} ->
            Flat = begin
                _pipe = flatten_dynamic(<<""/utf8>>, Value, []),
                maps:from_list(_pipe)
            end,
            {ok, add_locale(Catalog, Locale, Flat)};

        {error, Error} ->
            {error, {parse_error, gleam@string:inspect(Error)}}
    end.

-file("src/telega_i18n.gleam", 247).
?DOC(
    " Load every `*.toml` file in `dir` as a locale named after the file\n"
    " (`en.toml` → `\"en\"`), merging them into `catalog`.\n"
).
-spec load_toml_dir(catalog(), binary()) -> {ok, catalog()} |
    {error, i18n_error()}.
load_toml_dir(Catalog, Dir) ->
    gleam@result:'try'(
        begin
            _pipe = simplifile_erl:read_directory(Dir),
            gleam@result:map_error(
                _pipe,
                fun(E) -> {file_error, gleam@string:inspect(E)} end
            )
        end,
        fun(Files) -> _pipe@1 = Files,
            _pipe@2 = gleam@list:filter(
                _pipe@1,
                fun(_capture) ->
                    gleam_stdlib:string_ends_with(_capture, <<".toml"/utf8>>)
                end
            ),
            gleam@list:fold(
                _pipe@2,
                {ok, Catalog},
                fun(Acc, File) ->
                    gleam@result:'try'(
                        Acc,
                        fun(Cat) ->
                            gleam@result:'try'(
                                begin
                                    _pipe@3 = simplifile:read(
                                        <<<<Dir/binary, "/"/utf8>>/binary,
                                            File/binary>>
                                    ),
                                    gleam@result:map_error(
                                        _pipe@3,
                                        fun(E@1) ->
                                            {file_error,
                                                gleam@string:inspect(E@1)}
                                        end
                                    )
                                end,
                                fun(Content) ->
                                    Locale = gleam@string:replace(
                                        File,
                                        <<".toml"/utf8>>,
                                        <<""/utf8>>
                                    ),
                                    add_toml(Cat, Locale, Content)
                                end
                            )
                        end
                    )
                end
            ) end
    ).

-file("src/telega_i18n.gleam", 271).
?DOC(
    " Load every `*.json` file in `dir` as a locale named after the file\n"
    " (`en.json` → `\"en\"`), merging them into `catalog`.\n"
).
-spec load_json_dir(catalog(), binary()) -> {ok, catalog()} |
    {error, i18n_error()}.
load_json_dir(Catalog, Dir) ->
    gleam@result:'try'(
        begin
            _pipe = simplifile_erl:read_directory(Dir),
            gleam@result:map_error(
                _pipe,
                fun(E) -> {file_error, gleam@string:inspect(E)} end
            )
        end,
        fun(Files) -> _pipe@1 = Files,
            _pipe@2 = gleam@list:filter(
                _pipe@1,
                fun(_capture) ->
                    gleam_stdlib:string_ends_with(_capture, <<".json"/utf8>>)
                end
            ),
            gleam@list:fold(
                _pipe@2,
                {ok, Catalog},
                fun(Acc, File) ->
                    gleam@result:'try'(
                        Acc,
                        fun(Cat) ->
                            gleam@result:'try'(
                                begin
                                    _pipe@3 = simplifile:read(
                                        <<<<Dir/binary, "/"/utf8>>/binary,
                                            File/binary>>
                                    ),
                                    gleam@result:map_error(
                                        _pipe@3,
                                        fun(E@1) ->
                                            {file_error,
                                                gleam@string:inspect(E@1)}
                                        end
                                    )
                                end,
                                fun(Content) ->
                                    Locale = gleam@string:replace(
                                        File,
                                        <<".json"/utf8>>,
                                        <<""/utf8>>
                                    ),
                                    add_json(Cat, Locale, Content)
                                end
                            )
                        end
                    )
                end
            ) end
    ).

-file("src/telega_i18n.gleam", 300).
?DOC(
    " Translate `key` in an explicit `locale`, interpolating `{placeholder}`\n"
    " values from `args`. Missing keys return the key unchanged.\n"
    "\n"
    " Prefer [`t`](#t) inside handlers; this is the pure building block, handy\n"
    " for tests and locale-agnostic call sites.\n"
).
-spec translate(catalog(), binary(), binary(), list({binary(), binary()})) -> binary().
translate(Catalog, Locale, Key, Args) ->
    case lookup(Catalog, Locale, Key) of
        {ok, Template} ->
            interpolate(Template, Args);

        {error, _} ->
            Key
    end.

-file("src/telega_i18n.gleam", 389).
-spec english_category(integer()) -> binary().
english_category(Count) ->
    case Count of
        1 ->
            <<"one"/utf8>>;

        _ ->
            <<"other"/utf8>>
    end.

-file("src/telega_i18n.gleam", 396).
-spec east_slavic_category(integer()) -> binary().
east_slavic_category(Count) ->
    N = gleam@int:absolute_value(Count),
    Mod10 = N rem 10,
    Mod100 = N rem 100,
    case nil of
        _ when (Mod10 =:= 1) andalso (Mod100 =/= 11) ->
            <<"one"/utf8>>;

        _ when ((Mod10 >= 2) andalso (Mod10 =< 4)) andalso ((Mod100 < 12) orelse (Mod100 > 14)) ->
            <<"few"/utf8>>;

        _ ->
            <<"many"/utf8>>
    end.

-file("src/telega_i18n.gleam", 382).
-spec base_language(binary()) -> binary().
base_language(Locale) ->
    case gleam@string:split_once(Locale, <<"-"/utf8>>) of
        {ok, {Language, _}} ->
            Language;

        {error, _} ->
            Locale
    end.

-file("src/telega_i18n.gleam", 375).
?DOC(
    " Return the CLDR plural category (`\"one\"`, `\"few\"`, `\"many\"`, `\"other\"`) for\n"
    " `count` in `locale`. Russian and English have dedicated rules; every other\n"
    " locale uses the English rule.\n"
).
-spec plural_category(binary(), integer()) -> binary().
plural_category(Locale, Count) ->
    case base_language(Locale) of
        <<"ru"/utf8>> ->
            east_slavic_category(Count);

        <<"uk"/utf8>> ->
            east_slavic_category(Count);

        <<"be"/utf8>> ->
            east_slavic_category(Count);

        _ ->
            english_category(Count)
    end.

-file("src/telega_i18n.gleam", 315).
?DOC(
    " Pluralizing variant of [`translate`](#translate). Picks the CLDR category\n"
    " for `count`, looks up `\"<key>.<category>\"` (falling back to `\"<key>.other\"`),\n"
    " and injects `count` as `{count}`.\n"
).
-spec translate_count(
    catalog(),
    binary(),
    binary(),
    integer(),
    list({binary(), binary()})
) -> binary().
translate_count(Catalog, Locale, Key, Count, Args) ->
    Args@1 = [{<<"count"/utf8>>, erlang:integer_to_binary(Count)} | Args],
    Category = plural_category(Locale, Count),
    case lookup(
        Catalog,
        Locale,
        <<<<Key/binary, "."/utf8>>/binary, Category/binary>>
    ) of
        {ok, Template} ->
            interpolate(Template, Args@1);

        {error, _} ->
            case lookup(Catalog, Locale, <<Key/binary, ".other"/utf8>>) of
                {ok, Template@1} ->
                    interpolate(Template@1, Args@1);

                {error, _} ->
                    interpolate(Key, Args@1)
            end
    end.

-file("src/telega_i18n.gleam", 411).
?DOC(
    " Resolve the active locale from a session override and the sender's Telegram\n"
    " `language_code`, falling back to the catalog default.\n"
).
-spec resolve_locale(
    catalog(),
    gleam@option:option(binary()),
    gleam@option:option(binary())
) -> binary().
resolve_locale(Catalog, Session_locale, Update_locale) ->
    _pipe = Session_locale,
    _pipe@1 = gleam@option:'or'(_pipe, Update_locale),
    gleam@option:unwrap(_pipe@1, erlang:element(2, Catalog)).

-file("src/telega_i18n.gleam", 440).
-spec first_user(list(gleam@option:option(telega@model@types:user()))) -> gleam@option:option(telega@model@types:user()).
first_user(Candidates) ->
    _pipe = Candidates,
    _pipe@1 = gleam@list:find_map(
        _pipe,
        fun(Candidate) -> gleam@option:to_result(Candidate, nil) end
    ),
    gleam@option:from_result(_pipe@1).

-file("src/telega_i18n.gleam", 422).
?DOC(" Extract the sender's `language_code` from a raw update, if present.\n").
-spec user_language_code(telega@model@types:update()) -> gleam@option:option(binary()).
user_language_code(Raw) ->
    _pipe = [gleam@option:then(
            erlang:element(3, Raw),
            fun(M) -> erlang:element(5, M) end
        ),
        gleam@option:then(
            erlang:element(4, Raw),
            fun(M@1) -> erlang:element(5, M@1) end
        ),
        gleam@option:then(
            erlang:element(8, Raw),
            fun(M@2) -> erlang:element(5, M@2) end
        ),
        gleam@option:map(
            erlang:element(16, Raw),
            fun(C) -> erlang:element(3, C) end
        ),
        gleam@option:map(
            erlang:element(14, Raw),
            fun(Q) -> erlang:element(3, Q) end
        ),
        gleam@option:map(
            erlang:element(15, Raw),
            fun(R) -> erlang:element(3, R) end
        ),
        gleam@option:map(
            erlang:element(17, Raw),
            fun(Q@1) -> erlang:element(3, Q@1) end
        ),
        gleam@option:map(
            erlang:element(18, Raw),
            fun(Q@2) -> erlang:element(3, Q@2) end
        ),
        gleam@option:map(
            erlang:element(22, Raw),
            fun(M@3) -> erlang:element(3, M@3) end
        ),
        gleam@option:map(
            erlang:element(23, Raw),
            fun(M@4) -> erlang:element(3, M@4) end
        ),
        gleam@option:map(
            erlang:element(24, Raw),
            fun(R@1) -> erlang:element(3, R@1) end
        )],
    _pipe@1 = first_user(_pipe),
    gleam@option:then(_pipe@1, fun(User) -> erlang:element(7, User) end).

-file("src/telega_i18n.gleam", 484).
-spec state_key() -> gleam@erlang@atom:atom_().
state_key() ->
    erlang:binary_to_atom(<<"telega_i18n_state"/utf8>>).

-file("src/telega_i18n.gleam", 455).
?DOC(
    " Store the active catalog and locale for the current process. The\n"
    " [`middleware`](#middleware) calls this before each handler; call it yourself\n"
    " only if you resolve locales outside the router.\n"
).
-spec enter(catalog(), binary()) -> nil.
enter(Catalog, Locale) ->
    _ = erlang:put(state_key(), gleam_stdlib:identity({state, Catalog, Locale})),
    nil.

-file("src/telega_i18n.gleam", 461).
?DOC(" Clear the i18n state for the current process.\n").
-spec leave() -> nil.
leave() ->
    _ = erlang:erase(state_key()),
    nil.

-file("src/telega_i18n.gleam", 475).
-spec read_state() -> {ok, state()} | {error, nil}.
read_state() ->
    Value = erlang:get(state_key()),
    case Value =:= gleam_stdlib:identity(
        erlang:binary_to_atom(<<"undefined"/utf8>>)
    ) of
        true ->
            {error, nil};

        false ->
            {ok, gleam_stdlib:identity(Value)}
    end.

-file("src/telega_i18n.gleam", 468).
?DOC(
    " The locale active in the current process, if [`enter`](#enter) (or the\n"
    " middleware) has run.\n"
).
-spec current_locale() -> gleam@option:option(binary()).
current_locale() ->
    case read_state() of
        {ok, {state, _, Locale}} ->
            {some, Locale};

        {error, _} ->
            none
    end.

-file("src/telega_i18n.gleam", 517).
?DOC(
    " Translate using the active process locale without a context. [`t`](#t)\n"
    " delegates here.\n"
).
-spec translate_current(binary(), list({binary(), binary()})) -> binary().
translate_current(Key, Args) ->
    case read_state() of
        {ok, {state, Catalog, Locale}} ->
            translate(Catalog, Locale, Key, Args);

        {error, _} ->
            Key
    end.

-file("src/telega_i18n.gleam", 493).
?DOC(
    " Translate `key` for the locale active in the current handler, interpolating\n"
    " `{placeholder}` values from `args`. Requires the [`middleware`](#middleware)\n"
    " (or a manual [`enter`](#enter)); otherwise returns the key unchanged.\n"
).
-spec t(
    telega@bot:context(any(), any(), any()),
    binary(),
    list({binary(), binary()})
) -> binary().
t(_, Key, Args) ->
    translate_current(Key, Args).

-file("src/telega_i18n.gleam", 502).
?DOC(" Pluralizing variant of [`t`](#t). See [`translate_count`](#translate_count).\n").
-spec tn(
    telega@bot:context(any(), any(), any()),
    binary(),
    integer(),
    list({binary(), binary()})
) -> binary().
tn(_, Key, Count, Args) ->
    case read_state() of
        {ok, {state, Catalog, Locale}} ->
            translate_count(Catalog, Locale, Key, Count, Args);

        {error, _} ->
            Key
    end.

-file("src/telega_i18n.gleam", 535).
?DOC(
    " Router middleware that resolves the active locale for every update and makes\n"
    " it available to [`t`](#t)/[`tn`](#tn).\n"
    "\n"
    " `from` reads an optional per-user override out of the session (e.g. a\n"
    " language the user chose in settings); return `None` to fall back to the\n"
    " sender's Telegram `language_code` and then the catalog default.\n"
).
-spec middleware(catalog(), fun((BGIA) -> gleam@option:option(binary()))) -> fun((fun((telega@bot:context(BGIA, BGIC, BGID), telega@update:update()) -> {ok,
        telega@bot:context(BGIA, BGIC, BGID)} |
    {error, BGIC})) -> fun((telega@bot:context(BGIA, BGIC, BGID), telega@update:update()) -> {ok,
        telega@bot:context(BGIA, BGIC, BGID)} |
    {error, BGIC})).
middleware(Catalog, From) ->
    fun(Handler) ->
        fun(Ctx, Update) ->
            Session_locale = From(erlang:element(5, Ctx)),
            Update_locale = user_language_code(
                telega@update:raw(erlang:element(3, Ctx))
            ),
            Locale = resolve_locale(Catalog, Session_locale, Update_locale),
            enter(Catalog, Locale),
            Handler(Ctx, Update)
        end
    end.