Članki
Next.js i18n v 22 jezikih: izkušnje iz projekta GAGA
Usmerjanje po jezikih, srbščina v latinici in cirilici, oblikovanje z Intl, fonti, CI za prevode in sveži tečaji: izkušnje s platforme Next.js v 22 jezikih.
Inženirska ekipa sigmacode.io10 min branja
Na tej strani (11)
- Vsaka jezikovna različica naj ima svoj URL
- Dve pisavi, en jezik
- Števila, valute in datume oblikujte z Intl
- Fonti in pokritost znakov
- Prevajalski proces, ki zdrži 22 jezikov
- Podatki v realnem času brez zastarelih vrednosti
- En vir resnice za PDF, Excel, CSV in XML
- SEO v številnih jezikih
- Dostopnost
- Testiranje: poti × jeziki
- Kaj je treba načrtovati
GAGA Menjačnica je menjalnica in trgovec s plemenitimi kovinami z več poslovalnicami v Novem Sadu v Srbiji. Naša ekipa je njihovo platformo menjacnicegaga.rs zgradila z Next.js in Reactom na Vercelu. Deluje v 22 jezikih, med njimi v srbščini v latinici in cirilici, nemščini, kitajščini, ruščini, turščini, ukrajinščini, grščini in bolgarščini. Prikazuje aktualne nakupne in prodajne tečaje ob referenčnih tečajih Narodne banke Srbije, vključuje pa tudi pretvornik valut, tečajnice za prenos v petih formatih, iskalnik poslovalnic ter vodnike po naložbenem zlatu in srebru.
Pri dvaindvajsetih jezikih internacionalizacija ni več ena od funkcij, temveč oblikuje arhitekturo. V tem članku povzemamo, kaj je po našem mnenju pri gradnji takšne platforme res pomembno. Namenjen je tehničnim direktorjem (CTO), lastnikom produkta in frontend inženirjem, ki načrtujejo podoben projekt. Več o samem projektu najdete v študiji primera GAGA.
Vsaka jezikovna različica naj ima svoj URL#
Najpomembnejša odločitev pride na vrsto prva: vsaka jezikovna različica vsake strani potrebuje svoj stabilen URL, ki ga iskalniki lahko preiščejo. Jezika ne preklapljajte s piškotkom ali z ugibanjem iz glave Accept-Language. Iskalniki tega ne morejo indeksirati, uporabniki ne deliti, omrežja CDN pa ne čisto predpomniti.
Priporočamo jezikovno predpono v poti, na primer /sr/..., /de/... in /zh/.... V Next.js App Routerju to pomeni segment [locale] v korenu aplikacije. Posredniški sloj zahtevkov (proxy.ts, prej middleware) lahko prvi obisk preusmeri na smiselno privzeto različico glede na jezik brskalnika. Od tam naprej je URL edini vir resnice, preklopnik jezika pa mora voditi na isto stran v drugem jeziku in ne na vstopno stran drugega jezika.
Jezikovne oznake načrtujte zgodaj in povsod uporabljajte oznake BCP 47. Že srbščina potrebuje dve, sr-Latn in sr-Cyrl. Če njihovo preslikavo v segmente URL-jev, vrednosti hreflang in jezikovne oznake za Intl določite na enem mestu, si pozneje prihranite veliko težav.
hreflang, x-default in zemljevidi strani#
Vsaka stran naj navede vse svoje alternative, vključno s samo seboj, in doda x-default za uporabnike, katerih jezika ne podpirate. V App Routerju to namesto vas opravi API za metapodatke:
// app/[locale]/rates/page.tsx
import type { Metadata } from "next";
const BASE = "https://example.com";
const LOCALES = ["sr-Latn", "sr-Cyrl", "en", "de", "zh", "ru"] as const;
const segment = (l: string) => l.toLowerCase(); // "sr-Latn" -> "sr-latn"
export async function generateMetadata({ params }: { params: Promise<{ locale: string }> }): Promise<Metadata> {
const { locale } = await params;
const path = "/rates";
const languages: Record<string, string> = Object.fromEntries(
LOCALES.map((l) => [l, `${BASE}/${segment(l)}${path}`])
);
languages["x-default"] = `${BASE}/en${path}`;
return {
alternates: { canonical: `${BASE}/${locale}${path}`, languages },
};
}
Zemljevid strani (sitemap) zgradite iz istega seznama jezikov in istega registra poti, z alternativami pri vsakem vnosu. Ko oznake hreflang in vnosi v zemljevidu strani nastajajo v ločenih delih kode, se bodo prej ali slej razšli, iskalniki pa bodo potihoma nehali zaupati obojim.
Dve pisavi, en jezik#
Srbščina se piše v latinici in cirilici, številni bralci pa imajo do ene od pisav izrazito naklonjenost. Obravnavajte ju kot dve polnopravni jezikovni različici in ne kot eno različico s preklopom prikaza. Vsaka dobi svoj URL, svojo vrednost hreflang in svoj atribut lang.
Mamljivo je vse napisati v eni pisavi in drugo samodejno transliterirati. Za tekoče besedilo je dobro preizkušen korak transliteracije lahko razumno izhodišče. Vendar ga ne uporabljajte na slepo za vse:
- Lastna imena in blagovne znamke. Imena podjetij, imena izdelkov in tuje besede pogosto tudi v ciriličnem besedilu ohranijo latinično obliko. Mehanski pretvornik jih bo „ustrežljivo“ spremenil v nekaj, česar ni nihče napisal.
- Dvoumnost dvočrkij. Latinični
nj,ljindžse običajno preslikajo v eno samo cirilično črko, vendar ne vedno. Zloženke in tuje izposojenke to pravilo kršijo. Pretvorba iz cirilice v latinico je deterministična, iz latinice v cirilico pa ne. - URL-ji, kode in identifikatorji. Kod valut, kot je
EUR, e-poštnih naslovov, slugov in vsega, kar je v interpolacijskih ogradah, ne smete nikoli transliterirati. - Iskanje in razvrščanje. Uporabniki lahko na cirilični strani v iskalno polje tipkajo v latinici. Pred primerjavo normalizirajte obe strani.
Naše priporočilo: hranite cirilični izvirnik (deterministična smer) ali vzdržujte dva ločena kataloga in ob tem kratek seznam izjem, za katerega skrbi rojeni govorec. Vse, kar je ustvaril stroj, je treba pred objavo pregledati.
Števila, valute in datume oblikujte z Intl#
Na strani z menjalnimi tečaji so številke izdelek. Srbščina kot decimalno ločilo uporablja vejico, nemščina tisočice ločuje s piko, kitajski uporabniki pa pričakujejo spet drugačna pravila. Ne oblikujte ročno. Uporabite vgrajene API-je Intl in jim podajte celotno jezikovno oznako:
const rateFormatter = (locale: string) =>
new Intl.NumberFormat(locale, {
minimumFractionDigits: 4,
maximumFractionDigits: 4,
});
const moneyFormatter = (locale: string, currency: string) =>
new Intl.NumberFormat(locale, { style: "currency", currency });
const asOf = (locale: string, date: Date) =>
new Intl.DateTimeFormat(locale, {
dateStyle: "long",
timeStyle: "short",
timeZone: "Europe/Belgrade",
}).format(date);
rateFormatter("sr-Latn").format(117.1234); // "117,1234"
rateFormatter("de").format(117.1234); // "117,1234"
moneyFormatter("en", "EUR").format(1250); // "€1,250.00"
asOf("sr-Cyrl", new Date()); // e.g. "18. септембар 2026. 10:30" (exact output depends on the ICU version)
Nekaj stvari, ki jih je treba predvideti. Časovni pas nastavite izrecno. Strani, izrisane na strežniku, sicer uporabijo pas strežnika, ki je običajno UTC. Oblikovalnike ustvarite enkrat in jih uporabljajte znova, saj se njihovo ponavljajoče ustvarjanje v veliki tabeli hitro nabere. Število decimalnih mest pa določite tako v podatkovnem sloju kot v uporabniškem vmesniku, da se izvozi in zaslon ujemajo.
Fonti in pokritost znakov#
Dvaindvajset jezikov pomeni latinico, razširjeno latinico (srbščina, hrvaščina, turščina), cirilico (srbščina, ruščina, ukrajinščina, bolgarščina), grščino in znake CJK. Le redki fonti blagovnih znamk pokrijejo vse to, tisti, ki jih, pa so veliki.
Kaj je pomembno:
- Pokritost preverite za vsako pisavo posebej, preden izberete font. Preizkušajte z resničnimi nizi, ne z „Lorem ipsum“. Srbska cirilica ima svoje črke in lokalne ležeče oblike, ukrajinščina ima črke, ki jih ruščina nima, bolgarščina pa daje prednost lastnim oblikam znakov.
- Podnabore razdelite po pisavah in jih nalagajte glede na jezik.
next/fontpodpira podnabore, kot solatin,latin-ext,cyrillicingreek. Nemška stran naj ne prenaša ciriličnih znakov. - Celotnega fonta CJK ne gostite sami na vsaki strani. Kitajski fonti imajo lahko več megabajtov. Sklad sistemskih fontov za CJK, na primer
"PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif, je pogosto pravi kompromis. - Zasnujte izrecen sklad nadomestnih fontov in nastavite metrično združljive nadomestke, da se besedilo ob nalaganju spletnega fonta ne premakne.
Prevajalski proces, ki zdrži 22 jezikov#
Pri dveh ali treh jezikih zadoščata preglednica in nekaj discipline. Pri 22 potrebujete cevovod.
Katalogi sporočil s stabilnimi ključi. Uporabite eno datoteko JSON na jezik, s ključi, poimenovanimi po pomenu, na primer rates.table.buy, in nikoli po angleškem besedilu. Za množino in interpolacijo uporabite ICU MessageFormat. Pravila za množino se zelo razlikujejo: ruščina, ukrajinščina in srbščina imajo več oblik, kitajščina pa nobene.
Usklajenost ključev v CI. Manjkajoč ključ v enem od jezikov je najpogostejša napaka večjezične strani in hkrati tista, ki jo je najlažje ujeti samodejno:
// scripts/check-i18n.ts — run in CI, fail the build on drift
import { readdirSync, readFileSync } from "node:fs";
const dir = "messages";
const flatten = (o: Record<string, unknown>, p = ""): string[] =>
Object.entries(o).flatMap(([k, v]) =>
v && typeof v === "object" ? flatten(v as Record<string, unknown>, `${p}${k}.`) : [`${p}${k}`]
);
const load = (f: string) => new Set(flatten(JSON.parse(readFileSync(`${dir}/${f}`, "utf8"))));
const source = load("en.json");
let failed = false;
for (const file of readdirSync(dir).filter((f) => f.endsWith(".json") && f !== "en.json")) {
const keys = load(file);
const missing = [...source].filter((k) => !keys.has(k));
const extra = [...keys].filter((k) => !source.has(k));
if (missing.length || extra.length) {
failed = true;
console.error(`${file}: missing ${missing.length}, extra ${extra.length}`, { missing, extra });
}
}
process.exit(failed ? 1 : 0);
Isto skripto razširite, da preveri še ujemanje interpolacijskih ograd med jeziki. Prevedeno ime ograde se zlomi med izvajanjem in ne med gradnjo.
Prevajanje s pomočjo AI, s človeškim pregledom. Strojno prevajanje in prevajanje z modeli LLM danes dajeta dobre prve osnutke, pri 22 jezikih pa to spremeni ekonomiko. Vendar ima menjalnica opravka z denarjem in zaupanjem. Oznake tečajev, pravna obvestila in naložbene vodnike naj pred objavo pregleda človek, ki jezik tekoče obvlada. Prevajalcem dajte kontekst: posnetke zaslona, omejitve števila znakov in glosar ustaljenih izrazov, kot so „nakupni tečaj“, „prodajni tečaj“ in „referenčni tečaj“.
Podatki v realnem času brez zastarelih vrednosti#
Menjalni tečaji se čez dan spreminjajo. Past je v tem, da jih predpomnite enako agresivno kot preostanek pretežno statične strani Next.js.
Splošne strategije, ki jih priporočamo:
- Ločite ogrodje strani od podatkov. Postavitev strani, prevodi in vodniki so lahko statični ali se revalidirajo redko. Tečajna tabela naj ima svoje kratko okno revalidacije ali pa naj se pridobi v odjemalcu oziroma pretaka.
- Revalidacija naj bo kratka in premišljena. Izberite okno, ki ustreza temu, kako pogosto se tečaji dejansko spreminjajo, in ga dokumentirajte. Kjer je mogoče, ob objavi novih tečajev uporabite revalidacijo na zahtevo, namesto da se zanašate samo na časovnik.
- Na robu omrežja predpomnite previdno. Kratek
s-maxagesstale-while-revalidateohranja strani hitre, vendar poskrbite, da je okno zastarelosti takšno, da ga poslovanje lahko sprejme. - Vedno prikažite časovni žig „stanje na dan“, oblikovan po jeziku in v časovnem pasu poslovalnice. To je pošten odgovor na vprašanje, ki ga sproži vsak sistem predpomnjenja: kako sveže je to?
En vir resnice za PDF, Excel, CSV in XML#
GAGA svojo tečajnico objavlja za prenos v formatih PDF, JPG, Excel, CSV in XML. Pet formatov pomeni pet priložnosti, da se številke ne ujemajo.
Pravilo: vsak izvoz zgradite iz iste normalizirane podatkovne strukture, tiste, iz katere se izriše tudi tabela na zaslonu. Zaokroževanje, vrstni red in metapodatke o valutah umestite v to strukturo in ne v vsak izvoznik posebej. Vsak format je potem le tanek serializator. Na kaj je treba računati:
- CSV potrebuje navedeno ločilo in kodiranje. Excel v številnih evropskih jezikovnih nastavitvah pričakuje podpičje, UTF-8 pa bolje obdela z BOM.
- Excel naj dobi prave številske celice s številskimi oblikami in ne vnaprej oblikovanih nizov, da lahko uporabniki z njimi računajo.
- XML potrebuje stabilno, dokumentirano shemo, saj bo nekdo nanjo vezal svojo integracijo.
- PDF in slike morajo vdelati fonte, ki pokrivajo zahtevano pisavo, kar vas vrne k pokritosti znakov.
Časovni žig „stanje na dan“ dodajte tudi v vsak izvoz.
SEO v številnih jezikih#
Poleg hreflang in zemljevidov strani:
- Naslove, opise in metapodatke Open Graph prevedite za vsak jezik. Na grški strani ne puščajte angleških metapodatkov.
- Sluge lokalizirajte le, če jih lahko ohranite stabilne. Spremenjen slug v enem jeziku pomeni preusmeritve in porušeno gručo hreflang.
- Izogibajte se vsebinsko šibkim dvojnikom. Če ima jezik le delno vsebino, razmislite, ali naj se sploh že indeksira.
- Kanonični URL-ji naj kažejo na stran samo, nikoli na drugo jezikovno različico.
Dostopnost#
Na elementu html za vsak jezik nastavite lang s celotno oznako (sr-Latn, sr-Cyrl), da bralniki zaslona izberejo pravi glas in izgovorjavo. Ko se na strani pojavi besedna zveza v drugem jeziku, jo označite z njenim lang.
Tudi če danes ne podpirate jezika, ki se piše z desne proti levi, to načrtujte. Uporabljajte logične lastnosti CSS, kot je margin-inline-start namesto margin-left, in dir izpeljite iz jezika. Arabščino je pozneje veliko ceneje dodati, če postavitev ne predpostavlja smeri z leve proti desni.
Testiranje: poti × jeziki#
Pri 22 jezikih je napako, ki se pokaže le v enem od njih, zlahka spregledati. Priporočamo samodejen dimni test (smoke test), ki gre čez vsako javno pot in vsak jezik ter preveri osnove:
- stran vrne 200 in se izriše brez napak med izvajanjem;
htmlima pravilenlang;- alternative hreflang so popolne in kažejo na URL-je, ki obstajajo;
- vidni niso nobeni surovi ključi sporočil (kot je
rates.table.buy) ali prazni nizi; - pretvornik in tečajna tabela številke izrišeta v pričakovani obliki.
Dodajte vizualne posnetke za najdaljše jezike. Nemške in grške oznake pogosto porušijo postavitve, ki so bile v angleščini videti v redu.
Kaj je treba načrtovati#
Če začenjate večjezično platformo Next.js, so to odločitve, ki jih je treba sprejeti prvi dan: strategija URL-jev z jezikovno predpono, en register jezikov, ki poganja usmerjanje, metapodatke in zemljevide strani, jasna pravila za pisave in transliteracijo, Intl za vse oblikovanje, načrt fontov za vsako pisavo posebej, preverjanje usklajenosti prevodov v CI, izrecna pravila o svežini podatkov v živo in en sam vir resnice za vsak izvoz.
Nič od tega ni eksotično, vendar je veliko ceneje, če je del zasnove, kot če ga je treba dodajati naknadno. Naše delo vodi tehnični vodja z več kot 20 leti izkušenj, prav takšni temelji pa so v središču našega dela na področju Splet in platforme. Če načrtujete kaj podobnega, nam pišite.