Theme
Version
GitHub PyPI Discord
On this page

Locales and context

Every localized result depends on a locale context. It contains the selected locale, fallback chain, writing direction, optional time zone, and the revisions of the catalog and formatter data used by the render.

You normally create the context once from request data and provide it to the root component.

When registered components declare I18n.messages_locale, Citry can also make a source-mode context without engine settings. It infers the default from the unique application source locale, or from the unique library source locale when the application owns no messages. Configure the locale graph below when users need selectable translations, when source ownership is ambiguous, or when the application needs explicit fallback policy.

Configure the locale graph

The engine configuration accepts these fields:

from citry import Citry

app = Citry(
    extensions_defaults={
        "i18n": {
            "source_locale": "en-US",
            "default_locale": "en-US",
            "locales": ("en-US", "cs-CZ", "ar-EG"),
            "fallbacks": {
                "ar-EG": ("en-US",),
            },
            "catalogs": ("my_app_i18n",),
        },
    },
)

source_locale names the language used by application source messages. default_locale is used when make_context() receives no locale. It defaults to source_locale and must appear in locales.

locales is an ordered sequence of locales that users may select. A source locale may be fallback-only, but every default locale must be selectable.

fallbacks maps one known locale to an ordered sequence of other known locales. Citry rejects unknown nodes and cycles when it creates the engine. After those configured fallbacks, each message may use the source locale of the catalog package that owns it.

catalogs is an ordered sequence of import-package names. See Organize catalogs for the package layout and precedence rules.

Citry canonicalizes locale names

Locale names use Unicode BCP 47 spelling. Citry canonicalizes configured and requested names through the same Rust implementation. For example, EN-us becomes en-US and a recognized deprecated language alias becomes its current form.

Two inputs that become the same canonical locale are a configuration error. Catalog directory names are stricter: they must already use the canonical spelling so a package has one stable resource path for each locale.

Unicode extensions may select data such as a numbering system or calendar. The complete tag must be one of the configured locales or an inferred source-mode locale before it can be selected:

from citry.ext.i18n import make_context


context = make_context(app, locale="hi-IN-u-nu-deva")

The complete canonical locale remains part of the context. Citry does not silently reduce it to only its language and region.

Create a context from request data

Pass the same application that owns the components:

from citry.ext.i18n import make_context


context = make_context(
    app,
    locale=request.query_params["locale"],
    time_zone="Europe/Prague",
)

An unknown or empty locale raises an error. An invalid IANA time-zone name also raises an error. Omitting locale selects the configured default_locale or the inferred source-mode default; omitting time_zone creates a zone-free context.

make_context() returns a new immutable value. It does not change the engine's default context and does not affect another request.

Provide the context at the render root

Pass the exact context through Citry's ordinary root-provide channel:

rendered = Page().render(
    provides={"citry_i18n": context},
)

Every descendant rendered along that tree sees the context through self.i18n.context, template tr() and fmt, and the built-in i18n components.

A separate render() call creates a separate root:

class Summary(Component):
    citry = app

    def template_data(self, kwargs, slots):
        context = self.i18n.context
        standalone = Detail().render(
            provides={"citry_i18n": context},
        )
        return {"standalone": standalone}

Passing the context again is intentional. The output of Detail().render() depends on the arguments visible at that call, not on where the function happened to run.

Override one subtree

<c-i18n> provides another context to its descendants:

<main>
  <c-account-card />

  <c-i18n locale="ar-EG" tag="aside">
    <c-account-card />
  </c-i18n>
</main>

With tag="aside", Citry emits a real element with the selected lang and derived dir attributes. Without tag, a server-only provider is transparent and adds no HTML wrapper.

The provider accepts locale, direction, and time_zone overrides. Omitted fields inherit. When the locale changes and direction is omitted, Citry derives the new direction from the locale.

Client-enabled providers need a real tag because the browser uses that element as the subtree boundary. See Browser i18n.

Use the context outside a component

Inside components, use self.i18n. Outside components, use i18n.for_context(context):

service = i18n.for_context(context)

heading = service.tr("my-app-account-title")
amount = service.format.currency(
    total,
    "EUR",
    format="account-balance",
)

The service exposes context, tr(), resolve(), format, and parse. Every operation uses the same explicit context.

Use context identity in a cache key

Localized output must not share a cache entry with output produced under a different context. Pass the context's plain immutable identity through Cache's ordinary vary() contract:

class LocalizedCard(Component):
    class Cache:
        enabled = True

        def vary(self, kwargs, slots):
            return self.component.i18n.context.identity

Cache and i18n remain separate extensions. The cache receives an ordinary public value and does not need an i18n-specific option.