Skip to content

Localization

Almasix’s localization features retrieve strings in various languages so an application can support more than one locale. Catalogs live under the app’s lang/ directory. Groups use short keys in Python files; JSON catalogs use the default English string as the key — the same two approaches Laravel documents.

lang/
en/
messages.py
validation.py
sw/
messages.py
en.json
sw.json

Prism views can call the same helpers: @lang, __, trans, and trans_choice are injected into every template. See Control Structures.

Scaffolded apps may ship a thin lang/ tree. To publish the framework’s English catalogs (validation, auth, passwords, pagination, errors) into the application:

Terminal window
python smith lang:publish

Create an empty tree for a new locale:

Terminal window
python smith make:lang sw

Report keys present in the fallback locale but missing from a target:

Terminal window
python smith lang:missing --locale=sw

The default and fallback locales come from config/app.py, typically via the environment:

Variable Default Purpose
APP_LOCALE en Active locale when none is set on the request
APP_FALLBACK_LOCALE en Used when the active locale lacks a key
config/app.py
from almasix.config import env
config = {
"locale": env("APP_LOCALE", "en"),
"fallback_locale": env("APP_FALLBACK_LOCALE", "en"),
# ...
}

Change the locale for the current request (ASGI-scoped — not process-global):

examples/localization.py
from almasix.translation import set_locale, get_locale, is_locale
set_locale("sw")
get_locale() # "sw"
is_locale("sw") # True

Or through the Lang façade:

examples/localization.py
from almasix.translation import Lang
Lang.set_locale("sw")
Lang.get_locale()
Lang.set_fallback("en")

SetLocaleMiddleware negotiates the locale when nothing was set earlier in the pipeline: session key locale first, then Accept-Language, then the config default. Alias it (often as locale) after session middleware on web routes:

bootstrap/app.py
from almasix.translation import SetLocaleMiddleware
middleware.alias({"locale": SetLocaleMiddleware})

Place a Python file per group under lang/<locale>/. The module exposes a translations dict (nested keys are fine):

lang/en/messages.py
translations = {
"welcome": "Welcome to our application!",
"hello": "Hello, :name",
}
lang/sw/messages.py
translations = {
"welcome": "Karibu kwenye programu yetu!",
"hello": "Habari, :name",
}

For languages that differ by territory, name directories with underscores (en_GB), matching common locale tags.

For apps with many UI strings, store the default wording as the JSON key:

lang/sw.json
{
"I love Almasix.": "Napenda Almasix."
}

Do not invent short keys that collide with a group filename. Looking up __("Action") while lang/nl/action.py exists (and no nl.json) can resolve as the whole group file rather than a JSON line.

Use __ or trans with dotted file.key for group catalogs, or the literal string for JSON catalogs:

examples/localization.py
from almasix.translation import __, trans, Lang
__("messages.welcome")
trans("messages.welcome")
Lang.get("messages.welcome")
__("I love Almasix.")

If the key is missing in the active locale and the fallback, the helpers return the key itself — never blank, never an exception.

In Prism:

resources/views/welcome.prism.html
@lang("messages.welcome")
{{ __("messages.welcome", {"name": name}) }}

Placeholders are prefixed with :. Pass replacements as the second argument:

lang/en/messages.py
translations = {
"welcome": "Welcome, :name",
}
examples/localization.py
__("messages.welcome", {"name": "Ada"})
# Welcome, Ada

Placeholder case follows Laravel:

Placeholder Replacement
:name as given
:NAME uppercased
:Name first letter uppercased
lang/en/messages.py
translations = {
"welcome": "Welcome, :NAME", # Welcome, ADA
"goodbye": "Goodbye, :Name", # Goodbye, Ada
}

Separate singular and plural (or more forms) with |. Retrieve with trans_choice / Lang.choice:

lang/en/messages.py
translations = {
"apples": "There is one apple|There are many apples",
}
examples/localization.py
from almasix.translation import trans_choice
trans_choice("messages.apples", 10)

Interval forms and explicit counts work the same as Laravel:

lang/en/messages.py
translations = {
"apples": "{0} There are none|[1,19] There are some|[20,*] There are many",
"minutes_ago": "{1} :value minute ago|[2,*] :value minutes ago",
}
examples/localization.py
trans_choice("messages.minutes_ago", 5, {"value": 5})

:count is injected automatically from the number passed to trans_choice.

When a string has more than two segments without intervals, Almasix selects by CLDR plural categories for the active locale (via Babel) — zero / one / two / few / many / other — not English-only one/other pretending to be universal.

JSON catalogs support pipe plurals as values as well:

lang/sw.json
{
"There is one apple|There are many apples": "Kuna tofaa moja|Kuna matofaa mengi"
}
examples/localization.py
from almasix.translation import Lang
Lang.has("messages.welcome")
Lang.has_for_locale("messages.welcome", "sw") # no fallback

Optionally register a callback when a key cannot be resolved (default still returns the key):

app/providers/app_service_provider.py
from almasix.translation import Lang
Lang.handle_missing_keys_using(
lambda key, locale, replace: f"[missing:{key}]"
)

Packages register a namespace; look up with package::file.key:

examples/localization.py
from almasix.translation import Lang, __
Lang.add_namespace("acme", "/path/to/acme/lang")
__("acme::messages.welcome")

Runtime lines without a file:

examples/localization.py
Lang.add_lines({"flash": "Saved"}, locale="en", namespace="*")

To override a package’s strings without editing the package, place files under lang/vendor/<package>/<locale>/:

lang/vendor/acme/en/messages.py

Only define the keys you want to change; everything else still loads from the package catalog.

almasix.translation.Number formats values with Babel using the active (or given) locale — the localization counterpart to the support Helpers Number utilities:

examples/localization.py
from almasix.translation import Number, localize_date, localize_time, set_locale
from datetime import datetime
set_locale("de")
Number.format(1234.5)
Number.percentage(10) # "10%"
Number.currency(99.9, "EUR")
Number.file_size(1_500_000)
Number.for_humans(1_500_000)
localize_date(datetime.now(), format="medium")
localize_time(datetime.now(), format="short")