Blog/Webfejlesztés

LLM-fordítási pipeline i18n-hez: glosszárium, helyőrzők és review

Termék- és weboldalszövegek fordítása LLM-mel úgy, hogy a helyőrzők, a többes szám és a hangnem épen maradjon: glosszárium, ICU üzenetek, QA-ellenőrzések.

··9 perc olvasás

  • Translation
  • i18n
  • LLM
  • Vue
  • Localization
Három forráskártya, egy lefordított kártya glosszárium-szalaggal, majd ellenőrzött tábla.

A lényeg röviden

  • A helyőrzőket, tageket és az ICU szintaxist még a modell előtt védd le, utána pedig ellenőrizd; ez a vizsgálat szinte minden valódi hibát megfog.
  • Értékeket fordíts, soha ne kulcsokat, és tartsd változatlanul a fájlszerkezetet, hogy a build diffelhető és átnézhető maradjon.
  • A glosszárium és a ne-fordítsd lista több konzisztenciát ad, mint bármilyen prompt-szövegezés, mert géppel kikényszeríthető.
  • A németnél és a magyarnál előre kell dönteni: tegezés vagy magázás, összetett szavak kezelése és az idézőjelek stílusa.
  • A modell fordít, a szkriptek ellenőriznek, az ember pedig csak azt a szöveget nézi át, ahol egy hiba drága.

Cikk meghallgatása

0:000:00

Minden termékweboldal előbb-utóbb kap egy fordítási folyamatot, akár tervezik, akár nem. A szokásos változat egy táblázat, egy egyszer megírt szkript és az a szabály, hogy a német szövegeknek a release-ág levágása előtt készen kell lenniük. Ez addig működik, amíg egy helyőrzőt át nem neveznek, el nem tűnik egy többes számú alak, vagy ugyanaz a gombfelirat háromféleképpen nem lesz lefordítva.

Ez a bejegyzés azt a folyamatot írja le, amelyet Vue és Nuxt oldalakon használok: a szövegek JSON-ban élnek, a modell glosszáriummal és explicit kontextussal fordítja az értékeket, a szkriptek gépi ellenőrzést végeznek, az ember pedig csak a drága hibák kockázatát hordozó szövegeket nézi át. Ez szándékosan unalmas, és pont ez a lényeg: a fordítás legyen build-lépés, ne esemény.

Mi törik el valójában

A sima prózai szöveg gépi fordítása az európai nyelveknél megoldott kérdés. Szoftverben az törik el, amit a mondat nem hordoz: a szöveg belsejében lévő {count}, a link körüli HTML-tag, a többes szám, amelyre a németnek és a magyarnak is szüksége van, a terméknév, amelynek angolul kell maradnia.

  • Egy eltűnt vagy átnevezett helyőrző, amely a működő oldalt futásidejű hibává vagy üres hellyé teszi.
  • Egy elrontott ICU üzenet, mert a többes szám kategóriái nyelvenként eltérnek, összefűzéssel pedig nem fejezhetők ki.
  • Lefordított HTML- vagy Markdown-tagek, amelyek vagy a megjelenítést törik el, vagy a látható szövegbe kerülnek.
  • Következetlen terminológia: ugyanazt a funkciót három néven említi egy oldal.
  • Hangnem-eltolódás, amikor egy formális német felület hirtelen tegezni kezdi az olvasót.

A folyamat

A fordítási folyamat lépéseiA forrásszövegek kinyerése, kontextussal és glosszáriummal fordítása, szkriptes ellenőrzése, szükség esetén emberi átnézése és a builddel való publikálása.Kinyeréslocale JSONFordításkontextus + glosszáriumEllenőrzésszkriptekReviewdiff nézetPublikálásbuild
Öt lépés: kinyerés, fordítás, ellenőrzés, review, publikálás. Csak kettőben van modell.

A kinyerés az egyetlen lépés, amely forrásszövegeket állít elő. A komponensbe égetett szöveg soha nem lesz lefordítva, ezért az első hasznos ellenőrzés egy linter, amely hibát jelez a sablonokban látható szövegkonstansokra. A fordítási lépés állapotmentes: beolvassa a forrásfájlt, a glosszáriumot és ugyanazon kulcs korábbi fordítását, majd új értékeket ír.

Először a helyőrzők és tagek védelme

A helyőrző szerződés a szöveg és az azt megjelenítő kód között. A modell nem fordíthatja le, nem rendezheti át és nem hagyhatja el, a kód pedig nem feltételezheti, hogy épen maradt. A megbízható minta: a kérések előtt kinyerni minden helyőrzőt, majd a válasz után összevetni a listát; az eltérés kemény hiba, nem figyelmeztetés.

import re

PLACEHOLDER = re.compile(r"\{[A-Za-z_][\w]*(?:,[^}]*)?\}|\{[0-9]+\}|%[sd]|</?[a-z][^>]*>")

def slots(text: str) -> list[str]:
    "Every placeholder and tag in the string, in order of appearance."
    return PLACEHOLDER.findall(text)

def check(source: str, target: str) -> list[str]:
    "Returns the differences; empty list means the translation is safe to ship."
    a, b = slots(source), slots(target)
    return [f"missing {s}" for s in a if s not in b] + [f"added {s}" for s in b if s not in a]

A tagek ugyanezt a kezelést érdemlik. Ha a forrás <strong>{name}</strong> szöveget tartalmaz, a fordítás áthelyezheti a taget, de mindkét felét és a helyőrzőt meg kell tartania. Egy egyszerű sorrendi összevetés a gyakori eseteket kapja el, egy HTML-parser a többit.

A többes szám és a select az ICU dolga

A német és a magyar is megkülönbözteti az egyes és többes számot, ezért a csak angolul helyes többes szám ártalmatlannak tűnik egészen az első nullás számlálóig. Az ICU MessageFormat ezt rendesen megoldja, a Vue I18n pedig beszéli az ICU-t, így az üzenet a locale-fájlban maradhat és egységként fordítható.

Szövegendehu
Kosár darabszám{count, plural, one {# item} other {# items}}{count, plural, one {# Artikel} other {# Artikel}}{count, plural, one {# termék} other {# termék}}
Tárhelyméret{size} GB of {total} GB used{size} GB von {total} GB belegt{size} GB a {total} GB-ból
<p>{{ $t('cart.items', { count: items.length }, items.length) }}</p>

// en.json
"cart": { "items": "{count, plural, one {# item} other {# items}}" }

// de.json
"cart": { "items": "{count, plural, one {# Artikel} other {# Artikel}}" }

// hu.json
"cart": { "items": "{count, plural, one {# termék} other {# termék}}" }

Két szokás tartja működésben ezt. Soha ne építs mondatot úgy, hogy egy lefordított töredéket számhoz vagy névhez fűzöl, mert a szórend eltér; a szám az üzeneten belül maradjon. És a folyamat soha ne lásson félig lefordított többes számot: az egész ICU üzenetet egyetlen szövegként fordítsd, ugyanazokkal a kategórianevekkel, és egy ellenőrzés igazolja, hogy a forrás minden kategóriája létezik a célban.

Értékeket fordíts, soha ne kulcsokat

A fordítási lépés egy JSON-objektumot kap, és azonos alakú objektumot ad vissza. A kulcsok azonosítók, érintetlenek maradnak, csak az értékek változnak. Strukturált kimenettel ez API-szinten kikényszeríthető, séma nélkül is ez a legegyszerűbben ellenőrizhető szerződés: a kulcshalmaznak a művelet előtt és után azonosnak kell lennie.

{
  "page": "checkout",                  // context for the model, not translated
  "glossary": [["cart", "Warenkorb"], ["checkout", "Kassa"]],
  "strings": {
    "cart.title": "Your cart",
    "cart.empty": "Your cart is empty",
    "cart.items": "{count, plural, one {# item} other {# items}}"
  }
}

A csoportosítás fontosabb, mint a méret. Az azonos oldalhoz, komponenshez vagy hangnemhez tartozó szövegek egy kérésbe kerülnek, mert a modell a szomszédokat kontextusként használja. Kérésenként 20-60 szöveg jó tartomány: az újrapróbálkozás olcsó, a kontextus pedig specifikus marad. Csak azt fordítsd újra, ami változott, a forrásérték hash-összevetésével, így az első teljes kör után a napi költség a nullához közelít.

Glosszárium és fordítási memória

A glosszárium a folyamat legolcsóbb konzisztenciaeszköze: forráskifejezések listája a jóváhagyott célkifejezésekkel, valamint írásmódra és ragozásra vonatkozó szabállyal. A modell bemenetként kapja, egy ellenőrzés pedig utólag kikényszeríti. Ha tiltott fordítás jelenik meg, a szöveg egyszer ismétlődik az idézett szabálysértéssel, utána pedig review-ba kerül, nem ciklusba.

ForrásnémetmagyarSzabály
cartWarenkorbkosárfőnév, németül nagybetűs
checkoutKassafizetésne fordítstd, ne ragozd
AI agentAI-AgentAI-ügynökaz angol kifejezés marad, első előfordulásnál magyarázattal

A fordítási memória a másik fél: a korábbi kiadásokból származó pontos egyezések modellhívás nélkül újrahasznosulnak, a közelítő egyezések javaslatként mennek tovább. Nyelvenkénti JSON-felállásban a memória csupán forrásszövegek és elfogadott célok táblája, a hatás ugyanaz: a modell csak az igazán újat látja, a reviewer pedig csak azt, ami a legutóbbi kiadás óta változott.

Német és magyar sajátosságok

A német a következetlenséget jobban bünteti, mint a nyelvtani hibát. Válassz egy megszólítási formát termékenként, dokumentáld, és tedd a glosszáriummal együtt szállított stílusútmutatóba; egy webshop, amely a láblécben magáz és a bevezetőben tegez, összerakottnak hat. A főnevek nagybetűsek maradnak, az összetett szavak egybe írandók, az idézőjelek pedig a német „így” formát követik.

A magyar agglutináló és nincs nyelvtani neme, ami két gyakorlati módon változtatja meg a folyamatot. Egyrészt a „felhasználó” vagy „ügyfél” nem vihető tovább névmással: a fordítás vagy megismétli a főnevet, vagy nélküle fogalmaz. Másrészt a toldalékok a főnévhez kapcsolódnak, ezért a toldalék belsejébe kerülő helyőrzőt kerülni kell, vagy át kell alakítani az üzenetet, amíg a toldalék a szó mellé kerül.

  • Nyelvenként egy oldalas hangnem-jegyzet, a glosszárium mellett a repóban.
  • Minden visszatérő kifejezés legyen glosszáriumelem, ne a promptban ismételt szöveg.
  • Egy anyanyelvi lektor egyszer javítsa a hangnem-jegyzetet, utána a javítások lint-szabállyá válnak.
  • A végeredményt a layoutban teszteld, ne a JSON-ban: a német és a magyar általában hosszabb az angolnál.

Ellenőrzések, amelyek szinte mindent elkapnak

Az első szakasz minden hibatípusához tartozik gépi ellenőrzés. Ugyanabban a commitban futtasd őket, mint a fordítást, ne egy későbbi takarító sprintben; egy folyamat, amely nem tud hangosan elbukni, csendben szállít hibás szövegeket.

// verify-locales.mjs - runs in CI, exits non-zero on the first real problem
const checks = [
  ['placeholder parity', (src, tgt) => sameSlots(src, tgt)],
  ['icu category parity', (src, tgt) => sameIcuCategories(src, tgt)],
  ['no empty values', (src, tgt) => tgt.trim().length > 0],
  ['glossary respected', (src, tgt) => glossaryViolations(src, tgt).length === 0],
  ['length sanity', (src, tgt) => tgt.length < src.length * 2.2],
]
  • Helyőrző- és tag-paritás, szövegenként és ICU üzenetenként.
  • Azonos kulcshalmaz a forrás- és a cél-locale fájlban.
  • Nincs üres vagy változatlan érték ott, ahol a forrás megváltozott.
  • A glosszárium-sértések a sértő kifejezéssel együtt, hogy az újrapróbálkozó prompt idézhesse.
  • Hosszarány-korlátok, amelyek a csonkítást és a szétrobbanó bővülést még a designer előtt jelzik.

Költség és átbocsátás

A fordítás az LLM-számla kis része, mert a szövegek rövidek. Ökölszabály: ezer darab, egyenként húsz szavas szöveg nagyjából 20k bemeneti és 25k kimeneti token; egy középkategóriás modellnél, három dollár per millió bemeneti és tizenöt dollár per millió kimeneti token áron ez körülbelül negyvenöt cent ezer szövegre. A csúcsmodatok ennek többszörösébe kerülnek, és itt ritkán térülnek meg.

KörMit látKöltségtényező
Első teljes futásMinden forrásszöveg és a glosszáriumForrásmennyiség, egyszer
KiadáskülönbségCsak a megváltozott forrású kulcsokVáltozási arány
Pontos memória-találatSemmit, memóriából másolvaNulla
ReviewDiffs, jelölt szabálysértések, mintákEmberi percek

Az átbocsátás kérdése fontosabb az árnál. Néhány ezer szöveg teljes köré percek alatt lefut és CI-ben is futtatható; a szűk keresztmetszet a review, ezért a folyamat a reviewereknek a megváltozott kulcsok diffjét és a megjelölt szövegek listáját mutassa, ne egy elejétől olvasandó fájlt.

Ahol az ember még számít

A review-kapu akkor éri meg az árát, ha a hiba drága: onboarding, árazási oldalak, jogi szövegek, bármi, amit az ügyfél visszaidézhet. A hibaüzenetek és beállításcímkék hosszú farkánál az automatikus ellenőrzések és a glosszárium jobbak egy sietős emberi körnél, és ennek tagadása csak a következő kiadásra tolja a munkát.

  1. A folyamat nyelvenkénti diffet és megjelölt szövegek listáját állítja elő.
  2. A reviewer csak a megjelölt és jól látható elemeket hagyja jóvá vagy javítja.
  3. A jóváhagyott értékek visszaíródnak, ami frissíti a fordítási memóriát.
  4. A build publikálja az oldalt, és az ellenőrzések a publikált artefaktumon újra lefutnak.

Ebből semmi sem egzotikus. Forrásszövegek JSON-ban, mellettük glosszárium, egy modell, amely értékeket fordít, egy szkript, amely szerződéseket ellenőriz, és egy ember, aki a diffet nézi. A folyamat szándékosan unalmas, mert egy termék érdekes részei soha nem a fordítások.

Források

  1. Unicode ICU User Guide: Formatting Messages
  2. Vue I18n: Message Format Syntax
  3. Nuxt i18n: Getting started
  4. Unicode CLDR: Plural rules
  5. OpenAI: Structured outputs
  6. DeepL API: Glossaries
  7. W3C: Localization vs. internationalization
  8. GNU gettext manual
  9. RFC 5646: Tags for Identifying Languages
  10. Rat für deutsche Rechtschreibung
  11. Microsoft Writing Style Guide

Gyakori kérdések

Kell még translation management rendszer?

Nem feltétlenül. Egy nyelvankénti JSON-fájl review-kapuval a repositoryban a legtöbb termékoldalhoz elég. Egy TMS akkor térül meg, ha külső fordítók, sok nyelv vagy release-ágak vannak.

Mekkora legyen egy fordítási batch?

Elég kicsi ahhoz, hogy egy hiba olcsón újrapróbálható legyen, elég nagy ahhoz, hogy legyen kontextus. A gyakorlatban 20-60 szöveg kérésenként, oldal vagy komponens szerint csoportosítva.

Láthatja a modell a teljes fájlt kontextusként?

Igen, csak olvasási segítségként. Küldd a forrásfájlt a fordítandó szövegek megjelölésével, és a válasz csak a lefordított értékeket tartalmazza. Így a prompt és a válaszséma is kicsi marad.

Mi a helyzet az olyan nyelvekkel, ahol nincs angol típusú többes szám?

Az ICU kezeli, de csak ha a forrásszöveg is ICU-t használ. A számlálókat és többes számokat először ICU üzenetbe kell tenni, és magát az üzenetet fordítani, nem az összefűzött mondatot.

Kell még utószerkesztés?

A jól látható szövegeknél igen. A ritkán olvasott hibaüzeneteknél és beállításcímkéknél egy jó glosszárium és az automatikus ellenőrzések általában jobbak, mint egy sietős emberi átfutás.

Pont erre van szükséged?

Írj a projektedről vagy a pozícióról – szívesen hallok felőled.