# Internationalization (i18n) — Xertica UI

Xertica UI projects use **`i18next`** with the **`react-i18next`** binding for all UI string translations. The `LanguageSelector` component is wired directly to `i18next` — changing the language immediately updates every `useTranslation()` consumer in the app and invalidates the React Query cache so that data-layer strings (mock API responses) also refresh.

---

## Setup

### 1. Install

```bash
npm install i18next react-i18next
```

Both packages are listed in `templates/package.json` and are installed automatically by `npx xertica-ui@latest init`.

### 2. Create locale files

Locales are organized as one **folder per language**, split by category:

```
src/locales/
├── .languages.json               ← CLI-managed selection ({ "version": 1, "codes": [...] })
├── pt-BR/                        ← default & fallback (only your selected languages are present)
│   ├── common.json               ← shared action labels (view, edit, save, cancel…)
│   ├── nav.json                  ← navigation labels
│   ├── errors.json               ← error boundary UI
│   ├── languageSelector.json     ← language picker UI
│   ├── themeToggle.json          ← theme toggle UI
│   ├── pages/
│   │   ├── home.json
│   │   ├── templates.json
│   │   ├── login.json
│   │   ├── resetPassword.json
│   │   ├── verifyEmail.json
│   │   ├── loginTemplate.json    ← starter template pages
│   │   ├── formTemplate.json
│   │   ├── dashboardTemplate.json
│   │   └── crudTemplate.json
│   └── components/
│       ├── assistant.json
│       ├── sidebar.json
│       ├── media.json
│       ├── projectCard.json
│       ├── profileCard.json
│       ├── notificationCard.json
│       ├── activityCard.json
│       ├── stats.json
│       └── team.json
├── en/    ← same structure
└── es/    ← same structure
```

Each JSON file contains only the keys for its category (no top-level wrapper key). For example, `locales/pt-BR/common.json`:

```json
{ "view": "Visualizar", "edit": "Editar", "loading": "Carregando...", "cancel": "Cancelar" }
```

And `locales/pt-BR/pages/home.json`:

```json
{ "welcome": "Bem-vindo ao Design System!", "subtitle": "...", "templateCliTitle": "Template CLI" }
```

> All files under `locales/<lang>/` are automatically discovered by `import.meta.glob` in `i18n.ts`. Adding a new JSON file requires no changes to `i18n.ts` — Vite picks it up on the next build.

### 3. Create `src/i18n.ts`

```ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

// Merges the split JSON files (pages/, components/) into one flat bundle per language.
// The key is the file basename — folder (pages/components) is discarded.
// Adding a new JSON file under locales/<lang>/... is auto-discovered by Vite.
function bundleLang(chunks: Record<string, unknown>): Record<string, unknown> {
  const out: Record<string, unknown> = {};
  for (const [filePath, value] of Object.entries(chunks)) {
    const base = filePath.split('/').pop();
    if (!base) continue;
    out[base.replace(/\.json$/, '')] = value;
  }
  return out;
}

// `import.meta.glob` requires a literal pattern — one call per language.
// `eager: true` inlines the JSON at build time (no runtime fetch).
const ptBR = bundleLang(
  import.meta.glob('./locales/pt-BR/**/*.json', { eager: true, import: 'default' })
);
const en = bundleLang(
  import.meta.glob('./locales/en/**/*.json', { eager: true, import: 'default' })
);
const es = bundleLang(
  import.meta.glob('./locales/es/**/*.json', { eager: true, import: 'default' })
);

const savedLanguage =
  typeof window !== 'undefined' ? (localStorage.getItem('xertica_language') ?? 'pt-BR') : 'pt-BR';

i18n.use(initReactI18next).init({
  resources: {
    'pt-BR': { translation: ptBR },
    en: { translation: en },
    es: { translation: es },
  },
  lng: savedLanguage,
  fallbackLng: 'pt-BR',
  interpolation: { escapeValue: false }, // React escapes already
});

export default i18n;
```

### 4. Initialize before rendering

```ts
// src/main.tsx — BEFORE any component import
import './i18n'; // side-effect: initializes i18next synchronously
import App from './app/App';
```

---

## Using Translations in Components

```tsx
import { useTranslation } from 'react-i18next';

function HomeContent() {
  const { t } = useTranslation();

  return (
    <div>
      <h1>{t('home.welcome')}</h1>
      <p>{t('home.subtitle')}</p>
      <button>{t('common.view')}</button>
    </div>
  );
}
```

### Interpolation

```json
{ "team": { "showing": "Exibindo {{count}} de {{total}} usuários" } }
```

```tsx
t('team.showing', { count: 5, total: 127 });
// → "Exibindo 5 de 127 usuários"
```

---

## Language Switching

The `LanguageSelector` component (from `xertica-ui/brand`) handles the full switching flow automatically:

1. User selects a language in the dropdown
2. `setLanguage(lang)` is called on `LanguageContext`
3. `LanguageContext` writes to `localStorage` (key: `xertica_language`) and calls `i18n.changeLanguage(lang)`
4. All components using `useTranslation()` re-render with the new locale
5. `queryClient.invalidateQueries()` is called — React Query refetches any query whose result contains translated strings

```tsx
// Manual language change (without LanguageSelector)
import { useLanguage } from 'xertica-ui/hooks';

const { setLanguage } = useLanguage();
setLanguage('en'); // persists + calls i18n.changeLanguage('en') + invalidates React Query cache
```

### Language codes

| Code      | Display        | Stored as                   |
| --------- | -------------- | --------------------------- |
| `'pt-BR'` | Português (BR) | `'pt-BR'` in `localStorage` |
| `'en'`    | English        | `'en'`                      |
| `'es'`    | Español        | `'es'`                      |

---

## Translation Key Namespaces

The project uses a single `translation` namespace. Each top-level key maps to a separate JSON file under `locales/<lang>/`:

**Root files** (`locales/<lang>/<key>.json`):

| Namespace          | File                    | Example keys                                                                    |
| ------------------ | ----------------------- | ------------------------------------------------------------------------------- |
| `common`           | `common.json`           | `common.view`, `common.edit`, `common.loading`, `common.close`, `common.copied` |
| `nav`              | `nav.json`              | `nav.home`, `nav.designSystem`, `nav.settings`                                  |
| `errors`           | `errors.json`           | `errors.somethingWentWrong`, `errors.tryAgain`, `errors.pageLoadError`          |
| `languageSelector` | `languageSelector.json` | `languageSelector.label`, `languageSelector.ptBR`                               |
| `themeToggle`      | `themeToggle.json`      | `themeToggle.switchToLight`, `themeToggle.darkMode`                             |

**Page files** (`locales/<lang>/pages/<key>.json`):

| Namespace           | File                           | Example keys                                                                      |
| ------------------- | ------------------------------ | --------------------------------------------------------------------------------- |
| `home`              | `pages/home.json`              | `home.welcome`, `home.subtitle`, `home.templateCliTitle`                          |
| `templates`         | `pages/templates.json`         | `templates.title`, `templates.alerts.infoTitle`, `templates.forms.firstName`      |
| `login`             | `pages/login.json`             | `login.heading`, `login.submit`, `login.forgotPassword`                           |
| `resetPassword`     | `pages/resetPassword.json`     | `resetPassword.heading`, `resetPassword.errorMismatch`                            |
| `verifyEmail`       | `pages/verifyEmail.json`       | `verifyEmail.heading`, `verifyEmail.resend`                                       |
| `loginTemplate`     | `pages/loginTemplate.json`     | `loginTemplate.title`, `loginTemplate.submit`                                     |
| `formTemplate`      | `pages/formTemplate.json`      | `formTemplate.title`, `formTemplate.save`, `formTemplate.errors.fullNameRequired` |
| `dashboardTemplate` | `pages/dashboardTemplate.json` | `dashboardTemplate.title`, `dashboardTemplate.stats.totalRevenue`                 |
| `crudTemplate`      | `pages/crudTemplate.json`      | `crudTemplate.title`, `crudTemplate.actions.editProfile`                          |

**Component files** (`locales/<lang>/components/<key>.json`):

| Namespace          | File                               | Example keys                                                                                             |
| ------------------ | ---------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `assistant`        | `components/assistant.json`        | `assistant.title`, `assistant.inputPlaceholder`, `assistant.tabs.chat`, `assistant.feedbackDialog.title` |
| `sidebar`          | `components/sidebar.json`          | `sidebar.collapse`, `sidebar.logout`, `sidebar.moreOptions`                                              |
| `media`            | `components/media.json`            | `media.play`, `media.pause`, `media.downloadAudio`, `media.floatingMode`                                 |
| `projectCard`      | `components/projectCard.json`      | `projectCard.progress`, `projectCard.status.active`                                                      |
| `profileCard`      | `components/profileCard.json`      | `profileCard.status.online`, `profileCard.status.busy`                                                   |
| `notificationCard` | `components/notificationCard.json` | `notificationCard.title`, `notificationCard.markAllRead`                                                 |
| `activityCard`     | `components/activityCard.json`     | `activityCard.title`, `activityCard.type.create`                                                         |
| `stats`            | `components/stats.json`            | `stats.totalUsers`, `stats.last30Days`                                                                   |
| `team`             | `components/team.json`             | `team.name`, `team.roles.Developer`, `team.showing`                                                      |

---

## Translating Mock Data

Mock data fetch functions use `i18n.t()` (the instance, not the hook) so they respond to the active language when called from a React Query `queryFn`:

```ts
// features/home/data/mock.ts
import i18n from '../../../i18n';

export async function fetchFeatureCards(): Promise<FeatureCard[]> {
  return [
    {
      id: 'template-cli',
      title: i18n.t('home.templateCliTitle'), // ← translated at query time
      description: i18n.t('home.templateCliDescription'),
    },
  ];
}
```

### Language-aware React Query Keys

Every hook that returns translated strings includes the active language in its `queryKey` so that each locale gets its own cache slot and switches instantly without a page reload:

```ts
// features/home/hooks/useFeatureCards.ts
import { useLanguage } from 'xertica-ui/hooks';

export function useFeatureCards() {
  const { language } = useLanguage();
  return useQuery({
    queryKey: ['home', 'feature-cards', language], // ← language as third element
    queryFn: fetchFeatureCards,
    staleTime: 10 * 60 * 1000,
  });
}
```

**Why this works:**

- Switching from `pt-BR` → `en` changes the queryKey to `['home', 'feature-cards', 'en']`
- React Query finds no cache entry for this key → triggers an immediate refetch
- `fetchFeatureCards()` runs again → `i18n.t()` now returns English strings
- Switching back to `pt-BR` → cache hit (Portuguese data still stored) → instant, no refetch

The `setLanguage()` call in `LanguageContext` also calls `queryClient.invalidateQueries()` as a defensive backstop for any query not yet updated to include `language` in its key.

### Fallback factory functions (not frozen constants)

When you need static fallback data while a query loads, use **factory functions** (not `const` arrays):

```ts
// ✅ Correct — evaluated at call time, always returns current language
export function getMockRichSuggestions(): Suggestion[] {
  return [{ id: 'rich-1', text: i18n.t('assistant.richSuggestions.viewPerformance') }];
}

// ❌ Wrong — i18n.t() runs once at module load, frozen in initial language
export const MOCK_RICH_SUGGESTIONS = [
  { id: 'rich-1', text: i18n.t('assistant.richSuggestions.viewPerformance') },
];
```

```tsx
// Usage in component
richSuggestions={assistantConfig?.richSuggestions ?? getMockRichSuggestions()}
```

---

## Configuring Available Languages

The set of languages a project supports is configured **at runtime** via the `availableLanguages` prop on `<XerticaProvider>` (or `<LanguageProvider>`). The library ships with built-in support for `pt-BR`, `en`, and `es` (exposed as `DEFAULT_LANGUAGES`), but the system is fully extensible.

### Choosing languages via the CLI

When you scaffold a new project, the CLI asks you which languages to enable:

```bash
$ npx xertica-ui init my-app
✔ Which pages/templates to include? › Login, Home, Template
✔ Which languages should the app support? › Português (BR), English, Español
✔ Select the default color theme: › Xertica
```

Pick **all three** (default), **two**, or **just one**:

- **All three** — the CLI omits the `availableLanguages` prop entirely (the library default already matches).
- **Two or one** — the CLI injects the explicit `availableLanguages` array into `src/app/App.tsx`.
- **Just one (monolingual)** — additionally, the `LanguageSelector` auto-hides because there is nothing to switch between. A header banner comment in `App.tsx` documents this.

The CLI also:

- Copies **only** the locale **folders** for the selected languages into `src/locales/` (no orphan files). Each language is a directory tree with split JSON files.
- Generates `src/i18n.ts` with `import.meta.glob` calls for exactly those languages — Vite auto-discovers all JSON files in the folder at build time.
- Persists the selection in `src/locales/.languages.json` so the `update` command can preserve it.

### Adding or removing languages later

Run `npx xertica-ui update` and choose **Languages**:

```bash
$ npx xertica-ui update
✔ What do you want to update? › Languages

Current languages: Português (BR), English

✔ Select the languages this project should support: › Português (BR), English, Español

  + es

⚠️  This will regenerate src/app/App.tsx and src/i18n.ts (preserving language-only changes). Continue? › yes
✔ Languages updated successfully!
  Copied: es/
```

The command:

1. Computes the add/remove diff and shows it to you.
2. Copies the newly-added locale **folders** from `node_modules/xertica-ui/templates/src/locales/<lang>/`.
3. Removes the **folders** of unselected languages from your project (also removes any legacy flat `<lang>.json` files if upgrading from pre-2.2.0).
4. Regenerates `src/i18n.ts` and `src/app/App.tsx` to reflect the new set.
5. Updates `src/locales/.languages.json`.

> The `update` → **Project files** flow also reads `.languages.json` and preserves your selection — updating `App.tsx` and `i18n.ts` won't reset your languages to the defaults.

### Default — three built-in languages

When you don't pass `availableLanguages`, the provider uses `DEFAULT_LANGUAGES`:

```tsx
import { XerticaProvider } from 'xertica-ui';

<XerticaProvider>
  {/* pt-BR, en, es are all available — selector shows all three */}
</XerticaProvider>;
```

### Monolingual — single language, no selector

Pass a single-element array to lock the app to one language. The `LanguageSelector` component **auto-hides** because there is nothing to switch to:

```tsx
<XerticaProvider availableLanguages={[{ code: 'en', label: 'English' }]}>
  {/* App is permanently English. LanguageSelector renders nothing. */}
</XerticaProvider>
```

To force the selector to render even when monolingual (e.g. for a future-proofing placeholder), pass `<LanguageSelector showWhenMonolingual />`.

### Adding a new language at runtime

There are two ways to add a language without editing `src/i18n.ts`.

**Option A — declarative, via `availableLanguages`** (recommended):

```tsx
import { XerticaProvider, DEFAULT_LANGUAGES } from 'xertica-ui';
import fr from './locales/fr.json';

<XerticaProvider
  availableLanguages={[
    ...DEFAULT_LANGUAGES,
    { code: 'fr', label: 'Français', shortLabel: 'FR', resources: fr },
  ]}
>
  {/* French now appears in the selector; its strings are registered automatically */}
</XerticaProvider>;
```

The provider calls `i18n.addResourceBundle()` on mount for any entry that carries `resources`.

**Option B — imperative, via `registerLanguageResource`**:

```ts
import { registerLanguageResource } from 'xertica-ui';
import fr from './locales/fr.json';

registerLanguageResource('fr', fr);
// Then list it on the provider
<XerticaProvider availableLanguages={[
  ...DEFAULT_LANGUAGES,
  { code: 'fr', label: 'Français', shortLabel: 'FR' },
]}>
```

### Removing built-in languages

Just omit them from `availableLanguages`. The translation bundles registered in `src/i18n.ts` remain loaded (they're cheap) but they're invisible to the UI:

```tsx
<XerticaProvider
  availableLanguages={[
    { code: 'pt-BR', label: 'Português' },
    { code: 'en', label: 'English' },
  ]}
>
  {/* Only Portuguese and English appear — Spanish is hidden */}
</XerticaProvider>
```

### Statically (library-only) — adding a built-in default

Only used when contributing to the library itself, not for app consumers:

1. Create `locales/<code>/` folder mirroring an existing language (copy `locales/en/` as a starting point, then translate all values)
2. `i18n.ts` uses `import.meta.glob` — **no import changes needed**. Vite auto-discovers the new folder on the next build.
3. Add a `LanguageDefinition` entry to `DEFAULT_LANGUAGES` in `contexts/LanguageContext.tsx`
4. Add an entry to `SUPPORTED_LANGUAGES` in `bin/language-config.ts` so the CLI exposes it in its prompts
5. Mirror the same folder structure under `templates/src/locales/<code>/` for the scaffold template

### `LanguageDefinition` shape

```ts
interface LanguageDefinition {
  /** BCP-47 language code stored in localStorage and passed to i18n.changeLanguage() */
  code: string;
  /** Full display label shown in the LanguageSelector dropdown */
  label: string;
  /** Short label shown in `variant="minimal"` (e.g. "PT", "EN"). Defaults to code.slice(0,2).toUpperCase() */
  shortLabel?: string;
  /** Optional translation JSON. When provided, it is registered with i18next automatically. */
  resources?: Record<string, unknown>;
}
```

### `useLanguage()` extras

```ts
const {
  language, // current locale code
  setLanguage, // change locale + persist + invalidate React Query
  availableLanguages, // LanguageDefinition[] currently configured
  isMonolingual, // true when availableLanguages.length === 1
} = useLanguage();
```

---

## AI Rules

- **ALWAYS import `'./i18n'` in `main.tsx` before any component** — if omitted, `useTranslation()` falls back to returning the raw key string
- **Use `t('namespace.key')` for all user-facing strings** — never hardcode text in JSX
- **Use `i18n.t()` (the instance) in non-component code** (mock fetch functions, utility files) — `useTranslation()` is a hook and can only be called inside React components and custom hooks
- **Include `language` in the `queryKey`** for every React Query hook that returns translated strings — this gives each locale its own cache slot and enables instant switching without reload
- **Use factory functions, not frozen constants**, when you need pre-translated fallback arrays — `const arr = [i18n.t(...)]` evaluates once at module load and is permanently frozen
- **Never hardcode language fallbacks in JSX** — add missing keys to the appropriate file under `locales/<lang>/` instead
- `setLanguage()` from `useLanguage()` is the single point of control — it persists to `localStorage`, calls `i18n.changeLanguage()`, and invalidates the React Query cache
- Import `useLanguage` from `'xertica-ui/hooks'` (or directly from `'xertica-ui'`) — both resolve to the same export. The `xertica-ui/brand` subpath exports only the `LanguageSelector` component and `Language` type, NOT the `useLanguage` hook.
- The `fallbackLng: 'pt-BR'` ensures missing keys in `en.json` or `es.json` display the Portuguese fallback instead of the raw key string
- **The set of available languages is runtime-configurable** — pass `availableLanguages` to `<XerticaProvider>` to add, remove, or restrict to a single locale. Never modify `DEFAULT_LANGUAGES` directly
- **The `LanguageSelector` auto-hides when `isMonolingual` is true** — do not conditionally render it yourself; pass it `showWhenMonolingual` if you want it visible
- **`Language` is typed as `string`** (not a strict union) to support runtime-added locales. Use `LanguageDefinition['code']` from `useLanguage().availableLanguages` for autocomplete
