Internationalization
next-intl on web, flutter_localizations on mobile, adding a new language
OptiLearn ships an English + Hindi UI. Web and mobile use different stacks; this page covers both and how to add a third language.
Web — next-intl, cookie-driven
next-intl with a cookie-driven locale strategy — there is no /en/... /hi/... URL prefix. Existing URLs and SSO callbacks keep working unchanged.
Files
src/i18n/
├── config.ts # locale list + cookie name
└── request.ts # next-intl request config (reads cookie, loads bundle)
messages/
├── en.json # English bundle
└── hi.json # Hindi bundle
How it works
messages/<locale>.jsonholds all UI strings, namespaced (common.actions.save,nav.items.dashboard, etc.)- The
optilearn-localecookie sets the active locale src/i18n/request.tsreads the cookie on every server request and loads the matching bundle- Components use
useTranslations()(client) orgetTranslations()(server) fromnext-intl - Bundles are statically imported in
request.tsso the build emits both — no filesystem IO at request time
Switching language
The language switcher writes the cookie client-side. The cookie is scoped to OptiLearn's host — it does NOT leak into OptiCRM, which manages its own locale separately.
// src/i18n/config.ts
export const locales = ["en", "hi"] as const;
export const defaultLocale = "en";
export const LOCALE_COOKIE_NAME = "optilearn-locale";
Message format
next-intl supports ICU MessageFormat — including plurals:
{
"stats": {
"inProgress": "{count, plural, =0 {No courses in progress} =1 {1 in progress} other {# in progress}}"
}
}
const t = useTranslations("dashboard.stats");
t("inProgress", { count: 3 }); // "3 in progress"
Mobile — flutter_localizations, ARB files
The Flutter app uses Flutter's first-party localization stack:
lib/
└── l10n/
├── app_en.arb # English ARB
├── app_hi.arb # Hindi ARB
└── (generated) app_localizations.dart
flutter_localizations provides Material/Cupertino widget strings; ARB files own app strings. The locale picker on the mobile Settings screen writes to local storage and rebuilds the MaterialApp with the new locale.
Adding a new language
Web
- Create
messages/<code>.json— copyen.jsonand translate - Add
<code>to thelocalestuple insrc/i18n/config.ts - Add a label to the
common.language.<code>namespace in every existing bundle - Add the option to the language switcher dropdown (UI component)
- Verify
next-intlbuild picks up the new bundle (it's a static import path)
That's it — the cookie strategy means no URL routing changes are needed.
Mobile
- Create
lib/l10n/app_<code>.arb— copyapp_en.arband translate every key - Run
flutter gen-l10nto regenerateapp_localizations.dart(or rely onflutter run's auto-regen) - Add the locale to the
supportedLocaleslist inMaterialApp - Verify
GlobalMaterialLocalizations.delegateships translations for the new language code — most major languages are covered out of the box. Check the Flutter localization support list before promising the language to users. - Add the option to the in-app language picker
- Test RTL if the language is right-to-left (Arabic, Hebrew) — Flutter handles this automatically given the locale, but check critical screens
Don't ship a language whose GlobalMaterialLocalizations support is missing — every Material widget (date pickers, dialogs, snack bars) will fall back to English and the app will look half-translated.
Translation workflow
There's no translation management system wired up yet. Current workflow:
- PR adds new English keys
- Translator opens the PR and adds the same keys to
hi.json/app_hi.arb - Reviewer sanity-checks both bundles have the same key set before merging
For larger teams, consider hooking Lokalise / Crowdin / POEditor in front of the JSON / ARB files.
Related
- Web message format: next-intl docs
- Mobile l10n: Flutter internationalization
- i18n Admin Setup — how an admin enables the picker and which languages are live