Skip to content
github-actions[bot] edited this page Jul 3, 2026 · 1 revision

Internationalization (i18n) — ɳTask Mobile

Status: Implemented — NselfI18nProvider wired, RTL layout + Hijri date support (T-P3-E6-W1-S2-T02, 2026-06-16)


Overview

ɳTask mobile uses @nself/i18n (i18next + react-i18next) for translations and I18nManager for RTL layout support. The i18n module lives at apps/mobile/src/i18n/index.ts.


Supported Locales

Locale Language Direction Status
en English LTR Full
ar Arabic RTL Full

Additional locales can be added via @nself/i18n locale files and expanding SUPPORTED_LOCALES in src/i18n/index.ts.


RTL Layout Support

RTL layout is handled by React Native's I18nManager.forceRTL(). When the device locale is Arabic (or any RTL locale — Hebrew/Farsi/Urdu), the entire layout coordinate system flips automatically without requiring conditional styles.

How it works

  1. initializeI18n() is called at module level in apps/mobile/src/app/index.tsx — before any component mounts.
  2. It detects the device locale via expo-localization.
  3. If the locale is RTL, I18nManager.forceRTL(true) is called.
  4. React Native's layout engine mirrors all flexDirection: 'row', marginStart, and paddingStart values automatically.
// apps/mobile/src/i18n/index.ts
import { I18nManager } from 'react-native';
import * as Localization from 'expo-localization';
import { initializeI18next } from '@nself/i18n';

export function initializeI18n(overrideLocale?: string): void {
  const locale = overrideLocale ?? detectLocale();
  const isRtl = RTL_LOCALES.has(locale);
  if (I18nManager.isRTL !== isRtl) {
    I18nManager.forceRTL(isRtl);
  }
  initializeI18next(locale as 'en');
}

Locale change at runtime

If a user changes locale in settings and the RTL state needs to flip, call:

import * as Updates from 'expo-updates';
initializeI18n(newLocale);
if (requiresReload) {
  await Updates.reloadAsync(); // Full app reload to apply new RTL layout
}

Note: A reload is needed because React Native layout engine reads I18nManager.isRTL at startup only.


Hijri (Islamic Umm al-Qura) Date Formatting

ɳTask displays due dates in the Hijri calendar when the app is running in Arabic RTL mode.

formatHijriDate(date, locale?)

import { formatHijriDate } from '../i18n';

// Arabic output: '١٩ ذو الحجة ١٤٤٧'
formatHijriDate(new Date('2026-06-16'), 'ar');

// English output: '19 Dhū al-Ḥijja 1447'
formatHijriDate(new Date('2026-06-16'), 'en');
  • Uses Intl.DateTimeFormat with calendar: 'islamic-umalqura'
  • Supported in Hermes engine (Expo SDK 53 / RN 0.79+)
  • Falls back to ISO YYYY-MM-DD if the calendar extension is unavailable (older CI environments)

Where Hijri dates appear

Component Location Condition
TaskCard Due date label When I18nManager.isRTL === true

To add Hijri dates to more components, import formatHijriDate and check I18nManager.isRTL:

import { formatHijriDate } from '../../i18n';
import { I18nManager } from 'react-native';

const displayDate = I18nManager.isRTL
  ? formatHijriDate(isoDate, 'ar')
  : isoDate;

Icon Flipping

Custom SVG icons that convey directionality (arrows, back buttons, etc.) must be mirrored for RTL. This is tracked as a TODO — see the icon TODO comments in the relevant screen files. React Native's built-in icons and system controls mirror automatically with I18nManager.forceRTL.


StyleSheet Notes

When I18nManager.forceRTL(true) is active, the layout engine mirrors the coordinate system:

  • marginLeft becomes the end-side margin (right in visual terms)
  • marginRight becomes the start-side margin (left in visual terms)
  • flexDirection: 'row' reverses automatically

Prefer marginStart/marginEnd and paddingStart/paddingEnd for directional spacing when you need explicit control. Physical marginLeft/marginRight values are acceptable inside flexDirection: 'row' containers since the container itself mirrors.


Translation Files

Locale JSON files live in apps/mobile/src/i18n/{locale}/:

src/i18n/
  en/
    common.json
    dates.json
    nav.json
    screens.json
  ar/
    common.json
    dates.json
    nav.json
    screens.json

All translations are loaded via @nself/i18n's initializeI18next(). Add new locales by:

  1. Adding a locale folder under src/i18n/
  2. Adding the locale code to SUPPORTED_LOCALES in src/i18n/index.ts
  3. Adding the locale resources to @nself/i18n/src/provider.tsx

Clone this wiki locally