Localization (i18n)
The API negotiates a locale per request and returns all human-readable strings
(error and success messages, validation messages, e-mail content) in that
locale. Translation never affects machine-readable fields — only message,
validation errors, and notification copy are localized.
Supported locales
The supported-locale allowlist is the single source of truth in
config/locales.php. As of this draft:
| Code | Name | Native name | Direction |
|---|---|---|---|
en | English | English | ltr |
tr | Turkish | Türkçe | ltr |
The default (and fallback) locale is en. Any locale outside the allowlist is
ignored and the request falls back to the default — unknown or injected locale
strings are never honoured.
Discovering locales (for clients)
Clients (the admin SPA, the docs site) read the list at runtime so language switchers stay in sync with the server. This endpoint is public, unauthenticated and tenant-agnostic:
GET /api/locales
{
"default": "en",
"active": "en",
"dir": "ltr",
"locales": [
{ "code": "en", "name": "English", "native_name": "English", "dir": "ltr" },
{ "code": "tr", "name": "Turkish", "native_name": "Türkçe", "dir": "ltr" }
]
}
default— the fallback locale.active— the locale this request negotiated (so a client can confirm the outcome of Accept-Language negotiation).dir— the text direction (ltr/rtl) of the active locale.locales[].dir— per-locale direction, so a front-end can setdocument.dir/ thedirHTML attribute. Direction metadata is provided for every locale up front so the layout is RTL-ready before any RTL language ships.
How the locale is chosen
For each request the server resolves the locale by this priority, restricted to the allowlist at every step:
- Authenticated user preference —
users.settings.locale. - Tenant default — the
localekey intenant_settings, when a tenant is bound. Accept-Languagenegotiation — the highest-ranked language tag in the header that is in the allowlist. Quality values (q=) and region subtags (tr-TR→tr) are honoured.- Fallback — the default locale (
en).
Examples
GET /api/locales
Accept-Language: tr,en;q=0.8
→ active is tr.
GET /api/locales
Accept-Language: de-DE,fr;q=0.9
→ neither de nor fr is supported → active falls back to en.
A failed login illustrates the localized body:
POST /api/login
Accept-Language: tr
{ "email": "x@y.test", "password": "wrong" }
{
"message": "...",
"errors": { "email": ["Bu kimlik bilgileri kayıtlarımızla eşleşmiyor."] }
}
Setting a persistent preference
Accept-Language is a per-request hint. To persist a choice, store it on the
user (settings.locale) or the tenant default (tenant_settings.locale); the
stored preference then takes precedence over Accept-Language on every request.
E-mails and queued notifications
Notification e-mails (verification, magic-link, password reset) are localized to the recipient's preferred locale, resolved at send time — not to the locale of whoever triggered the send. This matters for queued jobs, which run outside the request and re-resolve the locale from the target user/tenant.