Updated
Chinees↔Engelse vertaling en localisatie via een chat-API
Het vertalen van UI-teksten en documenten met een chatmodel is eenvoudig te demonstreren, maar lastig in productie te brengen. De fouten zijn alledaags: een hernoemde placeholder, een glossariaterm op drie verschillende manieren vertaald, een JSON-antwoord ingewikkeld in prose. Deze handleiding behandelt vertaling als een pipeline met controles in elke fase, gebruikmakend van de OpenAI-compatible chat completions endpoint.
Denk in fasen, niet in één prompt
Een productievertaaltaak heeft vier fasen: strings voorbereiden, het model aanroepen, het resultaat valideren en het terug samenvoegen. De meeste kwaliteitsproblemen ontstaan door de validatiefase over te slaan. Een localization engineer zou nooit een vertaald bestand uitbrengen zonder de placeholder linter te draaien, en dezelfde discipline geldt wanneer de vertaler een model is.
Het endpoint is https://api.chinesellmapi.com/v1/chat/completions, model-id uncensored, Bearer-authenticatie. Het verwerkt beide richtingen, Engels naar Chinees en Chinees naar Engels, en de voorbeelden hieronder gebruiken Engelse brongegevens die in Vereenvoudigd Chinees worden omgezet. Wissel de richting in de system prompt af voor de omgekeerde situatie.
Houd rekening met het gedeelde contextvenster van 100.000 tokens, de standaard max_tokens van 2.048 (verhoog dit voor lange documenten, tot 32.000), en de limiet van 8 MB voor de request-body. Geen van deze drie beperkingen geldt voor UI-strings, maar alle drie zijn van belang voor volledige documenten.
Glossarium injecteren
Bedrijfsnamen, productnamen en juridisch gevoelige termen vereisen één vaste weergave. De goedkoopste mechanisme is een glossariumblok in de system prompt, met de bronterm en de vereiste doelterm. Formuleer de regel expliciet: gebruik het glossarium letterlijk bij elke vervoeging van de bronterm. Houd het glossarium kort, omdat elke regel als input wordt gefactureerd bij elke call; een paar tientallen entries zijn normaal, duizenden niet.
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.chinesellmapi.com/v1", api_key=os.environ["API_KEY"])
GLOSSARY = {
"workspace": "工作区",
"pull request": "合并请求",
"seat": "席位",
"billing cycle": "计费周期",
}
def build_system(glossary, target="Simplified Chinese"):
rows = "\n".join(f"- {src} => {dst}" for src, dst in glossary.items())
return (
f"You are a software localization translator. Translate English UI strings into {target}.\n"
"Rules:\n"
"1. Use the glossary below verbatim whenever the source term appears, in any inflection.\n"
"2. Copy placeholders such as {name}, %s, %d and {{count}} exactly, character for character.\n"
"3. Copy HTML tags and attributes exactly; translate only the text between tags.\n"
"4. Output the translation only, with no notes.\n\n"
"Glossary:\n" + rows
)
def translate(text):
r = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": build_system(GLOSSARY)},
{"role": "user", "content": text},
],
temperature=0.2,
max_tokens=400,
)
return r.choices[0].message.content.strip()
print(translate("Invite {name} to the workspace before the next billing cycle."))
Als je glossary groot is, filter deze dan per verzoek: neem alleen de verzoeken op waarvan de bronterm in de batch voorkomt. Een eenvoudige substring-test op lowercase tekst is voldoende voor een eerste versie en houdt de prompt klein. Stel de temperature in op ongeveer 0,2; creatieve variatie is een bug in UI-kopie.
Bepaal ook vooraf de style guide voor de doeltaal: formeel of informeel aanspreekvorm, of je spaties plaatst tussen Chinese karakters en ingebedde Latijnse woorden of getallen, en welke leestekenset je gebruikt. Zet deze beslissingen in dezelfde system prompt zodat elke batch ze overneemt.
Placeholders en markup intact houden
Placeholders breken op voorspelbare manieren: het model vertaalt de variabele naam, laat een trailing %s weg, of voegt spaties toe binnen accolades. Prompt rules verminderen deze fouten, maar kunnen ze niet elimineren, dus verifieer dit in code. Haal placeholders en tags uit source en target met een regular expression, en vergelijk ze vervolgens als gesorteerde lijsten. De volgorde kan legitiem veranderen tussen talen, dus vergelijk ze als multisets in plaats van sequenties.
import re
PLACEHOLDER = re.compile(r"\{\{?\w+\}?\}|%[sd]|</?[a-zA-Z][^>]*>")
def placeholders_ok(source: str, target: str) -> bool:
# Same multiset of placeholders and tags, regardless of order.
return sorted(PLACEHOLDER.findall(source)) == sorted(PLACEHOLDER.findall(target))
src = 'You have <b>{count}</b> unread messages in %s.'
bad = '你在 %s 中有 <b>{数量}</b> 条未读消息。'
good = '你在 %s 中有 <b>{count}</b> 条未读消息。'
print(placeholders_ok(src, bad), placeholders_ok(src, good)) # False True
Als de check faalt, probeer dan één keer opnieuw met het mislukte paar geciteerd terug in de prompt, bijvoorbeeld een user message die zegt dat de vorige uitvoer de placeholder heeft gewijzigd en moet worden gecorrigeerd. Als het nog steeds faalt, markeer de string dan voor menselijke review in plaats van te blijven herhalen. Rich text verdient extra zorg: vertaal bij voorkeur de text nodes en bouw de markup zelf opnieuw, omdat dit tag damage door constructie onmogelijk maakt.
Gestructureerde uitvoer via instructies
Het batchen van strings in één verzoek vereist een machine-readable reply. De API accepteert de standaard chat completion velden; gestructureerde uitvoer komt hier uit duidelijke instructies in plaats van een schema-enforcing switch, dus schrijf het contract in de prompt, voeg ids toe zodat je resultaten kunt realignen en parseer defensief. Modellen wrapen soms JSON in code fences of voegen een vriendelijke zin toe, dus verwijder fences voordat je parseert en behandel een parse error als een retryable event.
import json
import os
import re
from openai import OpenAI
client = OpenAI(base_url="https://api.chinesellmapi.com/v1", api_key=os.environ["API_KEY"])
SYSTEM = (
"Translate each item from English to Simplified Chinese. "
"Reply with a JSON object only, no code fences, no commentary. "
'Shape: {"items": [{"id": <number>, "zh": "<translation>"}]}. '
"Keep the same ids, keep every placeholder and HTML tag unchanged."
)
def translate_batch(strings):
payload = [{"id": i, "en": s} for i, s in enumerate(strings)]
r = client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": SYSTEM},
{"role": "user", "content": json.dumps(payload, ensure_ascii=False)},
],
temperature=0.2,
max_tokens=2000,
)
raw = r.choices[0].message.content.strip()
raw = re.sub(r"^```(?:json)?|```$", "", raw, flags=re.M).strip() # tolerate stray fences
data = json.loads(raw)
by_id = {item["id"]: item["zh"] for item in data["items"]}
return [by_id[i] for i in range(len(strings))]
print(translate_batch(["Save changes", "Delete {count} files?", "Welcome back, <b>{name}</b>"]))
Valideer altijd na het parseren: de ids moeten overeenkomen, het aantal moet overeenkomen en elk item moet de placeholder check uit het vorige onderdeel doorstaan. Vijftig strings per verzoek is een verstandige startbatch voor korte UI tekst; verhoog deze alleen terwijl de validatie pass rate hoog blijft.
Batchvertaling onder de rate limit
Elke sleutel is beperkt tot 300 verzoeken per minuut. Een localization job met 50.000 strings van 20 strings per request is 2.500 requests, wat binnen tien minuten past als je gelijkmatig pace. Het script hieronder combineert een semaphore voor in-flight calls met een lock-based pacer, en back-off bij 429 en 503. In tegenstelling tot een validation failure zijn deze twee fouten transient: 429 betekent slow down, en 503 met upstream_busy betekent retry na een paar seconden. Een 402 betekent dat het balance exhausted is, wat geen retry kan oplossen.
import asyncio
import os
import time
from openai import AsyncOpenAI, APIStatusError
client = AsyncOpenAI(
base_url="https://api.chinesellmapi.com/v1",
api_key=os.environ["API_KEY"],
max_retries=0,
timeout=90,
)
RATE = 240 / 60 # requests per second, below the 300/min cap
gate = asyncio.Lock()
next_slot = 0.0
slots = asyncio.Semaphore(6)
async def pace():
global next_slot
async with gate:
now = time.monotonic()
if next_slot > now:
await asyncio.sleep(next_slot - now)
next_slot = max(now, next_slot) + 1 / RATE
async def call(batch, attempt=0):
async with slots:
await pace()
try:
r = await client.chat.completions.create(
model="uncensored",
messages=[
{"role": "system", "content": "Translate to Simplified Chinese. Keep placeholders unchanged. One line per input line."},
{"role": "user", "content": "\n".join(batch)},
],
max_tokens=1500,
temperature=0.2,
)
return r.choices[0].message.content.splitlines()
except APIStatusError as e:
if e.status_code in (429, 503) and attempt < 4:
await asyncio.sleep(2 ** attempt + 1) # 1s, 3s, 5s, 9s
return await call(batch, attempt + 1)
raise
async def run(all_strings, size=20):
chunks = [all_strings[i:i + size] for i in range(0, len(all_strings), size)]
results = await asyncio.gather(*(call(c) for c in chunks))
return [line for part in results for line in part]
if __name__ == "__main__":
strings = [f"Item {n} was updated by {{user}}" for n in range(60)]
out = asyncio.run(run(strings))
print(len(out), out[0])
Deze versie splitst op lines voor beknoptheid, wat prima is voor single-line strings; voor anything multi-line gebruik je de JSON variant uit het vorige onderdeel zodat line breaks binnen een string niet verward worden met string boundaries.
De omgekeerde richting: Chinese brontekst
Chinees vertalen naar Engels heeft zijn eigen failure modes. Chinees laat onderwerpen en meervouden vrij weg, dus het model moet raden; geef het context. Een string zoals "已发送" kan "Sent", "Has been sent" of "You sent it" zijn, afhankelijk van of het een button labelt, een status badge of een toast. De oplossing is een context veld naast elke string, aangeleverd door je developers en doorgegeven in de JSON batch: waar de string verschijnt, de maximale lengte, en of het een label of een zin is.
Meet lengte limieten expliciet. Engelse UI tekst is vaak langer dan het Chinese origineel, en het omgekeerde geldt voor Chinese targets, die neigen tot korter in karakters maar breder op het scherm omdat elke glyph full width is. Stel een character budget in de prompt in wanneer een button of table header een harde limiet heeft, en controleer dit in code na ontvangst van de response.
Voor Chinese source documenten met namen van personen en plaatsen, specificeer vooraf een romanization convention, zoals Hanyu Pinyin zonder tone marks, en voeg terugkerende namen toe aan het glossarium. Zonder dat kan dezelfde persoon onder twee spellings binnen één document voorkomen, wat precies de inconsistentie is die een reviewer als eerste opmerkt.
Sampling en review voordat je merge
Geautomatiseerde checks vangen structurele fouten op, geen vertaalfouten. Voeg een lichte menselijke stap toe: sample een vast percentage van elke batch, bijvoorbeeld elke twintigste string, plus elke string die een retry nodig had, en stuur die naar een bilingual reviewer. Houd de edit rate van de reviewer per batch bij. Als deze stijgt, verscherp dan de prompt, verklein de batch size, of voeg glossarium entries toe voor de termen die worden gecorrigeerd.
Houd de prompt, glossarium versie en batch settings naast elk output bestand. Als een term verandert in het glossarium, kun je dan alleen de strings die deze bevatten opnieuw vertalen, in plaats van de hele corpus opnieuw te draaien. Een eenvoudige content hash van bront string plus glossarium versie werkt als cache key, en betekent dat ongewijzigde strings de volgende keer niets kosten.
Houd tot slot een regression set van lastige strings bij: die met nested placeholders, plural forms, embedded HTML en lange compound nouns. Draai deze telkens als je de prompt wijzigt en vergelijk outputs side by side voordat je de wijziging uitrolt.
Kosten schatten voor een localization run
Aannames, helder gesteld: 30.000 source strings, 12 Engelse woorden elk, vertaald in batches van 20. Neem ongeveer 20 input tokens per string plus 300 tokens van de vaste system prompt per request, en ongeveer 35 output tokens per string. Dat zijn 1.500 requests, ongeveer 1,05 miljoen input tokens en ongeveer 1,05 miljoen output tokens. Bij $0,25 per miljoen input tokens en $1,00 per miljoen output tokens kost de run ongeveer $0,26 plus $1,05, in totaal ongeveer $1,31. Beschouw dit als een order-of-magnitude estimate en vervang de aannames door cijfers uit je eigen usage logs.
Het trial credit is $0,50 voor zeven dagen, wat genoeg is om een pipeline te valideren op een sample van een paar duizend strings. Ga verder met de app quickstart voor script control en UTF-8 handling, of bekijk de pricing page voor huidige tarieven.
Vragen en antwoorden
Kan de API in beide richtingen vertalen?
Ja. Chinees naar Engels en Engels naar Chinees werken via dezelfde chat-endpoint; je kiest de richting in de system prompt.
Hoe stop ik het model ervan placeholders zoals {name} te veranderen?
Formuleer de regel in de prompt, controleer dit vervolgens in code door de placeholder-lijsten tussen bron en doel te vergelijken, en probeer opnieuw of markeer afwijkingen.
Is er een JSON-modus-schakelaar?
Gestructureerde uitvoer verkrijg je via instructies. Specificeer de exacte structuur in de prompt, verwijder losse code fences en parseer en valideer vervolgens het resultaat.
Hoe snel kan ik een grote batch runnen?
Elke sleutel staat 300 verzoeken per minuut toe. Verdeel de verzoeken gelijkmatig en back-off bij 429- of 503-antwoorden.
Je sleutel is nog maar één formulier verwijderd
Maak een account aan, kopieer de sleutel, pas de basis-URL aan. Dat is de hele setup.