Theming and languages
Rebranding is one number. Eleven languages ship in the wheel, one of them right-to-left, and there is nothing to configure to get them.
Available since v0.1.0
The palette is one number
The whole admin’s colour derives from one hue in OKLCH. There is no build step and no stylesheet to fork.
FastFortSettings(
ui={"accent_hue": 232, "accent_chroma": 0.15},
)
accent_hue is 0–360; accent_chroma is 0–0.37. Every accent, border, focus
ring, hover state and shadow follows from those two, in both themes.
OKLCH rather than HSL because lightening a swatch in OKLCH keeps its chroma — an HSL ramp drifts towards grey at the light end, which is why hand-tuned palettes end up with forty hex values that have to be maintained together.
Themes
ui={"theme": "system", "density": "comfortable"}
system · light · dark, and comfortable · compact. system is the
default and follows the browser; a reader can override it in the admin’s own
header, and the choice persists.
The flag that decides which is applied by boot.js, which is the only script
that runs before the first paint. Anywhere later is a frame late, and a frame
late is a visible flash of the wrong palette on every navigation.
Your own CSS
ui={"custom_css_url": "/static/admin-overrides.css"}
Loaded after the built-in stylesheet, which is seven cascade layers
(00-reset … 06-widgets) concatenated into one response. Your file lands after
all of them, so a plain selector wins without !important.
A rich-text editor
ui={"richtext_url": "https://cdn.example.com/ckeditor.js"}
FastFort renders the textarea and marks it data-ff-richtext; this names the
script that finds them. None leaves a plain textarea — a working control rather
than a broken editor.
Bundling one was the alternative and it is the wrong trade: half a megabyte of
editor in the wheel for every project, most of which want a different one or none
at all. If the URL is on another origin, that origin is added to the CSP
script-src — which is why it is configuration and not something a template can
inject.
Environment labels
ui={"environment_label": "PRODUCTION", "environment_tone": "danger"}
Rendered in the header, in info, warning or danger. So the difference
between the two windows somebody has open is visible without reading.
Logo and favicon
ui={"logo_url": "/static/logo.svg", "favicon_url": "/static/favicon.png"}
Languages
Eleven catalogues ship inside the wheel: English, Uzbek, Russian, Turkish, German, French, Spanish, Chinese, Japanese, Korean and Arabic.
There is nothing to configure. The admin is already translated the moment you
install it, and follows the browser’s Accept-Language by default.
ui={"language": None} # follow the browser — the default
ui={"language": "uz"} # pin every visitor to one language
None is the default deliberately: a project that wants its admin in one
language regardless of who opens it should have to say so, rather than everyone
being silently pinned to English.
Arabic turns the layout around
The stylesheet is written in logical properties throughout —
inset-inline-start, not left; padding-inline, not padding-left. One dir
attribute reverses the entire admin: the sidebar, the table alignment, the
breadcrumb separators, the sort arrows.
A test counts the physical directions in the stylesheet and fails when a new one appears. There are exactly two deliberate exceptions: the checkbox tick (a tick is a tick in any language) and the map, which is a coordinate space placed by script in physical pixels.
Translating your own words
FastFort translates its interface — the buttons, the filters, the messages.
It does not translate your model names, and verbose_name is not run through the
catalogue. A model’s name is your word for your own domain, and guessing it in
eleven languages is not something a framework should attempt. Django does not
translate your model names either.
For your own strings:
ui={"locale_dir": "app/locale"}
A directory of <language>.json files whose entries take precedence over
FastFort’s own.
{
"Products": "Mahsulotlar",
"Shipments": "Jo'natmalar",
"Mark discontinued": "Ishlab chiqarishdan olindi deb belgilash"
}
Strings the browser-side script draws
Anything a widget draws for itself — the calendar’s month names, the duration
control’s labels, the command palette’s placeholder — is a string the server
sends, through data-ff-t-* attributes on <html>.
The script is one cached file for every language, so it can never contain user-visible English. A test compares the script’s fallback table against the server’s, because a mismatch leaves one control in English in every language with nothing failing.
Timezone
ui={"timezone": "Asia/Tashkent"}
Datetimes are stored as they always were and rendered in this zone.