DocsDeveloper

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.

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

  1. messages/<locale>.json holds all UI strings, namespaced (common.actions.save, nav.items.dashboard, etc.)
  2. The optilearn-locale cookie sets the active locale
  3. src/i18n/request.ts reads the cookie on every server request and loads the matching bundle
  4. Components use useTranslations() (client) or getTranslations() (server) from next-intl
  5. Bundles are statically imported in request.ts so 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

  1. Create messages/<code>.json — copy en.json and translate
  2. Add <code> to the locales tuple in src/i18n/config.ts
  3. Add a label to the common.language.<code> namespace in every existing bundle
  4. Add the option to the language switcher dropdown (UI component)
  5. Verify next-intl build 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

  1. Create lib/l10n/app_<code>.arb — copy app_en.arb and translate every key
  2. Run flutter gen-l10n to regenerate app_localizations.dart (or rely on flutter run's auto-regen)
  3. Add the locale to the supportedLocales list in MaterialApp
  4. Verify GlobalMaterialLocalizations.delegate ships 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.
  5. Add the option to the in-app language picker
  6. Test RTL if the language is right-to-left (Arabic, Hebrew) — Flutter handles this automatically given the locale, but check critical screens
Warning

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:

  1. PR adds new English keys
  2. Translator opens the PR and adds the same keys to hi.json / app_hi.arb
  3. 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.