Files

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-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.