docs: add i18n design spec (next-intl, FR/EN/ES/IT, URL routing)

This commit is contained in:
2026-08-10 15:22:01 +02:00
parent ee063aa460
commit e632608f1c
@@ -0,0 +1,137 @@
# 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.