12 — Internationalisation#
Two concerns, kept separate:
- Admin UI language — what the staff see. Per user (
users.locale), Bengali by default. - Public site languages — what visitors see. Per site (
sites.locales), with content per language.
12.1 Admin UI#
- Every string comes from
lang/{bn,en}/*.php. No literal UI text in Blade, Livewire or PHP — including validation messages, email subjects, toast texts, empty states, chart labels and CSV headers. - Files:
admin.php(navigation, screens, actions),blocks.php(block names, field labels, help),validation.php(Laravel override plus our custom rules),public.php(strings that appear in public output, likeRead more),emails.php,roles.php,errors.php. - Keys are hierarchical and descriptive:
admin.pages.bulk.publish_confirm, notadmin.msg17. - A test asserts key parity: every key present in
enexists inbnand vice versa, and no value is an empty string. This test is the only thing that reliably keeps a second language alive. - A second test greps Blade and PHP under
app/andresources/views/for suspicious literal text in user-facing positions (button labels, headings,placeholder=) and fails on new occurrences against a small allow-list. - Language switch: a selector in the admin profile menu, saved to
users.locale, applied by middleware. It never depends on the browser header once a user has chosen.
Writing style for strings#
Plain, short, and the same register in both languages. Buttons are verbs (Publish, প্রকাশ করুন).
Errors say what to do next, not what the system failed to do. No developer vocabulary in any
owner-facing string — not "payload", "validation failed", "null", "cache", "JSON". Where a technical
word is unavoidable (SSL, cron), the string explains it in half a sentence.
12.2 Public site languages#
sites.locales lists the active public languages, sites.default_locale names the primary one. A
single-language site keeps ["bn"] and nothing about URLs or switchers appears anywhere.
URL strategy#
| Case | URL |
|---|---|
| Single language | /about — no prefix, ever |
| Multi-language, default locale | /about (default is unprefixed) or /bn/about if prefix_default is on |
| Multi-language, other locale | /en/about |
| Homepage | / and /en |
Decided in ADR-0005. The prefix is a path segment, not a subdomain and not a query parameter: it is cacheable per path, obvious to the owner, and does not need DNS work on shared hosting.
SetLocale middleware resolves in this order: an explicit URL prefix, then a kp_locale cookie
(only for the bare /), then sites.default_locale. The browser Accept-Language header is
not used for the first request, because a cached HTML file cannot vary by header without
splitting the cache per language per visitor.
Translated content#
pages.locale holds the language of the row. pages.translation_of groups translations: the first
page created is the group root and translations point at its id (the root points at itself or null).
Translationspanel in the editor:bn: About us (published),en: — Create translation. Creating one copies the current tree into a newpagesrow with the other locale, a locale-aware slug, andtranslation_ofset. The copy is a starting point; the two rows are independent after that.- Translations keep separate SEO fields, separate featured images and separate publish states.
hreflangalternates are emitted for every published sibling in the group, plusx-defaultfor the site default.- Menus: items carry
visibility.locales, or a site runs one menu per language and the header preset picks by locale. Both work; the installer seeds the simpler single-menu-per-language layout when a second language is enabled. - Headers, footers and theme are shared across locales by default. A part can be restricted to a locale with a condition if a language genuinely needs different chrome.
- Categories and tags are per-locale rows (they have a
site_idand a slug; a translated category is simply another row), linked by convention rather than a column — translating taxonomy structure is a Phase 5 concern if it ever becomes one. - Missing translation: the language switcher links to the translated page when it exists; otherwise it links to the translated homepage and the switcher marks the entry. It never 404s and never silently shows the other language's content.
12.3 Bengali typography#
| Concern | Rule |
|---|---|
| Fonts | Noto Sans Bengali (body) and Noto Serif Bengali (headings), self-hosted woff2, bundled |
unicode-range |
Bengali block split from Latin so Latin text does not pull the Bengali file |
| Subsetting | by Unicode range only. Never by observed glyphs — Bengali conjuncts would break |
| Line height | default 1.7 for Bengali body text (Bengali needs more leading than Latin); the token default reflects this |
| Font size | base 16 px; Bengali at the same optical size reads smaller, so the bn locale adds 1.0625rem body size via a locale class on <html> |
| Numerals | site option numerals: bengali | latin, applied by the shared formatting helper for dates and counts |
| Dates | all dates render through fmt_date() / fmt_datetime(), which take the site timezone, the site date format and the locale — including Bengali month names. No date() or ->format() anywhere in a view |
| Sorting | utf8mb4_0900_ai_ci orders Bengali acceptably for lists; the UI never promises dictionary collation |
| Search | query and index are normalised identically (NFC, strip zero-width joiner/non-joiner where not semantic) before matching — see 06 |
| Slugs | Bengali titles produce either a transliterated ASCII slug (default, better for sharing) or the Bengali text percent-encoded; the choice is a site option, and existing slugs never change when it is flipped |
| Text direction | LTR only. Bengali is LTR; no RTL support is claimed |
| Line breaking | word-break: normal with overflow-wrap: anywhere on narrow containers; never break-all, which mangles conjuncts |
Encoding hygiene: the database, connection, HTML and files are all UTF-8 (utf8mb4). Export files
(CSV) carry a BOM so Excel opens Bengali correctly. Any tooling that round-trips files must preserve
UTF-8 — on Windows, bulk edits run through Python or git, never through a shell that re-encodes.
12.4 Language switcher block#
Settings: display as flags, language codes, or full names; dropdown or inline; show the current language or not; hide when only one language is active (default on, so a single-language site never sees it even if the block is in the header).
It emits real links (/en/about), never JavaScript-only switching, so crawlers and
hreflang agree with what a visitor can click.
12.5 Adding a language#
Settings -> Site info -> Languages -> Addlists the locales KodePress ships strings for.- Choose the locale, confirm.
sites.localesis updated. - KodePress offers to create translation stubs for all published pages (a draft copy per page), or to start empty.
- For a locale with no bundled admin strings, the admin UI falls back to English for missing keys and the screen says which keys are missing, with a CSV export/import to translate them.
Bundled admin locales at launch: bn, en. Others are a translation contribution, not a code
change.
12.6 Tests for this document#
| Test | Asserts |
|---|---|
lang_key_parity |
bn and en have identical key sets, no empty values |
no_literal_ui_text |
grep sweep over views and components passes the allow-list |
default_locale_unprefixed |
/about serves the default locale |
second_locale_prefixed |
/en/about serves the translation |
hreflang_alternates_emitted |
one per published sibling plus x-default |
switcher_falls_back_to_home |
when the translation does not exist, no 404 |
dates_render_in_site_timezone_and_locale |
Bengali month name, Asia/Dhaka offset |
bengali_slug_transliterates |
and an existing slug is untouched when the option changes |
csv_export_bom |
Bengali survives an Excel-style read |
cache_is_keyed_per_locale |
/about and /en/about are separate cache entries |
KodePress documentation · generated from the Markdown sources by
tools/build-docs-site.py · internal preview, not indexed.