138 lines
3.7 KiB
Markdown
138 lines
3.7 KiB
Markdown
# 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.
|