-
Notifications
You must be signed in to change notification settings - Fork 0
Internationalization
Tanuki ships with a minimal, file-based translation system: a global t() helper plus one JSON dictionary per language in lang/, and an optional URL-prefix language switcher wired in by default but inert until used.
lang/
├── en.json
└── es.json
t('nav.home') // → "Home" (or "Inicio" if locale is es)
t('todo.count_summary', ['total' => 5]) // → replaces ":total" inside the string- The active language comes from
current_locale(): a session override if one is set, otherwiseAPP_LOCALEfrom.env. - Keys use dot notation to reach nested sections of the JSON file (
errors.404_title→{"errors": {"404_title": "..."}}). - If a key is missing in the active locale,
t()falls back to English; if it's missing there too, it returns the raw key — a missing translation is visible in the UI instead of breaking the page. - Dictionaries are loaded and cached once per request, so calling
t()many times has no extra cost. - Placeholders use a leading colon (
:name) and are replaced via the second argument.
cp lang/en.json lang/fr.json
# translate the values in lang/fr.jsonAdd fr to ACCEPTED_LANGUAGES (see below) if you want it selectable via URL, and give it a flag/label in locale_meta() (utils.php) if you want it in the switcher.
format_date($datetime) // 'en' → m/d/Y H:i, 'es' → d/m/Y H:iUnlike the base i18n system, the switcher is wired in by default but stays entirely inert until a /xx/ URL is visited or a flag is clicked — no overhead for a project that never uses it.
# .env
APP_LOCALE=en
ACCEPTED_LANGUAGES=en,es- Visiting a URL prefixed with a 2-letter code that has a matching dictionary (
/es/todo) sets that language for the session and strips the prefix before normal routing continues — no need to duplicate routes with a language segment. -
ACCEPTED_LANGUAGEScontrols which prefixes are enabled. A prefix whose dictionary exists but isn't listed there returns a 404 — deliberate: you can ship a dictionary you're still working on without exposing it. - Once chosen (URL prefix or flag), the language sticks in the session — other links don't need a prefix.
-
If
ACCEPTED_LANGUAGESisn't set at all, the switcher doesn't render and/xx/prefixes aren't treated as locale prefixes — the project is single-language, using onlyAPP_LOCALE. - Flags aren't hardcoded —
includes/head.phploops overaccepted_locales(), so removing a language from.envremoves its flag automatically. Adding a new language still needs one line inlocale_meta()(utils.php) for its flag emoji and label.
Known limitation: a route's first path segment can't share a name with an enabled language code that also has a matching dictionary file (e.g. avoid a route literally named /es) — the router treats any 2-letter segment with a matching lang/xx.json as a locale prefix before normal route matching.
includes/head.php sets this from current_locale() automatically.