Pređi na sadržaj

Članci

Izrada Next.js platforme na 22 jezika: pouke iz projekta GAGA

Rutiranje po jezicima, srpski na latinici i ćirilici, Intl formatiranje, fontovi, CI za prevode i sveži kursevi: pouke sa platforme na 22 jezika.

Inženjerski tim sigmacode.io10 min čitanja

Na ovoj stranici (11)
  1. Svaka jezička verzija treba sopstveni URL
  2. Dva pisma, jedan jezik
  3. Brojevi, valute i datumi uz Intl
  4. Fontovi i pokrivenost glifova
  5. Proces prevođenja koji izdržava 22 jezika
  6. Podaci u realnom vremenu bez zastarelih vrednosti
  7. Jedan izvor istine za PDF, Excel, CSV i XML
  8. SEO za mnogo lokalizacija
  9. Pristupačnost
  10. Testiranje: rute × lokalizacije
  11. Šta planirati

GAGA Menjačnica je menjačnica i trgovac plemenitim metalima sa nekoliko poslovnica u Novom Sadu. Naš tim je izradio njihovu platformu menjacnicegaga.rs u Next.js-u i React-u, sa hostingom na Vercelu. Platforma radi na 22 jezika, među kojima su srpski na latinici i ćirilici, nemački, kineski, ruski, turski, ukrajinski, grčki i bugarski. Prikazuje aktuelne kupovne i prodajne kurseve pored referentnih kurseva Narodne banke Srbije, a uključuje i konvertor valuta, kursne liste za preuzimanje u pet formata, pretragu poslovnica i vodiče za investiciono zlato i srebro.

Sa 22 jezika internacionalizacija više nije funkcionalnost, već oblikuje arhitekturu. U ovom tekstu opisujemo šta je po našem iskustvu važno kada se gradi ovakva platforma. Namenjen je tehničkim direktorima, vlasnicima proizvoda i frontend inženjerima koji planiraju sličan projekat. Više o samom projektu pročitajte u studiji slučaja GAGA.

Svaka jezička verzija treba sopstveni URL#

Najvažnija odluka dolazi prva: svaka jezička verzija svake stranice treba sopstveni, stabilan URL koji pretraživači mogu da indeksiraju. Ne menjajte jezik pomoću kolačića ili čitanjem zaglavlja Accept-Language. Pretraživači to ne mogu da indeksiraju, korisnici ne mogu da podele, a CDN-ovi ne mogu čisto da keširaju.

Preporučujemo jezički prefiks u putanji, na primer /sr/..., /de/... i /zh/.... U Next.js App Routeru to znači segment [locale] u korenu aplikacije. Proxy za zahteve (proxy.ts, ranije middleware) može prvu posetu da preusmeri na razuman podrazumevani jezik na osnovu jezika pregledača. Posle toga URL je jedini izvor istine, a birač jezika mora da vodi na istu stranicu na drugom jeziku, a ne na početnu stranicu tog jezika.

Identifikatore lokalizacija odredite rano i svuda koristite BCP 47 oznake. Samo srpskom trebaju dve: sr-Latn i sr-Cyrl. Ako njihovo mapiranje na URL segmente, hreflang vrednosti i Intl lokalizacije držite na jednom mestu, kasnije ćete izbeći mnogo problema.

hreflang, x-default i sitemape#

Svaka stranica treba da navede sve svoje jezičke alternative, uključujući sebe, i da doda x-default za korisnike čiji jezik ne podržavate. U App Routeru to umesto vas radi Metadata API:

ts
// 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 generišite iz iste liste lokalizacija i istog registra ruta, sa alternativama za svaki unos. Čim hreflang oznake i unosi u sitemapi nastaju u odvojenim delovima koda, pre ili kasnije će se razići, a pretraživači će tiho prestati da veruju i jednima i drugima.

Dva pisma, jedan jezik#

Srpski se piše i latinicom i ćirilicom, a mnogi čitaoci imaju jasnu sklonost prema jednom od njih. Tretirajte ih kao dve potpune lokalizacije, a ne kao jednu lokalizaciju sa prekidačem za prikaz. Svaka dobija sopstveni URL, sopstvenu hreflang vrednost i sopstveni 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. Ali nemojte ga slepo primenjivati na sve:

  • Vlastita imena i brendovi. Nazivi firmi i proizvoda, kao i strane reči, često i u ćiriličnom tekstu zadržavaju latinični oblik. Mehanički konvertor će od njih „uslužno“ napraviti nešto što niko nije napisao.
  • Dvosmislenost digrafa. Latinični nj, lj i obično odgovaraju jednom ćiriličnom slovu, ali ne uvek. Složenice i pozajmljenice krše to pravilo. Konverzija iz ćirilice u latinicu je jednoznačna, a obrnuto nije.
  • URL-ovi, kodovi i identifikatori. Oznake valuta kao što je EUR, imejl adrese, slugovi i sve unutar interpolacionih placeholdera nikada ne smeju da se transliterišu.
  • Pretraga i sortiranje. Korisnici na ćiriličnoj stranici mogu u polje za pretragu da ukucaju latinicu. Normalizujte obe strane pre poređenja.

Naša preporuka: čuvajte ćirilični izvor (jednoznačni smer) ili vodite dva odvojena kataloga, uz malu listu izuzetaka o kojoj brine izvorni govornik. Sve što je proizvela mašina treba pregledati pre objavljivanja.

Brojevi, valute i datumi uz Intl#

Na sajtu sa kursevima brojevi su proizvod. Srpski koristi zarez kao decimalni separator, nemački razdvaja hiljade tačkom, a kineski korisnici očekuju opet drugačije konvencije. Ne formatirajte ručno. Koristite ugrađene Intl API-je i prosleđujte punu oznaku lokalizacije:

ts
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 renderovane na serveru inače koriste zonu servera, najčešće UTC. Formatere napravite jednom i koristite ih ponovo, jer se njihovo stalno pravljenje u velikoj tabeli sabira. Broj decimala odredite i u sloju podataka, a ne samo u interfejsu, kako bi se izvozi i prikaz na ekranu poklapali.

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 koji brend font pokriva sve to, a oni koji pokrivaju su veliki.

Šta je važno:

  • Proverite pokrivenost za svako pismo pre izbora fonta. Testirajte stvarnim tekstom, a ne sa „Lorem ipsum“. Srpska ćirilica ima sopstvena slova i lokalne kurzivne oblike, ukrajinski ima slova kojih nema u ruskom, a bugarski preferira sopstvene oblike glifova.
  • Pravite podskupove po pismu i učitavajte ih po lokalizaciji. next/font podržava podskupove kao što su latin, latin-ext, cyrillic i greek. Nemačka stranica ne bi trebalo da preuzima ćirilične glifove.
  • Ne hostujte sami ceo CJK font na svakoj stranici. Kineski fontovi mogu da imaju nekoliko megabajta. Sistemski niz fontova za CJK, na primer "PingFang SC", "Microsoft YaHei", "Noto Sans SC", sans-serif, često je pravi kompromis.
  • Definišite eksplicitan niz rezervnih fontova sa metrički kompatibilnim zamenama, kako se tekst ne bi pomerao kada se veb font učita.

Proces prevođenja koji izdržava 22 jezika#

Za dva ili tri jezika dovoljni su tabela i malo discipline. Za 22 vam treba pipeline.

Katalozi poruka sa stabilnim ključevima. Koristite jedan JSON fajl po lokalizaciji, sa ključevima imenovanim prema značenju, na primer rates.table.buy, a nikada prema engleskom tekstu. Za množinu i interpolaciju koristite ICU MessageFormat. Pravila množine se znatno 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 sajtovima, a ujedno ju je najlakše automatski otkriti:

ts
// 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 proverom da li se interpolacioni placeholderi poklapaju u svim lokalizacijama. Prevedeno ime placeholdera puca tek u toku izvršavanja, a ne pri buildu.

Prevođenje uz pomoć veštačke inteligencije, sa ljudskom proverom. Mašinsko prevođenje i prevođenje LLM-ovima danas daju dobre prve verzije, a kod 22 jezika to menja ekonomiku projekta. Ali menjačnica posluje sa novcem i poverenjem. Nazive kurseva, pravna obaveštenja i investicione vodiče pre objavljivanja treba da pregleda osoba koja tečno govori jezik. Prevodiocima dajte kontekst: snimke ekrana, ograničenja broja karaktera i glosar ustaljenih pojmova kao što su „kupovni kurs“, „prodajni kurs“ i „referentni kurs“.

Podaci u realnom vremenu bez zastarelih vrednosti#

Kursevi se menjaju tokom dana. Zamka je keširati ih jednako agresivno kao ostatak Next.js sajta koji je pretežno statičan.

Opšte strategije koje preporučujemo:

  • Odvojite okvir stranice od podataka. Raspored, prevodi i vodiči mogu biti statični ili se retko revalidirati. Tabela kurseva treba da ima sopstveni, kratak prozor revalidacije ili da se učitava na klijentu, odnosno strimuje.
  • Revalidaciju držite kratkom i promišljenom. Izaberite prozor koji odgovara stvarnoj učestalosti promena kurseva i dokumentujte ga. Gde je moguće, koristite revalidaciju na zahtev pri objavljivanju novih kurseva, umesto da se oslanjate samo na tajmer.
  • Na edge-u keširajte oprezno. Kratak s-maxage uz stale-while-revalidate održava stranice brzim, ali proverite da li je prozor zastarelosti prihvatljiv za poslovanje.
  • Uvek 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 sistem keširanja: koliko su ovi podaci sveži?

Jedan izvor istine za PDF, Excel, CSV i XML#

GAGA objavljuje kursnu listu za preuzimanje u formatima PDF, JPG, Excel, CSV i XML. Pet formata znači pet prilika da se brojevi razilaze.

Pravilo: svaki izvoz generišite iz iste normalizovane strukture podataka, iste one koja renderuje tabelu na ekranu. Zaokruživanje, redosled i metapodaci o valutama pripadaju toj strukturi, a ne pojedinačnom izvozniku. Svaki format tada postaje tanak serializator. Šta treba planirati:

  • CSV treba definisan separator i kodiranje. Excel u mnogim evropskim lokalizacijama očekuje tačku i zarez i pouzdanije obrađuje UTF-8 sa BOM-om.
  • Excel treba da dobije prave numeričke ćelije sa formatima brojeva, a ne unapred formatirane stringove, kako bi korisnici mogli da računaju sa njima.
  • XML treba stabilnu, dokumentovanu šemu, jer će se neko integrisati na nju.
  • PDF i slike moraju da ugrade 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#

Pored hreflanga i sitemapa:

  • Prevedite naslove, opise i Open Graph metapodatke za svaku lokalizaciju. Ne ostavljajte engleske metapodatke na grčkoj stranici.
  • Slugove lokalizujte samo ako možete da ih održite stabilnim. Promenjen slug u jednoj lokalizaciji znači preusmeravanja i razbijenu hreflang grupu.
  • Izbegavajte sadržajno siromašne duplikate. Ako lokalizacija ima samo delimičan sadržaj, razmislite da li treba već da bude indeksirana.
  • Kanonski URL-ovi treba da upućuju na samu stranicu, nikada na drugu jezičku verziju.

Pristupačnost#

Postavite lang na element html za svaku lokalizaciju, sa punom oznakom (sr-Latn, sr-Cyrl), kako bi čitači ekrana izabrali pravi glas i izgovor. Kada se unutar stranice pojavi izraz na drugom jeziku, označite ga sopstvenim lang.

Čak i ako danas ne podržavate nijedan jezik koji se piše zdesna nalevo, planirajte ga. Koristite logička CSS svojstva kao što je margin-inline-start umesto margin-left i izvodite dir iz lokalizacije. Kasnije dodavanje arapskog je znatno jeftinije kada raspored ne pretpostavlja smer sleva nadesno.

Testiranje: rute × lokalizacije#

Sa 22 lokalizacije lako je propustiti grešku koja se javlja samo u jednoj od njih. Preporučujemo automatizovani smoke test koji prolazi kroz svaku javnu rutu i svaku lokalizaciju i proverava osnove:

  • stranica vraća 200 i renderuje se bez grešaka u toku izvršavanja;
  • html ima ispravan lang;
  • hreflang alternative su potpune i upućuju na URL-ove koji postoje;
  • nisu vidljivi sirovi ključevi poruka (kao što je rates.table.buy) ni prazni stringovi;
  • konvertor i tabela kurseva prikazuju brojeve u očekivanom formatu.

Dodajte vizuelne snimke za najduže jezike. Nemačke i grčke oznake često razbiju raspored koji je na engleskom izgledao dobro.

Šta planirati#

Ako započinjete višejezičnu Next.js platformu, ovo su odluke za prvi dan: URL strategija sa jezičkim prefiksom, jedan registar lokalizacija koji pokreće rutiranje, metapodatke i sitemape, jasna pravila za pisma i transliteraciju, Intl za svako formatiranje, plan fontova po pismu, CI provere pariteta prevoda, eksplicitna pravila svežine za podatke uživo i jedan izvor istine za svaki izvoz.

Ništa od toga nije egzotično, ali je mnogo jeftinije ugraditi to od početka nego naknadno. Naš rad vodi tehnički lider sa više od 20 godina iskustva, a upravo na te temelje se fokusiramo u oblasti Web i platforme. Ako planirate nešto slično, javite nam se.

Imate projekat na umu?

Recite nam šta razvijate. Dobićete iskrenu procenu, jasan obim i ponudu po fiksnoj ceni ili po fazama – obično u roku od nekoliko radnih dana.

Radije biste prvo pisali? Pišite nam