webroad.online
  1. 1Web
  2. 2HTML
  3. 3CSS
  4. 4JavaScript
  5. 5TypeScript
  6. 6Git
  7. 7Unelte
  8. 8React
  9. 9State management
  10. 10Next.js
  11. 11Formulare
  12. 12Date și backend
  13. 13SEO
  14. 14Tailwind CSS
  15. 15Animații
  16. 16Testare
  17. 17Arhitectură
Next.js · Lecția 12 din 13

Localizare corectă în Next.js cu next-intl

Rute cu [locale], next-intl, mesaje ICU, formatare cu Intl și hreflang.

Actualizat

i18n și l10n

  • i18n (internationalization: 18 litere între „i” și „n”) = pregătești codul pentru mai multe limbi: textele stau în fișiere de mesaje, nu în JSX, iar datele și prețurile trec prin Intl.
  • l10n (localization) = adaugi o limbă anume: traduci mesajele, alegi formatul datei, moneda, pluralele.

Faci i18n o dată. l10n faci pentru fiecare limbă nouă, de obicei fără să mai atingi codul.

Unde stă limba

Strategie Exemplu Pentru SEO
prefix în URL example.com/ro/preturi cea mai bună: fiecare limbă are URL-ul ei, ușor de indexat și de legat cu hreflang
domeniu example.ro, example.de bună, dar plătești și întreții mai multe domenii, iar autoritatea se împarte între ele
doar cookie example.com/preturi proastă: crawler-ul nu trimite cookie-uri, deci vede o singură limbă

Prefixul e alegerea implicită. Proxy-ul decide limba doar pentru adresele fără prefix:

GET /pricingproxy.ts→Accept-Languagero-RO,ro;q=0.9,en;q=0.7redirect→/ro/pricingapp/[locale] cu locale = ro

Fișierele de setup

next-intl (v4) e biblioteca cea mai folosită cu App Router. Un proiect arată așa:

  • i18n/
    • routing.tsdefineRouting: limbile și limba implicită
    • request.tsgetRequestConfig: limba curentă și mesajele ei
    • navigation.tscreateNavigation: Link, redirect, usePathname
  • messages/
    • en.jsontextele, grupate pe namespace-uri
    • ro.json
    • ru.json
  • app/
    • [locale]/
      • layout.tsxhtml lang, NextIntlClientProvider
      • page.tsx
  • proxy.tscreateMiddleware(routing)
  • next.config.tscreateNextIntlPlugin() leagă request.ts
// i18n/routing.ts
export const routing = defineRouting({ locales: ['en', 'ro', 'ru'], defaultLocale: 'en' })

// i18n/request.ts
import * as rootParams from 'next/root-params'

export default getRequestConfig(async () => {
  const locale = await rootParams.locale()             // segmentul [locale]
  if (!hasLocale(routing.locales, locale)) notFound()
  return { locale, messages: (await import(`../messages/${locale}.json`)).default }
})

// proxy.ts
export default createMiddleware(routing)
export const config = { matcher: '/((?!api|_next|_vercel|.*\\..*).*)' }

Layout-ul de limbă

// app/[locale]/layout.tsx
export function generateStaticParams() {
  return routing.locales.map(locale => ({ locale }))     // /en, /ro, /ru generate la build
}

export default async function LocaleLayout({ children, params }: LayoutProps<'/[locale]'>) {
  const { locale } = await params
  if (!hasLocale(routing.locales, locale)) notFound()     // /xx → 404, nu eroare
  return (
    <html lang={locale}>
      <body>
        <NextIntlClientProvider>{children}</NextIntlClientProvider>
      </body>
    </html>
  )
}

setRequestLocale. Înainte, pentru randare statică, chemai setRequestLocale(locale) în fiecare layout și pagină. Documentația next-intl spune acum că, dacă request.ts citește limba din next/root-params (inclus implicit din Next 16.3), nu mai e nevoie de el, iar integrarea cu cacheComponents e mai bună. generateStaticParams rămâne obligatoriu. Pe versiuni mai vechi de Next, setRequestLocale e încă necesar.

Traduceri pe server și pe client

// Server sau Client Component (fără async)
const t = useTranslations('Cart')
return <button>{t('add')}</button>

// cod async de server: acțiuni, route handlers, generateMetadata
export async function generateMetadata({ params }: PageProps<'/[locale]/pricing'>) {
  const { locale } = await params
  const t = await getTranslations({ locale, namespace: 'Pricing' })
  return { title: t('title') }
}

Mesaje ICU

{
  "greeting": "Salut, {name}!",
  "items": "{count, plural, =0 {Coșul e gol} one {# produs} few {# produse} other {# de produse}}",
  "invited": "{gender, select, female {Ea te-a invitat} male {El te-a invitat} other {Te-a invitat}}",
  "terms": "Accept <link>termenii</link>."
}
t('greeting', { name })
t('items', { count: 3 })                                        // „3 produse”
t.rich('terms', { link: chunks => <Link href="/terms">{chunks}</Link> })

Fiecare limbă își are propriile forme de plural: engleza are 2 (one, other), româna 3 (one, few, other), rusa 4 (one, few, many, other): 1 товар, 2 товара, 5 товаров, 21 товар. Traducătorul le scrie pe toate în mesaj, iar codul rămâne același.

Date, numere, timp relativ

const format = useFormatter()                   // pe server async: await getFormatter()
format.dateTime(post.date, { dateStyle: 'long' })                  // 9 octombrie 2026
format.number(price, { style: 'currency', currency: 'EUR' })       // 1.234,50 €
format.relativeTime(post.updatedAt, now)                           // acum 3 zile

Mesaje type-safe

// global.ts
declare module 'next-intl' {
  interface AppConfig {
    Locale: (typeof routing.locales)[number]
    Messages: typeof messages                   // import messages from './messages/en.json'
  }
}

Acum t('Cart.ad') e eroare de TypeScript, cu autocomplete pe chei.

// i18n/navigation.ts
export const { Link, redirect, usePathname, useRouter } = createNavigation(routing)

<Link href="/pricing"> duce la /ro/pricing dacă ești pe /ro, iar <Link href={usePathname()} locale="ru"> schimbă limba pe aceeași pagină. Cu pathnames în defineRouting poți traduce și URL-ul: '/pricing': { en: '/pricing', ro: '/preturi', ru: '/ceny' }.

SEO multilingv

  • O limbă = un URL. Nu servi două limbi la aceeași adresă.
  • hreflang pe fiecare versiune, reciproc, plus x-default. În Next, prin alternates.languages; next-intl trimite și un header link cu aceleași informații.
  • <html lang={locale}>, nu un lang="en" fix.
  • Nu redirecționa automat după Accept-Language sau IP adresele cu prefix. Googlebot crawlează mai ales din SUA, deci ar vedea doar engleza. Redirectul e ok doar pe /.
alternates: {
  canonical: `/${locale}/pricing`,
  languages: { en: '/en/pricing', ro: '/ro/pricing', ru: '/ru/pricing', 'x-default': '/en/pricing' },
}

Restul e în SEO tehnic și Metadata și SEO.

Organizarea mesajelor

  • Namespace-uri pe funcționalitate: Cart, Checkout, Pricing, nu un singur nivel cu 500 de chei.
  • Un fișier per limbă e ok la început. Când crește, îl împarți pe funcționalități (messages/ro/cart.json) și le unești în request.ts.
  • Flux: dezvoltatorul adaugă cheia în limba sursă, un script sau un serviciu de traduceri arată ce lipsește în celelalte, iar CI-ul pică dacă o cheie lipsește.

Greșeli frecvente

Greșeală Corect
text scris direct în JSX orice text vizibil vine din t()
t('hello') + ' ' + name t('greeting', { name }), cu variabila în mesaj
next/link cu href="/pricing" Link din i18n/navigation, păstrează limba
provider-ul trimite toate mesajele la client traduci în Server Components sau dai providerului doar namespace-urile necesare
count === 1 ? 'produs' : 'produse' {count, plural, ...} sau Intl.PluralRules

Și fără bibliotecă?

Un site mic poate face i18n doar cu Next: un segment [locale], proxy.ts pentru redirectul de pe /, dicționare JSON încărcate pe server și Intl pentru formatare. Chiar așa e făcut și webroad.online. next-intl merită când ai multe texte în Client Components, plurale și mesaje ICU, URL-uri traduse sau o echipă care vrea chei verificate de TypeScript.

Pe scurt

  • Limba stă în URL (/ro/...), proxy-ul alege limba doar pentru /, iar fiecare versiune are hreflang reciproc și <html lang> corect.
  • useTranslations în componente, getTranslations în cod async de server și în generateMetadata, un singur NextIntlClientProvider în layout.
  • Mesaje ICU pentru variabile și plurale, Intl pentru date și bani, niciodată stringuri lipite.

Surse oficiale

Exerciții

Ți-a fost utilă pagina?

Un click — fără cont.