Internationalization & Timezones
With starlette-admin, you can localize UI strings per user and convert displayed datetimes to the viewer's local timezone, independently of how your database stores them.
Full example: For a complete, working application that demonstrates internationalization and timezones, see examples/10-i18n-timezone in the GitHub repository.
Install the i18n extra
Translation support requires Babel. Without it, the admin still works, but it falls back to English and skips locale-aware date and number formatting.
Set the locale
from sqlalchemy import create_engine
from starlette_admin import I18nConfig
from starlette_admin.contrib.sqla import Admin
from starlette_admin.i18n import SUPPORTED_LOCALES
engine = create_engine("sqlite:///admin.sqlite")
admin = Admin(
engine,
title="My Admin",
i18n_config=I18nConfig(
default_locale="en",
language_switcher=SUPPORTED_LOCALES,
),
secret_key="a-long-random-string",
)
i18n_config defaults to None, which runs the admin in English. When you pass an I18nConfig, the admin installs LocaleMiddleware. The middleware resolves a locale on every request and exposes it to templates and to the translation functions (gettext and lazy_gettext) throughout your field and view code.
The language_switcher parameter adds a dropdown to the admin navigation bar so users can select their locale. Leave it as None, the default, to hide the switcher and rely on default_locale and per-request detection.
I18nConfig reference
| Attribute | Type | Default | Description |
|---|---|---|---|
default_locale |
str |
"en" |
Locale used when no cookie or header matches a supported locale. |
language_cookie_name |
str | None |
"language" |
Cookie read to detect the user's locale. Set to None to disable. |
language_header_name |
str | None |
"Accept-Language" |
Header read when the cookie is absent. Set to None to disable. |
language_switcher |
list[str] | None |
None |
Locales offered in the navbar switcher. None hides the switcher. |
How the locale is detected
LocaleMiddleware resolves the locale once per request, in this order:
- Cookie: The value of
language_cookie_name, if it matches a built-in supported locale. - Header: The
Accept-Languageheader, or the header you set inlanguage_header_name, subject to the same validity check. - Default: The
default_locale, when neither the cookie nor the header matches.
The built-in supported locales (starlette_admin.i18n.SUPPORTED_LOCALES) are German, English, French, Portuguese, Russian, Turkish, and both Simplified and Traditional Chinese. When a user selects a language in the navigation bar, the switcher writes the language cookie, so the choice persists across requests without server-side session storage.
Timezones
from sqlalchemy import create_engine
from starlette_admin import TimezoneConfig
from starlette_admin.contrib.sqla import Admin
engine = create_engine("sqlite:///admin.sqlite")
admin = Admin(
engine,
title="My Admin",
timezone_config=TimezoneConfig(
default_timezone="UTC",
database_timezone="UTC",
timezone_switcher=["UTC", "Europe/Paris", "America/New_York", "Asia/Tokyo"],
),
secret_key="a-long-random-string",
)
Note
Unlike i18n_config, timezone_config isn't None by default. If you omit it, the Admin class constructs a TimezoneConfig() for you, so timezone conversion is on out of the box. The admin treats naive datetimes as the database_timezone, which defaults to "UTC", and displays them to every user in "UTC", the default default_timezone, unless the user selects another one.
How the timezone is detected
TimezoneMiddleware resolves the timezone once per request:
- Cookie: The value of
timezone_cookie_name, which is"timezone"by default, if it's present. - Default: The
default_timezone, otherwise.
The navbar timezone switcher writes this cookie, exactly like the language switcher. Timezones have no equivalent of the Accept-Language header because browsers don't send one, so detection relies entirely on cookies. Client-side JavaScript that reads Intl.DateTimeFormat().resolvedOptions().timeZone typically sets the cookie, as does the switcher itself.
How field values are converted
DateTimeField and ArrowField convert values between the database_timezone and the viewer's resolved timezone.
- Reading (list, detail, export): The admin treats a naive value from your database as a value in the
database_timezoneand converts it to the viewer's timezone before formatting it. - Writing (create and edit forms): The admin treats a submitted value as a value in the viewer's timezone and converts it to the
database_timezonebefore it reaches your model.
As a result, two admins in different timezones can edit the same row and each see times in their own local time, while the database keeps a single, consistent timezone.
TimezoneConfig reference
| Attribute | Type | Default | Description |
|---|---|---|---|
default_timezone |
str |
"UTC" |
Timezone used when no cookie is set. Accepts any IANA timezone name. |
timezone_cookie_name |
str | None |
"timezone" |
Cookie read to detect the viewer's timezone. Set to None to disable. |
database_timezone |
str |
"UTC" |
Timezone your stored datetimes are assumed to be in. |
timezone_switcher |
list[str] | None |
None |
Timezones offered in the navbar switcher. None hides the switcher. |
use_user_locale_timezone |
bool |
True |
Prefer a timezone inferred from the user's locale over default_timezone. |
To restrict what users can select, pass a shorter timezone_switcher list. To enforce a single timezone for every viewer, set timezone_switcher to None and set default_timezone directly, for example to a company-wide "Europe/Paris".
The navbar switcher renders each timezone's display name and UTC offset with the get_timezone and get_timezone_display_name template globals. The Templates guide documents these globals and all the others.
What's next: