# Internationalization (i18n) Design

## Overview

This document describes the i18n system for Dyad. The goal is to support multiple languages across the Electron renderer and main process with type-safe translation keys, minimal boilerplate, and incremental adoption.

## Library: `react-i18next` + `i18next`

Use `react-i18next` (the de facto standard for React i18n) rather than building a custom solution.

Rationale:

- Mature ecosystem with broad community support
- Built-in pluralization, interpolation, nesting, and context support
- ICU message format support via plugin when needed
- TypeScript support for key autocompletion
- Works in both renderer (React) and main process (plain `i18next`)
- Lazy-loading of translation namespaces out of the box

### Dependencies

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

No additional plugins are needed initially. Translation files are bundled with the app (not fetched remotely), so no HTTP backend is required.

## Translation file structure

```
src/
  i18n/
    index.ts              # i18next initialization
    types.ts              # Generated types for key autocompletion
    locales/
      en/
        common.json       # Shared strings (buttons, labels, generic)
        settings.json     # Settings page
        chat.json         # Chat UI
        home.json         # Home page
        errors.json       # Error/toast messages
      zh-CN/
        common.json
        settings.json
        ...
      ja/
        common.json
        ...
```

### Namespace strategy

Split translations by feature area (namespace = one JSON file). This keeps files manageable and allows lazy-loading namespaces for routes that aren't immediately visible.

| Namespace      | Scope                                       |
| -------------- | ------------------------------------------- |
| `common`       | Buttons, generic labels, confirmations, nav |
| `settings`     | All settings page sections                  |
| `chat`         | Chat input, messages, streaming indicators  |
| `home`         | Home page, app list, templates              |
| `errors`       | Toast messages, error dialogs, validation   |
| `hub`          | Hub/library/marketplace                     |
| `integrations` | GitHub, Supabase, Neon, Vercel connectors   |

### Translation file format

Standard flat-key JSON with nesting where it aids organization:

```json
// en/settings.json
{
  "title": "Settings",
  "general": {
    "title": "General",
    "language": "Language",
    "zoom": "Zoom Level",
    "theme": "Theme"
  },
  "ai": {
    "title": "AI",
    "model": "Model",
    "provider": "Provider",
    "apiKey": "API Key"
  },
  "agent": {
    "toolPermissions": "Configure permissions for Agent built-in tools.",
    "permissionOption": {
      "ask": "Ask",
      "always": "Always allow",
      "never": "Never allow"
    }
  }
}
```

```json
// en/common.json
{
  "save": "Save",
  "cancel": "Cancel",
  "delete": "Delete",
  "confirm": "Confirm",
  "loading": "Loading...",
  "copyToClipboard": "Copy to clipboard",
  "copied": "Copied!",
  "itemCount_one": "{{count}} item",
  "itemCount_other": "{{count}} items"
}
```

Pluralization uses i18next's built-in suffix convention (`_one`, `_other`, `_zero`, etc.), which handles most languages. For languages with complex plural rules (e.g., Arabic, Polish), i18next resolves the correct form automatically.

## Initialization

```typescript
// src/i18n/index.ts
import i18n from "i18next";
import { initReactI18next } from "react-i18next";

// Import all locale bundles (bundled with the app)
import enCommon from "./locales/en/common.json";
import enSettings from "./locales/en/settings.json";
import enChat from "./locales/en/chat.json";
import enHome from "./locales/en/home.json";
import enErrors from "./locales/en/errors.json";
// ... other languages imported similarly

const resources = {
  en: {
    common: enCommon,
    settings: enSettings,
    chat: enChat,
    home: enHome,
    errors: enErrors,
  },
  // "zh-CN": { ... },
  // "ja": { ... },
};

i18n.use(initReactI18next).init({
  resources,
  lng: "en", // Default; overridden by user setting on startup
  fallbackLng: "en",
  defaultNS: "common",
  ns: ["common", "settings", "chat", "home", "errors"],
  interpolation: {
    escapeValue: false, // React already escapes
  },
});

export default i18n;
```

Import `src/i18n/index.ts` at the app entry point (`src/main.tsx` or equivalent) before rendering.

## React usage

### `useTranslation` hook

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

function AgentToolsSettings() {
  const { t } = useTranslation("settings");

  return (
    <div>
      <p className="text-sm text-muted-foreground">
        {t("agent.toolPermissions")}
      </p>
      <SelectItem value="ask">{t("agent.permissionOption.ask")}</SelectItem>
      <SelectItem value="always">
        {t("agent.permissionOption.always")}
      </SelectItem>
    </div>
  );
}
```

### Multiple namespaces

```tsx
const { t } = useTranslation(["settings", "common"]);

// Keys from the first namespace work directly
t("general.title"); // → "General" (from settings)

// Keys from other namespaces use prefix
t("common:save"); // → "Save" (from common)
```

### Interpolation

```tsx
t("errors:fileNotFound", { path: "/some/file.txt" });
// "File not found: {{path}}" → "File not found: /some/file.txt"
```

### Components with embedded markup

Use the `Trans` component for strings that contain JSX:

```tsx
import { Trans } from "react-i18next";

<Trans i18nKey="home:welcome" t={t}>
  Welcome to <strong>Dyad</strong>
</Trans>;
```

## Type safety

### Generating types from translation files

Create a type declaration so that `t("...")` calls get autocompletion and compile-time checking of keys.

```typescript
// src/i18n/types.ts
import "i18next";
import type enCommon from "./locales/en/common.json";
import type enSettings from "./locales/en/settings.json";
import type enChat from "./locales/en/chat.json";
import type enHome from "./locales/en/home.json";
import type enErrors from "./locales/en/errors.json";

declare module "i18next" {
  interface CustomTypeOptions {
    defaultNS: "common";
    resources: {
      common: typeof enCommon;
      settings: typeof enSettings;
      chat: typeof enChat;
      home: typeof enHome;
      errors: typeof enErrors;
    };
  }
}
```

This gives full autocomplete for `t("settings:general.title")` etc., and TypeScript errors for invalid keys.

## Language setting integration

### User settings

Add a `language` field to `UserSettingsSchema` in `src/lib/schemas.ts`:

```typescript
// In UserSettingsSchema
language: z.string().default("en"),
```

### Settings UI

Add a language selector to the General settings section (similar to the existing zoom selector):

```tsx
function LanguageSelector() {
  const { t } = useTranslation("settings");
  const [settings, setSettings] = useSettings();

  const languages = [
    { value: "en", label: "English" },
    { value: "zh-CN", label: "简体中文" },
    { value: "ja", label: "日本語" },
    { value: "ko", label: "한국어" },
    { value: "es", label: "Español" },
    { value: "fr", label: "Français" },
    { value: "de", label: "Deutsch" },
  ];

  const handleChange = (value: string) => {
    i18n.changeLanguage(value);
    setSettings({ ...settings, language: value });
  };

  return (
    <Select value={settings.language} onValueChange={handleChange}>
      {languages.map((lang) => (
        <SelectItem key={lang.value} value={lang.value}>
          {lang.label}
        </SelectItem>
      ))}
    </Select>
  );
}
```

Language labels are shown in their native script (not translated) so users can always find their language regardless of the current UI language.

### Startup sync

On app startup, read the persisted language from user settings and call `i18n.changeLanguage(savedLanguage)` before the first render. This can be done in the settings loading hook or in `src/i18n/index.ts` by reading the setting synchronously.

## Electron main process strings

Some user-facing strings originate in the main process (e.g., native dialogs, menu items, error messages sent over IPC). For these:

1. Import `i18next` directly (without `react-i18next`) in main process code.
2. Share the same locale JSON files.
3. Initialize a separate i18next instance in `src/main/i18n.ts`.

```typescript
// src/main/i18n.ts
import i18n from "i18next";
import enErrors from "../i18n/locales/en/errors.json";

const mainI18n = i18n.createInstance();
mainI18n.init({
  resources: { en: { errors: enErrors } },
  lng: "en",
  fallbackLng: "en",
  defaultNS: "errors",
});

export default mainI18n;
```

When the user changes language in the renderer, send the new language to the main process via IPC so it can call `mainI18n.changeLanguage(lng)`.

## Date, number, and relative time formatting

Use the browser's `Intl` API (already available in Electron's Chromium) rather than adding a formatting library:

```typescript
// Utility in src/i18n/format.ts
export function formatDate(date: Date, locale: string): string {
  return new Intl.DateTimeFormat(locale, {
    dateStyle: "medium",
    timeStyle: "short",
  }).format(date);
}

export function formatNumber(value: number, locale: string): string {
  return new Intl.NumberFormat(locale).format(value);
}

export function formatRelativeTime(date: Date, locale: string): string {
  const rtf = new Intl.RelativeTimeFormat(locale, { numeric: "auto" });
  const diffMs = date.getTime() - Date.now();
  const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24));
  if (Math.abs(diffDays) < 1) {
    const diffHours = Math.round(diffMs / (1000 * 60 * 60));
    return rtf.format(diffHours, "hour");
  }
  return rtf.format(diffDays, "day");
}
```

The existing `date-fns` dependency also supports locale-aware formatting if more complex date operations are needed.

## Incremental adoption strategy

Migrating all strings at once is impractical. Instead, adopt incrementally:

### Phase 1: Infrastructure

- Install dependencies, create `src/i18n/` directory structure, initialize i18next.
- Add `language` to UserSettings schema.
- Create `en/common.json` with the most common shared strings (button labels, generic terms).
- Add the language selector to settings.

### Phase 2: Settings page

- Extract all settings page strings into `en/settings.json`.
- Replace hardcoded strings in settings components with `t()` calls.
- This is a self-contained area with many strings, good for validating the approach.

### Phase 3: Core UI

- Extract chat, home, and error strings into their respective namespace files.
- Convert toast messages in `src/lib/toast.tsx` and callers.
- Convert dialog and modal text.

### Phase 4: First additional language

- Add one complete translation (e.g., `zh-CN`) to validate the full loop.
- Fix any layout issues from longer/shorter translated strings.
- Verify RTL considerations if an RTL language is planned.

### Phase 5: Remaining strings and languages

- Extract remaining hardcoded strings (integrations, hub, etc.).
- Add more language translations.
- Set up a translation workflow (see below).

## Translation workflow

### For contributors

- English is the source of truth. All new strings are added to `en/*.json` first.
- Other language files must mirror the English key structure. Missing keys fall back to English automatically.

### Lint rule

Add a CI check that verifies all keys present in `en/*.json` exist in every other locale. Missing keys produce warnings (not errors, since fallback handles them), making it easy to see translation coverage.

### Extraction (optional tooling)

Consider `i18next-parser` to scan source files for `t("...")` calls and auto-generate/update the English JSON files. This catches strings that were added in code but not in the JSON.

```json
// package.json script
"i18n:extract": "i18next-parser 'src/**/*.{ts,tsx}'"
```

## Key conventions

| Convention                     | Example                                                |
| ------------------------------ | ------------------------------------------------------ |
| Namespace maps to feature area | `settings`, `chat`, `common`                           |
| Nested keys use dot notation   | `settings:general.title`                               |
| Action labels are imperative   | `"save": "Save"`, `"delete": "Delete"`                 |
| Descriptions are sentence case | `"toolPermissions": "Configure permissions..."`        |
| Plurals use i18next suffixes   | `_one`, `_other`                                       |
| Interpolation uses `{{var}}`   | `"hello": "Hello, {{name}}"`                           |
| No string concatenation        | Use interpolation instead of `t("a") + value + t("b")` |

## What NOT to translate

- Log messages and debug output (keep in English for debugging)
- IPC channel names and internal identifiers
- Database column names and schema identifiers
- Error stack traces
- Third-party API responses
