---
name: i18n-patterns
description: Internationalization patterns for the Motadata frontend supporting English and Hindi. Covers translation key naming, namespace organization, pluralization, date/number/currency formatting with Intl API, lazy loading translation files, interpolation, and RTL-readiness. Use when generating components with user-facing strings or setting up i18n infrastructure.
---

# i18n Patterns

Internationalization for English (en) and Hindi (hi) in the Motadata frontend.

## When to Activate

- When adding any user-facing string to a component
- When setting up i18n infrastructure (react-i18next)
- When formatting dates, numbers, or currencies
- When creating translation files
- When reviewing components for hardcoded strings
- Used by: i18n-generator, page-generator, component-generator

## Critical Rule: No Hardcoded User-Facing Strings

Every string visible to users MUST go through i18n. The ONLY exception is text already handled by the motadata-react-library (component labels like "Close", "Clear").

```typescript
// BAD — hardcoded string
<h1>User Management</h1>
<Button>Save Changes</Button>
<p>No users found.</p>

// GOOD — translated
<h1>{t('users.title')}</h1>
<Button>{t('common.save')}</Button>
<p>{t('users.emptyState')}</p>
```

## Setup with react-i18next

```typescript
// src/frontend/i18n/config.ts
import i18n from 'i18next'
import { initReactI18next } from 'react-i18next'
import Backend from 'i18next-http-backend'
import LanguageDetector from 'i18next-browser-languagedetector'

i18n
  .use(Backend)
  .use(LanguageDetector)
  .use(initReactI18next)
  .init({
    fallbackLng: 'en',
    supportedLngs: ['en', 'hi'],
    defaultNS: 'common',
    ns: ['common'],

    interpolation: {
      escapeValue: false,  // React already escapes
    },

    backend: {
      loadPath: '/locales/{{lng}}/{{ns}}.json',
    },

    detection: {
      order: ['localStorage', 'navigator'],
      caches: ['localStorage'],
      lookupLocalStorage: 'i18n_lang',
    },

    react: {
      useSuspense: true,
    },
  })

export { i18n }
```

### Provider Setup

```typescript
// src/frontend/app/providers/I18nProvider.tsx
import { Suspense, type ReactNode } from 'react'
import { I18nextProvider } from 'react-i18next'
import { i18n } from '@/i18n/config'
import { Spinner } from 'motadata-react-library'

function I18nProvider({ children }: { children: ReactNode }) {
  return (
    <I18nextProvider i18n={i18n}>
      <Suspense fallback={<Spinner size="lg" className="mdt-mx-auto mdt-mt-20" />}>
        {children}
      </Suspense>
    </I18nextProvider>
  )
}

export { I18nProvider }
```

## Translation File Structure

```text
public/locales/
  ├── en/
  │   ├── common.json          # Shared: buttons, labels, errors
  │   ├── auth.json            # Auth feature strings
  │   ├── users.json           # Users feature strings
  │   ├── incidents.json       # Incidents feature strings
  │   └── settings.json        # Settings feature strings
  └── hi/
      ├── common.json
      ├── auth.json
      ├── users.json
      ├── incidents.json
      └── settings.json
```

## Translation Key Naming Convention

```json
// public/locales/en/common.json
{
  "save": "Save",
  "cancel": "Cancel",
  "delete": "Delete",
  "edit": "Edit",
  "create": "Create",
  "search": "Search",
  "loading": "Loading...",
  "noResults": "No results found",
  "confirmDelete": "Are you sure you want to delete this?",
  "error": {
    "generic": "Something went wrong. Please try again.",
    "network": "Unable to connect. Check your internet connection.",
    "notFound": "The requested resource was not found.",
    "unauthorized": "Your session has expired. Please log in again."
  },
  "pagination": {
    "showing": "Showing {{from}} to {{to}} of {{total}}",
    "next": "Next",
    "previous": "Previous"
  }
}
```

```json
// public/locales/en/users.json
{
  "title": "User Management",
  "createUser": "Create User",
  "editUser": "Edit User",
  "deleteUser": "Delete User",
  "fields": {
    "name": "Full Name",
    "email": "Email Address",
    "role": "Role",
    "status": "Status"
  },
  "roles": {
    "admin": "Administrator",
    "operator": "Operator",
    "viewer": "Viewer"
  },
  "status": {
    "active": "Active",
    "inactive": "Inactive",
    "suspended": "Suspended"
  },
  "emptyState": "No users found. Create your first user to get started.",
  "deleteConfirm": "Are you sure you want to delete {{name}}? This action cannot be undone.",
  "created": "User {{name}} created successfully",
  "updated": "User updated successfully",
  "deleted": "User deleted successfully"
}
```

```json
// public/locales/hi/users.json
{
  "title": "उपयोगकर्ता प्रबंधन",
  "createUser": "उपयोगकर्ता बनाएं",
  "editUser": "उपयोगकर्ता संपादित करें",
  "deleteUser": "उपयोगकर्ता हटाएं",
  "fields": {
    "name": "पूरा नाम",
    "email": "ईमेल पता",
    "role": "भूमिका",
    "status": "स्थिति"
  },
  "roles": {
    "admin": "प्रशासक",
    "operator": "ऑपरेटर",
    "viewer": "दर्शक"
  },
  "status": {
    "active": "सक्रिय",
    "inactive": "निष्क्रिय",
    "suspended": "निलंबित"
  },
  "emptyState": "कोई उपयोगकर्ता नहीं मिला। शुरू करने के लिए अपना पहला उपयोगकर्ता बनाएं।",
  "deleteConfirm": "क्या आप वाकई {{name}} को हटाना चाहते हैं? यह क्रिया पूर्ववत नहीं की जा सकती।",
  "created": "उपयोगकर्ता {{name}} सफलतापूर्वक बनाया गया",
  "updated": "उपयोगकर्ता सफलतापूर्वक अपडेट किया गया",
  "deleted": "उपयोगकर्ता सफलतापूर्वक हटाया गया"
}
```

### Key Naming Rules

```text
Namespace:   Feature name (auth, users, incidents, common)
Top level:   Page or section name (title, createUser, fields)
Nested:      Group related keys (fields.name, error.generic)
Casing:      camelCase for all keys
Interpolation: {{variableName}} — double curly braces

Naming pattern:
  {feature}.{context}.{key}
  users.fields.name
  users.status.active
  common.error.network
```

## Usage in Components

### Basic Translation

```typescript
import { useTranslation } from 'react-i18next'

function UsersPage() {
  const { t } = useTranslation('users')

  return (
    <PageHeader>
      <PageHeaderTitle>{t('title')}</PageHeaderTitle>
      <Button>{t('createUser')}</Button>
    </PageHeader>
  )
}
```

### Multiple Namespaces

```typescript
function UserForm() {
  const { t } = useTranslation(['users', 'common'])

  return (
    <form>
      <label>{t('users:fields.name')}</label>
      <Button type="submit">{t('common:save')}</Button>
      <Button type="button" variant="secondary">{t('common:cancel')}</Button>
    </form>
  )
}
```

### Interpolation

```typescript
// Key: "deleteConfirm": "Are you sure you want to delete {{name}}?"
t('deleteConfirm', { name: user.name })

// Key: "showing": "Showing {{from}} to {{to}} of {{total}}"
t('common:pagination.showing', { from: 1, to: 25, total: 142 })
```

### Pluralization

```json
// en/users.json
{
  "selectedCount_one": "{{count}} user selected",
  "selectedCount_other": "{{count}} users selected"
}
```

```typescript
t('selectedCount', { count: selectedRows.length })
// 1 → "1 user selected"
// 5 → "5 users selected"
```

## Date/Number/Currency Formatting

Use the built-in `Intl` API — no external library needed.

```typescript
// src/frontend/utils/formatters.ts

function formatDate(date: Date | string, locale: string = 'en'): string {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
  }).format(new Date(date))
}

function formatDateTime(date: Date | string, locale: string = 'en'): string {
  return new Intl.DateTimeFormat(locale, {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    hour: '2-digit',
    minute: '2-digit',
  }).format(new Date(date))
}

function formatRelativeTime(date: Date | string, locale: string = 'en'): string {
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: 'auto' })
  const diff = Date.now() - new Date(date).getTime()
  const seconds = Math.floor(diff / 1000)

  if (seconds < 60) return rtf.format(-seconds, 'second')
  if (seconds < 3600) return rtf.format(-Math.floor(seconds / 60), 'minute')
  if (seconds < 86400) return rtf.format(-Math.floor(seconds / 3600), 'hour')
  return rtf.format(-Math.floor(seconds / 86400), 'day')
}

function formatNumber(value: number, locale: string = 'en'): string {
  return new Intl.NumberFormat(locale).format(value)
}

function formatCurrency(value: number, currency: string = 'USD', locale: string = 'en'): string {
  return new Intl.NumberFormat(locale, {
    style: 'currency',
    currency,
  }).format(value)
}
```

### Usage with i18n Locale

```typescript
function UserCreatedAt({ date }: { date: string }) {
  const { i18n } = useTranslation()
  return <time dateTime={date}>{formatDateTime(date, i18n.language)}</time>
}
```

## Language Switcher

```typescript
function LanguageSwitcher() {
  const { i18n } = useTranslation()

  const languages = [
    { code: 'en', label: 'English' },
    { code: 'hi', label: 'हिन्दी' },
  ] as const

  return (
    <Select
      value={i18n.language}
      onValueChange={(lang) => i18n.changeLanguage(lang)}
      aria-label="Select language"
    >
      {languages.map((lang) => (
        <SelectItem key={lang.code} value={lang.code}>
          {lang.label}
        </SelectItem>
      ))}
    </Select>
  )
}
```

## Lazy Loading Translations

Namespace-based lazy loading is built into the `i18next-http-backend` config. Feature namespaces are loaded only when the feature route is accessed.

```typescript
// Load feature namespace on route entry
const UsersPage = lazy(async () => {
  // Pre-load the namespace before rendering
  await i18n.loadNamespaces('users')
  return import('@/features/users/pages/UsersListPage')
})
```

## Anti-Patterns

```typescript
// NEVER: Hardcode user-facing strings
<Button>Save</Button>  // BAD
<Button>{t('common:save')}</Button>  // GOOD

// NEVER: Concatenate translated strings
t('hello') + ' ' + t('world')  // BAD — word order varies by language
t('greeting', { name: 'John' })  // GOOD — let the translator handle order

// NEVER: Use sentence fragments as keys
t('the') + ' ' + t('user')  // BAD
t('theUser')  // GOOD — full phrases as keys

// NEVER: Embed HTML in translations
{ "title": "<strong>Users</strong>" }  // BAD — mixing concerns
// GOOD — use Trans component for rich text
<Trans i18nKey="richTitle" components={{ bold: <strong /> }} />

// NEVER: Format dates with string concatenation
`${date.getMonth()}/${date.getDate()}`  // BAD — locale-dependent
formatDate(date, i18n.language)  // GOOD — Intl API handles locale

// NEVER: Use numbers directly
<p>{count} items</p>  // BAD — no pluralization
<p>{t('itemCount', { count })}</p>  // GOOD — handles plurals
```

## Common Mistakes

1. **Hardcoding user-facing strings** — Every string visible to users must go through `t()`. Even "Cancel", "Save", "OK" must use `t('common:cancel')`. Hardcoded strings are a code review BLOCKER because they break Hindi translation completely.

2. **Concatenating translated strings** — `t('hello') + ' ' + t('world')` assumes English word order. Hindi and other languages may have different word order. Always use full phrases with interpolation: `t('greeting', { name })`.

3. **Formatting dates with string concatenation** — `${date.getMonth()}/${date.getDate()}` produces US-format dates regardless of locale. Use `Intl.DateTimeFormat` with the current i18n language so dates display correctly for both English and Hindi users.

4. **Embedding HTML in translation strings** — `"title": "<strong>Users</strong>"` mixes concerns and risks XSS. Use the `<Trans>` component with `components` prop for rich text: `<Trans i18nKey="richTitle" components={{ bold: <strong /> }} />`.

5. **Forgetting to load feature namespaces** — Each feature's translations are lazy-loaded. If you use `t('users:title')` without ensuring the `users` namespace is loaded, it renders the raw key. Pre-load namespaces in the lazy route: `await i18n.loadNamespaces('users')`.
