i18n
Two locales ship — en and ar, 542 keys each, identical key sets — and the
UI is genuinely bilingual: Arabic gets RTL layout, not just translated words.
| Piece | Where | Behaviour |
|---|---|---|
| Locale files | web/locales/en.json, ar.json |
flat key → string; nested JSON is flattened to dotted keys at load, so either shape works |
| Loader | internal/i18n |
one Translator at boot; a missing or unparseable locale file is a fatal boot error |
Lookup T(lang, key) |
everywhere | requested lang → default lang (prefers en) → the key itself as the last resort |
| Negotiation | web.LanguageMiddleware |
runs globally before every route (the login page included); ?lang= or the lang cookie, resolved into the request context |
| Rendering | web.NewLocalizedHTMLRender |
one compiled template set per language, with t/dir/lang/date funcs bound to it — this removed an older request-shared atomic "current language" that could leak one request's locale into a concurrent render |
| RTL | config.DirectionFor |
ar, he, fa, ur → rtl; everything else ltr. The layout template sets dir from it; the Cairo font stack (web/assets/css/cairo-fonts.css, vendored woff2) covers Arabic glyphs |
| Enabled languages | LANGS env |
comma-separated, first is default; deployed tenants run en,ar |
Key naming
- Flat,
snake_case, descriptive:add_student,cash_box,default_currency_not_enabled,attendance_summary_desc. Dots appear only where a nested object was flattened. - Errors are keys too: handlers call
h.i18n.T(lang, "save_failed")and the form re-renders with the translated message. - Format strings keep
%d/%sverbs (e.g.bulk_assigned:%d student(s) assigned) and are consumed withfmt.Sprintfon the Go side.
Where new strings must land
Every user-visible literal added to a template or a handler needs a key in
both web/locales/en.json and web/locales/ar.json — the key sets are
kept identical (this pass verified both files at 542 keys with no diff either
way), and a missing key falls back to English and then to the raw key, which
renders as a bug. Templates reach strings through the t func; handlers and
view-model code through h.i18n.T(lang, key).
Plugin strings follow the same rule from the outside: the host translates a
plugin field under plugin_<id>_<key> and falls back to the plugin's own
Field.Label — see settings. Labels on the
catalogue itself (the roles matrix) are dual-written in code: En/Ar columns
on every rbac.Resource.
Not determined
- Whether CI enforces en/ar key-set equality: no such check was found in the workflows or the test suite; the parity observed here is a convention the repo keeps, not a guard the repo runs.
Rendered from simple-lms/i18n.md · OKF bundle · regenerate with npm run render