Localization
The login UI ships ten locales out of the box: English, Simplified Chinese (zh-Hans), German (de), French (fr), Spanish (es), Vietnamese (vi), Portuguese (pt), Arabic (ar), Afrikaans (af) and Hindi (hi). Server API responses are localized in the first seven of these. Localization covers the server API responses, the login UI, and this documentation site.
Supported Languages
| Code | Language | Login UI | Server API |
|---|---|---|---|
en |
English (default) | ✓ | ✓ |
zh-Hans |
Simplified Chinese | ✓ | ✓ |
de |
German | ✓ | ✓ |
fr |
French | ✓ | ✓ |
es |
Spanish | ✓ | ✓ |
vi |
Vietnamese | ✓ | ✓ |
pt |
Portuguese | ✓ | ✓ |
ar |
Arabic (right-to-left) | ✓ | — |
af |
Afrikaans | ✓ | — |
hi |
Hindi | ✓ | — |
Server (API Responses)
The server uses ASP.NET Core’s built-in localization with IStringLocalizer<T> and .resx resource files. The language is selected from the Accept-Language HTTP header.
What is localized
- Password validation error messages
- Password policy labels (
GET /api/auth/password-policy) - Password reset flow messages (token errors, expiration, success)
- Generic error descriptions from the exception handling middleware
- Admin user management messages (email confirmation, verification, etc.)
- End session confirmation message
What is NOT localized
- Machine-readable
errorcodes ("email_required","invalid_credentials", etc.), these are API contracts and remain constant - OAuth/OIDC error codes and developer-facing error descriptions on token, authorize, and revocation endpoints
- Internal log messages and exception messages
Testing server localization
Send an Accept-Language header to any localized endpoint:
# English (default)
curl https://auth.example.com/api/auth/password-policy
# Simplified Chinese
curl -H "Accept-Language: zh-Hans" https://auth.example.com/api/auth/password-policy
# German
curl -H "Accept-Language: de" https://auth.example.com/api/auth/password-policy
Resource files
All server translation strings are in .resx files under src/Authagonal.Server/Resources/:
Resources/
SharedMessages.cs # Marker class
SharedMessages.resx # English (default)
SharedMessages.zh-Hans.resx
SharedMessages.de.resx
SharedMessages.fr.resx
SharedMessages.es.resx
SharedMessages.vi.resx
SharedMessages.pt.resx
Login UI
The login SPA uses react-i18next for client-side localization. Language is auto-detected from the browser’s navigator.language setting.
The registered locales live in a single LANGUAGES registry in login-app/src/i18n/index.ts, which drives both the i18next resource registration and every language picker, so the two can’t drift. Every registered locale currently appears in the default picker. DEFAULT_LANGUAGES is exported separately from LANGUAGES so that a future restricted locale could be excluded from pickers without touching call sites, but nothing is excluded today. Tenants can also narrow the picker the same way: a languages array in branding.json replaces the default list entirely (see Branding).
The active language is mirrored onto <html lang> and <html dir>, so right-to-left languages (ar) flip the auth card automatically, including when the language is switched in place via the picker.
Language detection
The detection order is:
- localStorage: persisted preference from a previous visit
- Query parameter:
?lng=deoverrides browser detection - Browser language:
navigator.language(automatic) - Fallback: English (
en)
Translation files
Translation JSON files are bundled with the app at login-app/src/i18n/:
i18n/
index.ts # i18n initialization + the LANGUAGES registry
en.json # English
zh-Hans.json # Simplified Chinese
de.json # German
fr.json # French
es.json # Spanish
vi.json # Vietnamese
pt.json # Portuguese
ar.json # Arabic
af.json # Afrikaans
hi.json # Hindi
Password policy labels
The reset-password page translates its password requirement checklist client-side based on the rule key returned by GET /api/auth/password-policy (falling back to the server-provided label for unrecognized rules). This ensures the requirements follow the language selected in the UI, even if the browser’s Accept-Language header differs. The registration page displays the server-provided label values, which are localized from Accept-Language.
npm package consumers
If you consume the login app via @authagonal/login, the i18n instance is exported:
import { i18n } from '@authagonal/login';
// Change language programmatically
i18n.changeLanguage('de');
Documentation
The docs site uses a directory-based approach. English pages are at the root, and translations are in locale subdirectories (/zh-Hans/, /de/, /fr/, /es/, /vi/, /pt/). A language switcher dropdown in the sidebar allows switching between languages.
Adding a New Language
To add support for a new language (e.g., Japanese ja):
1. Server
Create a new .resx file by copying the English one and translating the values:
src/Authagonal.Server/Resources/SharedMessages.ja.resx
Add "ja" to the supported cultures array in AuthagonalExtensions.cs:
var supportedCultures = new[] { "en", "zh-Hans", "de", "fr", "es", "vi", "pt", "ja" };
2. Login UI
Create a new translation JSON file by copying en.json and translating the values:
login-app/src/i18n/ja.json
Register it in the LANGUAGES array in login-app/src/i18n/index.ts. That one entry registers the i18next resource and adds the language to every picker:
import ja from './ja.json';
// In the LANGUAGES array:
{ code: 'ja', label: '日本語', resource: ja },
3. Documentation
Create a new directory with translated markdown files:
docs/ja/
index.md
installation.md
quickstart.md
...
Add a locale default in docs/_config.yml:
defaults:
- scope:
path: "ja"
values:
locale: "ja"
Add the language option to the switcher in docs/_layouts/default.html.
Adding New Strings
Server
- Add the key and English value to
SharedMessages.resx - Add translated values to each locale’s
.resxfile - Use
IStringLocalizer<SharedMessages>to access the string:
// Inject via parameter
IStringLocalizer<SharedMessages> localizer
// Use with key
localizer["MyNewKey"].Value
// With format parameters
string.Format(localizer["MyNewKey"].Value, param1)
Login UI
- Add the key and English value to
en.json - Add translated values to each locale’s JSON file
- Use the
t()function in components:
const { t } = useTranslation();
// Simple string
<p>{t('myNewKey')}</p>
// With interpolation
<p>{t('myNewKey', { name: 'value' })}</p>