Paprašę Cursor ar Claude Code pridėti i18n prie Next.js aplikacijos šiandien, greičiausiai gausite middleware.ts, setRequestLocale kiekviename layout'e ir useTranslations, kviečiamą iš async puslapio — sąranką, kuri buvo teisinga Next.js 15, bet Next.js 16 jau yra viena karta pasenusi. globalize.now yra DI valdoma lokalizacijos infrastruktūra, todėl būtent šį sluoksnį mes stebime: maršrutizavimo mechanizmas pasikeitė, o katalogas, kurį jis aptarnauja, — ne. Toliau — kas pasikeitė, kaip atpažinti, iš kurios eros yra jūsų sugeneruotas kodas, ir trys pataisymai, kuriems reikia popietės, o ne perrašymo nuo nulio.

Nė vienas iš pakeitimų nėra griaunantis (breaking change). Būtent todėl juos lengva praleisti.

Kas pasikeitė Next.js 16 i18n srityje?

Failas, kuris nustato jūsų lokalę, pakeitė pavadinimą. Next.js dokumentacija nurodo, kad middleware failo konvencija yra pasenusi ir pervadinta į proxy, tai įsigaliojo nuo v16.0.0, o pačios komandos paaiškinimas susijęs su terminologija — „middleware“ nuolat buvo suprantamas kaip Express middleware, todėl funkcija buvo pervadinta, kad tiksliau apibūdintų tinklo ribą, kuria ji iš tikrųjų yra.

Kartu su pervadinimu v16 versijoje atsirado dvi elgsenos pastabos. Proxy pagal numatytuosius nustatymus veikia Node.js vykdymo aplinkoje, ir tenai nebegalima nustatyti failo lygio runtime konfigūracijos parinkties — jei bandysite ją nustatyti, Next.js mes klaidą. Proxy taip pat nepalaikomas statiniame eksporte, o tai svarbu, jei lokalės nustatymas yra vienintelė priežastis, dėl kurios neeksportuojate.

Konkrečiai lokalės maršrutizavimui next-intl sąrankos vadovas dabar rodo failą kaip src/proxy.ts ir aiškiai nurodo, kad iki Next.js 16 jis buvo vadinamas middleware.ts. Importas jo viduje nepakito:

// src/proxy.ts
import createMiddleware from 'next-intl/middleware';
import {routing} from './i18n/routing';

export default createMiddleware(routing);

export const config = {
  matcher: '/((?!api|trpc|_next|_vercel|.*\\..*).*)'
};

Atkreipkite dėmesį į asimetriją, nes ji dažnai suklaidina: failas dabar yra proxy.ts, o importo kelias vis dar yra next-intl/middleware. Importo pervadinimas — dažna per didelio uolumo klaida.

Kodėl DI sugeneruotas i18n kodas atsilieka viena versija?

Todėl, kad kodavimo agentas numato labiausiai paplitusį šabloną, o labiausiai paplitęs šablonas yra tas, kuris turėjo metų metus kauptis. Kiekvienas tinklaraščio įrašas, Stack Overflow atsakymas ir GitHub pavyzdys apie App Router lokalės maršrutizavimą, parašytas iki 2026 metų pabaigos, mini middleware.ts. Modelis, sveriantis šį korpusą su kelių savaičių senumo v16 dokumentacija, pasirinks seną formą — užtikrintai, be jokio įspėjimo, kad įvyko pervadinimas.

Tai tas pats mechanizmas, dėl kurio jau rašėme anksčiau — kai Cursor po i18n sąrankos toliau prideda užkoduotus tekstus — agentas atkuria statistiškai įprastą kodo bazę, o ne jūsiškę. Būtent dėl to instrukcijų failai tik iš dalies išsprendžia problemą GitHub Copilot atveju: taisyklės, kurias skaito pokalbių sąsaja, nebūtinai skaitomos ir tiesioginio (inline) papildymo funkcijos.

Praktinė pasekmė siaura, bet reali. Sugeneruota sąranka veikia, todėl nieko garsiai nesugenda. Tada aptinkate klaidą, ieškote jos internete, o kiekvienas šiuolaikinis atsakymas aprašo failus, kurių pas jus nėra.

Kaip nustatyti, iš kurios eros yra mano sugeneruota sąranka?

Keturi grep'ai. Paleiskite juos iš projekto šaknies katalogo, ir per minutę sužinosite atsakymą.

# 1. Pre-16 locale negotiation file
ls middleware.ts src/middleware.ts 2>/dev/null

# 2. Legacy static-rendering API
grep -rn "setRequestLocale" app src 2>/dev/null

# 3. Hooks called inside async components
grep -rn -B3 "useTranslations" app | grep -n "async function"

# 4. Which next-intl era the config reads from
grep -rn "root-params\|await params" src/i18n/request.ts 2>/dev/null

Rezultatai 1 ir 2 pozicijose reiškia sąranką iš iki-16 versijos eros. Rezultatas 3 pozicijoje reiškia tikrą vykdymo laiko klaidą, laukiančią pirmos užklausos, kuri atvaizduos tą komponentą. Jokio rezultato 4 pozicijoje reiškia, kad jūsų užklausos konfigūracija lokalę nuskaito senuoju būdu.

Ar būtina pervadinti middleware.ts į proxy.ts?

Nebūtina daryti to skubiai, ir tikrai nereikėtų daryti rankiniu būdu. Next.js siūlo codemod, kuris pervadina ir failą, ir eksportuojamą funkciją:

npx @next/codemod@canary middleware-to-proxy .

Pervadinimas yra nutraukimas (deprecation), o ne pašalinimas, todėl esamas middleware.ts toliau veikia. Vis dėlto verta jį paleisti dėl priežiūros kaštų: kai jūsų failų pavadinimai atitiks dabartinę dokumentaciją, kiekvienas būsimas paieškos rezultatas bus aktualus jūsų saugyklai. Palikite tai nepakeista, ir mokėsite nedidelį „mokestį“ per kiekvieną derinimo sesiją — amžinai.

Ar setRequestLocale yra pasenęs (deprecated) next-intl?

Jis pažymėtas kaip legacy, o tai švelnesnis teiginys nei deprecated, ir verta jį suprasti tiksliai. next-intl dokumentacija apibūdina setRequestLocale kaip API, egzistavusią iki tol, kol buvo pristatyta next/root-params, sako, kad ji vis dar palaikoma dėl suderinamumo atgal, ir rekomenduoja naudoti next/root-params.

Naujesnė forma nuskaito atitikusią lokalę jūsų užklausos konfigūracijoje, o ne perduoda ją rankiniu būdu per kiekvieną layout ir puslapį:

// src/i18n/request.ts
import * as rootParams from 'next/root-params';
import {notFound} from 'next/navigation';
import {getRequestConfig} from 'next-intl/server';
import {hasLocale} from 'next-intl';
import {routing} from './routing';

export default getRequestConfig(async ({locale}) => {
  if (!locale) {
    const paramValue = await rootParams.locale();
    if (hasLocale(routing.locales, paramValue)) {
      locale = paramValue;
    } else {
      notFound();
    }
  }

  return {locale};
});

next/root-params pagal numatytuosius nustatymus prieinamas Next.js 16.3 ir naujesnėse versijose; senesnėse versijose jį reikia įjungti per experimental.rootParams. Sekant šią sąranką statinis atvaizdavimas gaunamas nemokamai, jei tik toliau eksportuosite generateStaticParams [locale] segmentui.

Senasis metodas reikalavo kviesti setRequestLocale kiekviename puslapyje ir layout'e, kurį norėjote atvaizduoti statiškai, prieš bet kokį kitą next-intl kvietimą, nes Next.js atvaizduoja layout'us ir puslapius nepriklausomai vienas nuo kito. Tai taisyklė, kurią DI agentas pamiršta rašydamas jau penktą failą. Panaikinus šį reikalavimą, panaikinama ir visa ta klaidų klasė.

Kodėl useTranslations lūžta mano asinchroniniame Server Component?

Todėl, kad hook'ų negalima kviesti iš async komponentų, o useTranslations yra hook'as. Tai React Server Components apribojimas, o ne next-intl ypatumas, ir next-intl sprendimas — lygiagretus laukiamų (awaitable) funkcijų rinkinys:

// Async component — await the server API
import {getTranslations} from 'next-intl/server';

export default async function ProfilePage() {
  const user = await fetchUser();
  const t = await getTranslations('ProfilePage');
  return <h1>{t('title', {username: user.name})}</h1>;
}
// Non-async component — the hook is correct here
import {useTranslations} from 'next-intl';

export default function UserDetails({user}) {
  const t = useTranslations('UserProfile');
  return <h2>{t('title')}</h2>;
}

getFormatter, getNow, getTimeZone, getMessages ir getLocale veikia pagal tą patį principą. Verta atidžiau pažvelgti į antrąjį pavyzdį: neasinchroninis komponentas be interaktyvių funkcijų yra bendras (shared) komponentas, ir next-intl parenka tinkamą realizaciją priklausomai nuo to, ar jis atvaizduojamas serveryje, ar kliento pusėje. Taigi useTranslations Server Component nėra klaida — klaida yra jį kviesti iš async komponento.

Kodėl gaunu NextIntlClientProvider konteksto klaidą?

next-intl trikčių šalinimo pastabose nurodomos dvi priežastys, ir joms reikia priešingų sprendimų. Arba komponentas iš tikrųjų veikia kliento pusėje be provider'io virš jo — tokiu atveju apgaubkite jį ir perduokite reikiamus pranešimus, arba jis atsidūrė kliento modulių grafe, nors tikėjotės serverinio atvaizdavimo — tokiu atveju perduokite jį per children iš Server Component, o ne importuokite viduje jo.

Antrasis atvejis būtent tas, su kuriuo susiduria DI sugeneruotos aplikacijos, nes agentai gausiai naudoja 'use client', kad interaktyvumas veiktų, o ši direktyva plinta žemyn per importų grafą. Rekomenduojamas metodas — versti serveryje ir perduoti jau gatavus tekstus per ribą:

import {useTranslations} from 'next-intl';
import Expandable from './Expandable'; // 'use client'

export default function FAQEntry() {
  const t = useTranslations('FAQEntry');
  return <Expandable title={t('title')}>{t('description')}</Expandable>;
}

Jei kokiam nors komponentui tikrai reikia pranešimų kliento pusėje, galite apriboti provider'į iki to konkretaus posmedžio, o ne siųsti visus pranešimus į naršyklę — messages={null} šakniniame provider'yje neperduoda nieko.

Kas iš viso to nepasikeitė?

Jūsų lokalės failai. Kiekvienas aukščiau minėtas pataisymas yra maršrutizavimo ir atvaizdavimo mechanika — JSON, kurį įkelia jūsų aplikacija, lieka nepaliestas. Ir būtent tai verta įsidėmėti, nes ši mechanika yra vienos popietės darbas, kuris keičiasi kartą per metus, o katalogas — tai dalykas, kuris genda kas savaitę, kai tik pasirodo nauja sąsaja.

Būtent toks yra skirtumas, kuriam mes kuriame savo sprendimą. next-intl ir jam giminingi įrankiai vertimus aptarnauja vykdymo metu — mes su jais nekonkuruojame, o jei vis dar renkatės tarp jų, next-intl vs react-i18next vs Lingui išdėsto kompromisus. globalize.now veikia vienu sluoksniu aukščiau — sukuria raktus ir lokalės failus, kuriuos šie vykdymo laiko įrankiai skaito, todėl Next.js integracijai visiškai nesvarbu, ar jūsų lokalės nustatymas gyvena middleware.ts, ar proxy.ts.

Konversija atliekama vieną kartą ir vyksta pačioje aplikacijoje: prijungiate saugyklą, globalize.now konvertuoja kodo bazę vieną kartą ir atidaro pull request su katalogu. Po to push užduotys išverčia naujus katalogo vienetus, kai tik jie atsiranda — tai kaip tik ta gedimo forma, aprašyta kodėl vertimo failai nuolat išsiskiria (drift), ir priežastis, dėl kurios šį spragą verta uždaryti prieš pasirodant trečiajai kalbai, o ne po to.

Jei jūsų projektas jau turi rankiniu būdu sukurtą next-intl sąranką, mes esame parašę, ką konkrečiai keičiame, o ko ne. Jei ne, Cursor apžvalga yra trumpiausias kelias pradėti — su viena pastaba: jos sugeneruotas failas pavadintas pagal senesnę konvenciją, todėl po to paleiskite codemod.

Nuo ko pradėti

Paleiskite keturis grep'us. Jei gaunate rezultatų pirmuose dviejuose, paleiskite codemod, perkelkite savo užklausos konfigūraciją į next/root-params — ir esate atnaujinti. Tada peržiūrėkite lokalės failus, nes būtent ši dalis vis dar keisis kitą mėnesį. Jei kuriate DI sukurtą aplikaciją ir norite, kad katalogas būtų sutvarkomas, o ne palaikomas rankiniu būdu, vibe coders puslapis yra apžvalga, o kainodara pateikta atskirame puslapyje.

globalize.now paverčia užkoduotą programos tekstą į vertimui paruoštus lokalės failus ir juos atnaujina, kai jūs diegiate naujus pakeitimus.

Išbandyti globalize.now nemokamai