# Internationalization (i18n)

Multi-language support using `i18next` and `react-i18next`. Provides interpolation, pluralization, and dynamic language switching.

## Setup

### Dependencies

`i18next` and `react-i18next` are included in the base template `package.json`. No additional install needed.

### File Structure

```
src/
├── config/
│   └── i18nConfig.ts       # i18n configuration (uses configureI18n)
└── locales/
    ├── en.json             # English translations
    └── es.json             # Spanish translations
```

### Step 2: Define English Translations

```json
// src/locales/en.json
{
  "common": {
    "save": "Save",
    "cancel": "Cancel",
    "delete": "Delete",
    "edit": "Edit",
    "search": "Search",
    "loading": "Loading...",
    "error": "An error occurred",
    "success": "Operation successful"
  },
  "auth": {
    "login": "Log In",
    "logout": "Log Out",
    "email": "Email",
    "password": "Password",
    "forgotPassword": "Forgot password?",
    "loginError": "Invalid email or password"
  },
  "policy": {
    "title": "Policy",
    "titlePlural": "Policies",
    "number": "Policy Number",
    "type": "Policy Type",
    "status": "Status",
    "premium": "Premium",
    "startDate": "Start Date",
    "endDate": "End Date",
    "createNew": "Create New Policy",
    "editPolicy": "Edit Policy",
    "deleteConfirm": "Are you sure you want to delete this policy?",
    "active": "Active",
    "cancelled": "Cancelled",
    "expired": "Expired"
  },
  "claim": {
    "title": "Claim",
    "titlePlural": "Claims",
    "number": "Claim Number",
    "amount": "Claim Amount",
    "status": "Status",
    "date": "Claim Date",
    "description": "Description",
    "submit": "Submit Claim",
    "pending": "Pending",
    "approved": "Approved",
    "rejected": "Rejected",
    "itemCount": "{count} claim",
    "itemCountPlural": "{count} claims"
  },
  "validation": {
    "required": "{field} is required",
    "invalidEmail": "Invalid email address",
    "minLength": "{field} must be at least {min} characters",
    "maxLength": "{field} must not exceed {max} characters",
    "invalidPhone": "Invalid phone number",
    "invalidDate": "Invalid date"
  },
  "errors": {
    "network": "Network error. Please check your connection.",
    "unauthorized": "Your session has expired. Please log in again.",
    "forbidden": "You don't have permission to perform this action.",
    "notFound": "The requested resource was not found.",
    "serverError": "A server error occurred. Please try again later."
  }
}
```

### Step 3: Define Spanish Translations

`es.json` follows the **identical key structure** as `en.json` with translated values. Example snippet:

```json
// src/locales/es.json — same keys as en.json, only values differ
{
  "common": { "save": "Guardar", "cancel": "Cancelar", "loading": "Cargando..." },
  "policy": { "title": "Póliza", "titlePlural": "Pólizas", "active": "Activa" },
  "claim": { "itemCount": "{count} reclamo", "itemCountPlural": "{count} reclamos" }
}
```

### ⚠️ CRITICAL: Both JSON files MUST have identical structure

```typescript
// ❌ WRONG - Mismatched keys
// en.json: { "common": { "save": "Save" } }
// es.json: { "common": { "guardar": "Guardar" } }  // Key name doesn't match!

// ✅ CORRECT - Same keys, different values
// en.json: { "common": { "save": "Save" } }
// es.json: { "common": { "save": "Guardar" } }
```

### Step 4: Configure i18n with `configureI18n`

**IMPORTANT:** Always use `configureI18n` from `@dynamic-framework/ui-react` instead of raw `i18next.init()`. This wrapper sets up the correct defaults for the Dynamic Framework.

```typescript
// src/config/i18nConfig.ts
import { configureI18n } from '@dynamic-framework/ui-react';

import en from '../locales/en.json';
import es from '../locales/es.json';

import { SITE_LANG } from './widgetConfig';

const resources = {
  es: { translation: es },
  en: { translation: en },
};

configureI18n(resources, { lng: SITE_LANG });

export const changeLanguage = (lang: keyof typeof resources) => {
  configureI18n(resources, { lng: lang });
};
```

**Key differences from raw i18next:**
- `configureI18n(resources, options)` — two separate arguments, not one config object
- Language comes from `SITE_LANG` (parsed from Liquid template variable `{{site.language}}`)
- No `LanguageDetector` — the portal controls the language
- No manual `i18n.use(initReactI18next)` — handled internally

### Step 5: Initialize in App

```typescript
// src/main.tsx
import { DContextProvider } from '@dynamic-framework/ui-react';
import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';

import './config/i18nConfig';  // ⚠️ Import BEFORE App

import App from './App';

import '@dynamic-framework/ui-react/dist/css/dynamic-ui.css';
import './styles/base.scss';

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <DContextProvider>
      <App />
    </DContextProvider>
  </StrictMode>,
);
```

## Usage Patterns

### Pattern 1: Basic Translation with useTranslation Hook

```typescript
// src/components/PolicyList.tsx
import { useTranslation } from 'react-i18next';

export const PolicyList = () => {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('policy.titlePlural')}</h1>
      <DButton>{t('policy.createNew')}</DButton>
    </div>
  );
};
```

### Pattern 2: Interpolation

```typescript
// src/components/ValidationError.tsx
import { useTranslation } from 'react-i18next';

export const ValidationError = ({ field, minLength }: Props) => {
  const { t } = useTranslation();

  return (
    <div className="error">
      {t('validation.minLength', { field, min: minLength })}
    </div>
  );
};

// Renders: "Email must be at least 8 characters" (en)
// Renders: "Correo electrónico debe tener al menos 8 caracteres" (es)
```

### Pattern 3: Pluralization

```typescript
// src/components/ClaimCount.tsx
import { useTranslation } from 'react-i18next';

export const ClaimCount = ({ count }: { count: number }) => {
  const { t } = useTranslation();

  return (
    <p>
      {t('claim.itemCount', { count })}
    </p>
  );
};

// count = 1: "1 claim" (en) / "1 reclamo" (es)
// count = 5: "5 claims" (en) / "5 reclamos" (es)
```

### Pattern 4: Language Switcher

```typescript
const { i18n } = useTranslation();
// Switch language: i18n.changeLanguage('es');
// Current language: i18n.language
```

### Pattern 5: Translation in Non-Component Code

```typescript
// src/utils/validators.ts
import i18n from '@/config/i18nConfig';

export function validateEmail(email: string): string | null {
  const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);

  if (!isValid) {
    return i18n.t('validation.invalidEmail');
  }

  return null;
}
```

### Pattern 6: Dynamic Translation Keys

```typescript
// src/components/PolicyStatus.tsx
import { useTranslation } from 'react-i18next';

export const PolicyStatus = ({ status }: { status: string }) => {
  const { t } = useTranslation();

  return (
    <DBadge color={getStatusColor(status)}>
      {t(`policy.${status}`)}
    </DBadge>
  );
};

// status = 'active': "Active" (en) / "Activa" (es)
// status = 'expired': "Expired" (en) / "Vencida" (es)
```

### Pattern 7: Translation with Components

```typescript
// src/components/DeleteConfirmation.tsx
import { useTranslation, Trans } from 'react-i18next';

export const DeleteConfirmation = ({ policyNumber }: Props) => {
  const { t } = useTranslation();

  return (
    <DModal>
      <Trans
        i18nKey="policy.deleteConfirmWithNumber"
        values={{ number: policyNumber }}
        components={{
          bold: <strong />,
          break: <br />
        }}
      />
    </DModal>
  );
};

// en.json: "Are you sure you want to delete policy <bold>{number}</bold>?<break/>This action cannot be undone."
// Renders: "Are you sure you want to delete policy **POL-001**?\nThis action cannot be undone."
```

## Advanced Patterns

### Pattern 1: Namespace Organization

```typescript
// src/config/i18nConfig.ts
import { configureI18n } from '@dynamic-framework/ui-react';
import { SITE_LANG } from './widgetConfig';

import enCommon from '../locales/en/common.json';
import enPolicy from '../locales/en/policy.json';
import esCommon from '../locales/es/common.json';
import esPolicy from '../locales/es/policy.json';

const resources = {
  en: { common: enCommon, policy: enPolicy },
  es: { common: esCommon, policy: esPolicy },
};

configureI18n(resources, {
  lng: SITE_LANG,
  defaultNS: 'common',
  ns: ['common', 'policy'],
});

// Usage with namespace
const { t } = useTranslation('policy');
t('title');  // Looks in policy namespace
```

### Pattern 2: Date/Number Formatting

```typescript
// src/components/PolicyDetails.tsx
import { useTranslation } from 'react-i18next';

export const PolicyDetails = ({ policy }: Props) => {
  const { t, i18n } = useTranslation();

  const formattedPremium = new Intl.NumberFormat(i18n.language, {
    style: 'currency',
    currency: 'USD'
  }).format(policy.premium);

  const formattedDate = new Intl.DateTimeFormat(i18n.language).format(
    new Date(policy.startDate)
  );

  return (
    <div>
      <p>{t('policy.premium')}: {formattedPremium}</p>
      <p>{t('policy.startDate')}: {formattedDate}</p>
    </div>
  );
};

// en: "Premium: $1,234.56" / "Start Date: 12/31/2023"
// es: "Prima: $1,234.56" / "Fecha de Inicio: 31/12/2023"
```

### Pattern 3: Context-Based Translations

```json
// en.json
{
  "friend": "A friend",
  "friend_male": "A boyfriend",
  "friend_female": "A girlfriend"
}
```

```typescript
const { t } = useTranslation();

t('friend', { context: 'male' });    // "A boyfriend"
t('friend', { context: 'female' });  // "A girlfriend"
t('friend');                         // "A friend"
```

### Pattern 4: Lazy Loading Translations

> Rarely needed in widgets. For very large translation sets, use `i18next-http-backend` with raw `i18next.init()` instead of `configureI18n`.

---

## Default behavior: single-brace interpolation

`@dynamic-framework/ui-react` re-exports i18next via `configureI18n()` with the interpolation delimiters overridden to single-brace:

```js
// internal to @dynamic-framework/ui-react bundle
interpolation: {
  escapeValue: false,
  prefix: '{',
  suffix: '}',
}
```

This avoids collision with Liquid templates (`{{ liquid_var }}` processed server-side by Modyo Channels) without requiring developers to escape interpolations.

**Consequence for widget code:**

- ✅ Locale JSON uses single-brace: `{ "greeting": "Hello {name}" }`
- ✅ Call site uses standard i18next signature: `t('greeting', { name: 'Alice' })`
- ❌ Double-brace `{{name}}` is NOT interpolated.
- ❌ Liquid syntax `{{ liquid_var }}` in locale JSON is NOT interpolated by i18next (correctly — Liquid is server-side).

**Concrete example:**

```json
// src/locales/en.json
{
  "examples": {
    "entityCreate": {
      "title": "Create {type}"
    }
  }
}
```

```tsx
// component
{t('examples.entityCreate.title', { type: 'Account' })}
// renders: "Create Account"
```

**Common mistake:**

If you copy widget snippets from i18next documentation directly (which uses `{{var}}` by default), the interpolation will silently fail at runtime — the string renders literally with the braces visible. Convert any `{{var}}` to single-brace `{var}`.

This applies to all widgets generated against `@dynamic-framework/ui-react`, regardless of version (≥ 2.0).

### Pluralización con sintaxis nativa de i18next

i18next sigue resolviendo plurales por sufijo de key (`_one`, `_other`, `_zero`) usando el `count` que pasás. La interpolación dentro del valor también usa single-brace:

```json
// locales/en.json
{
  "accounts": {
    "count_zero": "No accounts",
    "count_one": "{count} account",
    "count_other": "{count} accounts"
  }
}
```

```tsx
t('accounts.count', { count: accounts.length })
// 0 → "No accounts"
// 1 → "1 account"
// 5 → "5 accounts"
```

---

## Cuándo no usar i18n

Aunque i18n cubre la inmensa mayoría de strings user-facing, hay casos donde no agregar una key es la decisión correcta. **Estos son strings puramente técnicos**, no excepciones a la interpolación nativa.

**Casos típicos:**

- IDs internos, slugs, endpoints, header names: `useFetch('/api/accounts')` — no es UI.
- Console logs / mensajes para developers: `console.error('Failed to load')` — no se muestra a usuarios finales.
- Texto de testing fixtures o seed data.
- Etiquetas de telemetría / event tracking: `track('button_clicked', { id: 'save' })` — payload de datos, no UI.

**Para texto user-facing**, siempre usá la interpolación nativa de i18next (single-brace). No la sustituyas por concatenación en el componente — la concatenación rompe el orden de palabras en idiomas con sintaxis distinta y dificulta la pluralización:

```tsx
// ❌ Evitar para strings traducibles
<p>{accounts.length} {t('balance.activeAccountsLabel')}</p>

// ✅ Preferir
{t('balance.activeAccountsCount', { count: accounts.length })}
// con la key "activeAccountsCount": "{count} cuentas activas"
```

---

## Testing with i18n

Create a `renderWithI18n` helper that wraps components in `I18nextProvider` with minimal test translations:

```typescript
// src/test-utils/i18n-test.tsx
import { render, RenderOptions } from '@testing-library/react';
import { I18nextProvider } from 'react-i18next';
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n.use(initReactI18next).init({
  lng: 'en', fallbackLng: 'en',
  resources: { en: { translation: { 'policy.title': 'Policy', 'common.save': 'Save' } } }
});

export function renderWithI18n(ui: React.ReactElement, options?: RenderOptions) {
  return render(ui, {
    wrapper: ({ children }) => <I18nextProvider i18n={i18n}>{children}</I18nextProvider>,
    ...options
  });
}

// Usage: renderWithI18n(<PolicyList />); expect(getByText('Policy')).toBeInTheDocument();
```

## Best Practices

1. **Hierarchical keys**: `{ "policy": { "title": "Policy" } }` not `{ "policyTitle": "Policy" }`
2. **Use interpolation**: `t('greeting', { name })` not `t('hello') + name`
3. **Match keys across languages**: Same structure in en.json and es.json
4. **Use built-in pluralization**: `"itemCount"` / `"itemCountPlural"` keys
5. **Extract ALL user-facing text**: `<DButton>{t('common.save')}</DButton>` not `<DButton>Save</DButton>`

## Common Mistakes

### ❌ Mistake 1: Initializing i18n After App Render

```typescript
// ❌ WRONG
import App from './App';
import './config/i18nConfig';  // Too late!

// ✅ CORRECT
import './config/i18nConfig';  // Initialize FIRST
import App from './App';
```

### ❌ Mistake 2: Missing Translation Keys

```typescript
// ❌ WRONG - Key doesn't exist
t('policy.nonExistentKey')  // Returns "policy.nonExistentKey"

// ✅ CORRECT - Verify key exists in JSON
{
  "policy": {
    "title": "Policy"
  }
}
t('policy.title')  // Returns "Policy"
```

### ❌ Mistake 3: Inline String Concatenation

```typescript
// ❌ WRONG
const message = `${t('hello')} ${userName}!`;

// ✅ CORRECT
{
  "greeting": "Hello {name}!"
}
t('greeting', { name: userName })
```

Both `en.json` and `es.json` MUST have identical key structure with only the values translated.
