3.7 KiB
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:
{
"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-intlis pure JS — no native binaries. Compatible with o2switch shared hosting.- Must be added to root
package.json(the singlenode_modulesfor the whole app per CLAUDE.md constraint). - No changes to API routes, worker, Prisma schema, or tests.