Članci
Izrada Next.js platforme na 22 jezika: pouke iz projekta GAGA
Usmjeravanje po jezicima, srpski na latinici i ćirilici, Intl formatiranje, fontovi, CI za prijevode i svježi tečajevi: pouke s platforme na 22 jezika.
Inženjerski tim sigmacode.io10 min čitanja
Na ovoj stranici (11)
- Svaka jezična verzija treba vlastiti URL
- Dva pisma, jedan jezik
- Brojevi, valute i datumi uz Intl
- Fontovi i pokrivenost glifova
- Proces prevođenja koji izdrži 22 jezika
- Podaci u stvarnom vremenu bez zastarjelih vrijednosti
- Jedan izvor istine za PDF, Excel, CSV i XML
- SEO za mnogo lokalizacija
- Pristupačnost
- Testiranje: rute × lokalizacije
- Što planirati
GAGA Menjačnica je mjenjačnica i trgovac plemenitim metalima s nekoliko poslovnica u Novom Sadu u Srbiji. Naš tim izradio je njihovu platformu menjacnicegaga.rs u Next.js-u i Reactu, s hostingom na Vercelu. Platforma radi na 22 jezika, među kojima su srpski na latinici i ćirilici, njemački, kineski, ruski, turski, ukrajinski, grčki i bugarski. Prikazuje aktualne kupovne i prodajne tečajeve uz referentne tečajeve Narodne banke Srbije, a uključuje i preračunavanje valuta, tečajne liste za preuzimanje u pet formata, pretraživanje poslovnica te vodiče za investicijsko zlato i srebro.
S 22 jezika internacionalizacija više nije značajka, nego oblikuje arhitekturu. U ovom članku opisujemo što je po našem iskustvu važno pri izradi takve platforme. Namijenjen je tehničkim direktorima, vlasnicima proizvoda i frontend inženjerima koji planiraju sličan projekt. Više o samom projektu pročitajte u studiji slučaja GAGA.
Svaka jezična verzija treba vlastiti URL#
Najvažnija odluka dolazi prva: svaka jezična verzija svake stranice treba vlastiti, stabilan URL koji tražilice mogu indeksirati. Ne mijenjajte jezik kolačićem ili čitanjem zaglavlja Accept-Language. Tražilice to ne mogu indeksirati, korisnici to ne mogu podijeliti, a CDN-ovi to ne mogu čisto predmemorirati.
Preporučujemo jezični prefiks u putanji, primjerice /sr/..., /de/... i /zh/.... U Next.js App Routeru to znači segment [locale] u korijenu aplikacije. Proxy za zahtjeve (proxy.ts, ranije middleware) može prvi posjet preusmjeriti na razuman zadani jezik prema jeziku preglednika. Nakon toga URL je jedini izvor istine, a birač jezika mora voditi na istu stranicu na drugom jeziku, a ne na početnu stranicu tog jezika.
Identifikatore lokalizacija odredite rano i posvuda koristite BCP 47 oznake. Samo srpski treba dvije: sr-Latn i sr-Cyrl. Ako njihovo preslikavanje na URL segmente, hreflang vrijednosti i Intl lokalizacije držite na jednom mjestu, kasnije ćete si uštedjeti mnogo problema.
hreflang, x-default i sitemape#
Svaka stranica treba navesti sve svoje jezične alternative, uključujući sebe, te dodati x-default za korisnike čiji jezik ne podržavate. U App Routeru to za vas radi Metadata API:
// 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 },
};
}
Sitemapu generirajte iz istog popisa lokalizacija i istog registra ruta, s alternativama za svaki unos. Čim hreflang oznake i unosi u sitemapi nastaju u odvojenim dijelovima koda, prije ili kasnije će se razići, a tražilice će tiho prestati vjerovati i jednima i drugima.
Dva pisma, jedan jezik#
Srpski se piše i latinicom i ćirilicom, a mnogi čitatelji imaju jasnu sklonost jednom od njih. Tretirajte ih kao dvije potpune lokalizacije, a ne kao jednu lokalizaciju s prekidačem za prikaz. Svaka dobiva vlastiti URL, vlastitu hreflang vrijednost i vlastiti atribut lang.
Primamljivo je sve pisati jednim pismom, a drugo dobiti automatskom transliteracijom. Za tekući tekst dobro testiran korak transliteracije može biti razumno polazište. No nemojte ga slijepo primjenjivati na sve:
- Vlastita imena i robne marke. Nazivi tvrtki i proizvoda te strane riječi često i u ćiriličnom tekstu zadržavaju latinični oblik. Mehanički pretvarač „uslužno“ će od njih napraviti nešto što nitko nije napisao.
- Dvoznačnost dvoslova. Latinični
nj,ljidžobično odgovaraju jednom ćiriličnom slovu, ali ne uvijek. Složenice i posuđenice krše to pravilo. Pretvorba iz ćirilice u latinicu je jednoznačna, a obrnuto nije. - URL-ovi, kodovi i identifikatori. Oznake valuta poput
EUR, adrese e-pošte, slugovi i sve unutar interpolacijskih rezerviranih mjesta nikada se ne smiju transliterirati. - Pretraživanje i sortiranje. Korisnici mogu na ćiriličnoj stranici u tražilicu upisati latinicu. Normalizirajte obje strane prije usporedbe.
Naša preporuka: pohranjujte ćirilični izvor (jednoznačni smjer) ili vodite dva odvojena kataloga, uz mali popis iznimaka za koji je odgovoran izvorni govornik. Sve što je proizveo stroj treba pregledati prije objave.
Brojevi, valute i datumi uz Intl#
Na stranici s tečajevima brojevi su proizvod. Srpski koristi zarez kao decimalni separator, njemački odvaja tisućice točkom, a kineski korisnici očekuju opet drukčije konvencije. Ne formatirajte ručno. Koristite ugrađene Intl API-je i prosljeđujte punu oznaku lokalizacije:
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)
Nekoliko stvari koje treba planirati. Vremensku zonu postavite eksplicitno, jer stranice renderirane na poslužitelju inače koriste zonu poslužitelja, obično UTC. Formatere stvorite jednom i koristite ih ponovno, jer se njihovo stalno stvaranje u velikoj tablici zbraja. Broj decimalnih mjesta odredite i u podatkovnom sloju, a ne samo u sučelju, kako bi se izvozi i prikaz na zaslonu podudarali.
Fontovi i pokrivenost glifova#
22 jezika znače latinicu, proširenu latinicu (srpski, hrvatski, turski), ćirilicu (srpski, ruski, ukrajinski, bugarski), grčki alfabet i CJK znakove. Malo koje korporativno pismo pokriva sve to, a ona koja pokrivaju velika su.
Što je važno:
- Provjerite pokrivenost za svako pismo prije odabira fonta. Testirajte stvarnim tekstom, a ne s „Lorem ipsum“. Srpska ćirilica ima vlastita slova i lokalne kurzivne oblike, ukrajinski ima slova kojih nema u ruskom, a bugarski preferira vlastite oblike glifova.
- Podskupove radite po pismu i učitavajte ih po lokalizaciji.
next/fontpodržava podskupove poputlatin,latin-ext,cyrillicigreek. Njemačka stranica ne bi trebala preuzimati ćirilične glifove. - Ne hostajte sami puni CJK font na svakoj stranici. Kineski fontovi mogu imati nekoliko megabajta. Sistemski niz fontova za CJK, primjerice
"PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif, često je pravi kompromis. - Definirajte izričit niz zamjenskih fontova s metrički kompatibilnim zamjenama, kako se tekst ne bi pomicao kad se web font učita.
Proces prevođenja koji izdrži 22 jezika#
Za dva ili tri jezika dovoljni su tablica i malo discipline. Za 22 treba vam pipeline.
Katalozi poruka sa stabilnim ključevima. Koristite jednu JSON datoteku po lokalizaciji, s ključevima imenovanima prema značenju, primjerice rates.table.buy, a nikada prema engleskom tekstu. Za množinu i interpolaciju koristite ICU MessageFormat. Pravila množine znatno se razlikuju: ruski, ukrajinski i srpski imaju nekoliko oblika, a kineski nijedan.
Paritet ključeva u CI-ju. Ključ koji nedostaje u jednoj lokalizaciji najčešća je greška na višejezičnim stranicama, a ujedno ju je najlakše automatski otkriti:
// 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);
Istu skriptu proširite provjerom podudaraju li se interpolacijska rezervirana mjesta u svim lokalizacijama. Prevedeno ime rezerviranog mjesta puca tek pri izvođenju, a ne pri buildu.
Prevođenje uz pomoć umjetne inteligencije, s ljudskom provjerom. Strojno prevođenje i prevođenje LLM-ovima danas daju dobre prve verzije, a kod 22 jezika to mijenja ekonomiku projekta. No mjenjačnica posluje s novcem i povjerenjem. Nazive tečajeva, pravne napomene i investicijske vodiče prije objave treba pregledati osoba koja tečno govori jezik. Prevoditeljima dajte kontekst: snimke zaslona, ograničenja broja znakova i glosar ustaljenih pojmova poput „kupovni tečaj“, „prodajni tečaj“ i „referentni tečaj“.
Podaci u stvarnom vremenu bez zastarjelih vrijednosti#
Tečajevi se mijenjaju tijekom dana. Zamka je predmemorirati ih jednako agresivno kao ostatak Next.js stranice koja je pretežno statična.
Opće strategije koje preporučujemo:
- Odvojite ljusku od podataka. Izgled stranice, prijevodi i vodiči mogu biti statični ili se rijetko revalidirati. Tablica tečajeva treba vlastiti, kratak prozor revalidacije ili se dohvaćati na klijentu odnosno streamati.
- Revalidaciju držite kratkom i promišljenom. Odaberite prozor koji odgovara stvarnoj učestalosti promjena tečajeva i dokumentirajte ga. Gdje je moguće, koristite revalidaciju na zahtjev pri objavi novih tečajeva umjesto da se oslanjate samo na tajmer.
- Na rubu (edge) predmemorirajte oprezno. Kratak
s-maxageuzstale-while-revalidateodržava stranice brzima, ali provjerite je li prozor zastarjelosti prihvatljiv za poslovanje. - Uvijek prikažite vremensku oznaku „stanje na dan“, formatiranu prema lokalizaciji i u vremenskoj zoni poslovnice. To je pošten odgovor na pitanje koje otvara svaki sustav predmemoriranja: koliko su ovi podaci svježi?
Jedan izvor istine za PDF, Excel, CSV i XML#
GAGA objavljuje tečajnu listu za preuzimanje u formatima PDF, JPG, Excel, CSV i XML. Pet formata znači pet prilika da se brojevi razilaze.
Pravilo: svaki izvoz generirajte iz iste normalizirane podatkovne strukture, one iste koja renderira tablicu na zaslonu. Zaokruživanje, redoslijed i metapodaci o valutama pripadaju toj strukturi, a ne pojedinom izvozniku. Svaki format tada postaje tanak serializator. Što treba planirati:
- CSV treba definirani separator i kodiranje. Excel u mnogim europskim lokalizacijama očekuje točku sa zarezom i pouzdanije obrađuje UTF-8 s BOM-om.
- Excel treba dobiti prave numeričke ćelije s formatima brojeva, a ne unaprijed formatirane nizove znakova, kako bi korisnici s njima mogli računati.
- XML treba stabilnu, dokumentiranu shemu, jer će se netko na nju integrirati.
- PDF i slike moraju ugraditi fontove koji pokrivaju traženo pismo, čime se vraćate na pokrivenost glifova.
Vremensku oznaku „stanje na dan“ stavite i u svaki izvoz.
SEO za mnogo lokalizacija#
Osim hreflanga i sitemapa:
- Prevedite naslove, opise i Open Graph metapodatke za svaku lokalizaciju. Ne ostavljajte engleske metapodatke na grčkoj stranici.
- Slugove lokalizirajte samo ako ih možete održati stabilnima. Promijenjen slug u jednoj lokalizaciji znači preusmjeravanja i razbijenu hreflang skupinu.
- Izbjegavajte sadržajno siromašne duplikate. Ako lokalizacija ima samo djelomičan sadržaj, razmislite treba li je već indeksirati.
- Kanonski URL-ovi trebaju upućivati na samu stranicu, nikada na drugu jezičnu verziju.
Pristupačnost#
Postavite lang na element html za svaku lokalizaciju, s punom oznakom (sr-Latn, sr-Cyrl), kako bi čitači zaslona odabrali pravi glas i izgovor. Kad se unutar stranice pojavi izraz na drugom jeziku, označite ga vlastitim lang.
Čak i ako danas ne podržavate nijedan jezik koji se piše zdesna nalijevo, planirajte ga. Koristite logička CSS svojstva poput margin-inline-start umjesto margin-left i izvodite dir iz lokalizacije. Kasnije dodavanje arapskog znatno je jeftinije kad raspored ne pretpostavlja smjer slijeva nadesno.
Testiranje: rute × lokalizacije#
Uz 22 lokalizacije lako je propustiti grešku koja se javlja samo u jednoj od njih. Preporučujemo automatizirani smoke test koji prolazi kroz svaku javnu rutu i svaku lokalizaciju i provjerava osnove:
- stranica vraća 200 i renderira se bez grešaka pri izvođenju;
htmlima ispravanlang;- hreflang alternative su potpune i upućuju na URL-ove koji postoje;
- nisu vidljivi sirovi ključevi poruka (poput
rates.table.buy) ni prazni nizovi; - preračunavanje valuta i tablica tečajeva prikazuju brojeve u očekivanom formatu.
Dodajte vizualne snimke za najduže jezike. Njemačke i grčke oznake često razbiju raspored koji je na engleskom izgledao dobro.
Što planirati#
Ako započinjete višejezičnu Next.js platformu, ovo su odluke za prvi dan: URL strategija s jezičnim prefiksom, jedan registar lokalizacija koji pokreće usmjeravanje, metapodatke i sitemape, jasna pravila za pisma i transliteraciju, Intl za svako formatiranje, plan fontova po pismu, CI provjere pariteta prijevoda, izričita pravila svježine za podatke uživo te jedan izvor istine za svaki izvoz.
Ništa od toga nije egzotično, ali mnogo je jeftinije to ugraditi od početka nego naknadno. Naš rad vodi tehnički voditelj s više od 20 godina iskustva, a upravo na te temelje usredotočujemo se u području Web i platforme. Ako planirate nešto slično, javite nam se.