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-reset06-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.