# Internationalisation (i18n) — Design Spec

**Date:** 2026-08-10
**Locales:** FR, EN, ES, IT
**Library:** next-intl
**Default locale (fallback):** en

---

## Scope

Add multilingual support to the Ombrora-YTDLP Next.js frontend. The backend (API routes, worker, Prisma) is out of scope — no changes there.

---

## Routing

All UI pages move under a `[locale]` dynamic segment. Every URL gains a locale prefix.

| Before | After |
|--------|-------|
| `/` | `/en`, `/fr`, `/es`, `/it` |
| `/status/[uuid]` | `/en/status/[uuid]`, `/fr/status/[uuid]`, etc. |

A visitor hitting `/` is redirected by the middleware to the detected locale (or `en` if undetectable/unsupported).

API routes (`/api/*`) are unaffected.

---

## File Structure

```
src/
  app/
    [locale]/
      layout.tsx          # replaces src/app/layout.tsx
      page.tsx            # replaces src/app/page.tsx
      status/
        [uuid]/
          page.tsx        # replaces src/app/status/[uuid]/page.tsx
    api/                  # unchanged
  components/
    SubmitForm.tsx         # updated to use useTranslations
    StatusView.tsx         # updated to use useTranslations
    LanguageSwitcher.tsx   # new
  middleware.ts             # new — locale detection + redirect
  i18n/
    routing.ts             # locales + defaultLocale definition
    request.ts             # next-intl server config
messages/
  en.json
  fr.json
  es.json
  it.json
```

---

## Locale Detection (middleware)

`next-intl/middleware` reads the `Accept-Language` request header and redirects to the best matching locale. If no match among `[fr, en, es, it]`, falls back to `en`.

The detected locale is stored in the URL path (not a cookie, not a header). Switching language updates the URL.

---

## Translation Files

One JSON file per locale. All UI strings extracted from `SubmitForm.tsx`, `StatusView.tsx`, and the two page files.

Keys are namespaced by page/component:

```json
{
  "home": {
    "title": "...",
    "urlLabel": "...",
    "format": "...",
    "quality": "...",
    "subtitles": "...",
    "advanced": "...",
    "submit": "...",
    "submitting": "...",
    "errorRateLimit": "...",
    "errorGeneric": "..."
  },
  "status": {
    "title": "...",
    "notFound": "...",
    "loading": "...",
    "pending": "...",
    "processing": "...",
    "polling": "...",
    "ready": "...",
    "download": "...",
    "linkExpires": "...",
    "failed": "...",
    "deleted": "...",
    "unknown": "..."
  }
}
```

`yt-dlp` error output (`errorMsg`) is not translated — it stays as-is from the process stderr.

---

## LanguageSwitcher Component

Client component placed in the `[locale]/layout.tsx`, visible on all pages.

Displays four emoji-flag buttons: 🇫🇷 🇺🇸 🇪🇸 🇮🇹

On click, uses `next-intl`'s `useRouter` + `usePathname` to call `router.replace(pathname, { locale: selectedLocale })`, which swaps only the locale prefix while keeping the rest of the path (e.g. `/fr/status/uuid` → `/en/status/uuid`).

Active locale button is visually distinguished (e.g. opacity or underline).

---

## Metadata & HTML lang

`[locale]/layout.tsx` sets `<html lang={locale}>` dynamically. The page `<title>` is fetched via `getTranslations` (server-side) and included in the `metadata` export.

---

## Internal Navigation

`SubmitForm` redirects to `/[locale]/status/[uuid]` on success, preserving the current locale. Uses `next-intl`'s `useRouter` (which is locale-aware) instead of the raw Next.js router.

---

## Constraints

- `next-intl` is pure JS — no native binaries. Compatible with o2switch shared hosting.
- Must be added to root `package.json` (the single `node_modules` for the whole app per CLAUDE.md constraint).
- No changes to API routes, worker, Prisma schema, or tests.
