Aktualisiert
Chinesisch↔Englisch-Übersetzung und Lokalisierung über eine Chat-API
Die Übersetzung von UI-Strings und Dokumenten mit einem Chat-Modell ist einfach zu demonstrieren, aber schwer in der Produktion einzusetzen. Die Fehler sind alltäglich: ein umbenannter Platzhalter, ein Glossarbegriff auf drei verschiedene Arten übersetzt, eine JSON-Antwort eingebettet in Fließtext. Diese Anleitung behandelt Übersetzung als Pipeline mit Prüfungen in jedem Schritt, unter Verwendung des OpenAI-kompatiblen Chat-Completions-Endpunkts.
Denke in Schritten, nicht in einem Prompt
Ein Produktions-Übersetzungsauftrag hat vier Schritte: Strings vorbereiten, Modell aufrufen, Ergebnis validieren und zurückführen. Die meisten Qualitätsprobleme entstehen durch das Überspringen des Validierungsschritts. Ein Lokalisierungstechniker würde nie eine Übersetzungsdatei ausliefern, ohne den Platzhalter-Linter zu laufen zu lassen, und dieselbe Disziplin gilt, wenn der Übersetzer ein Modell ist.
Der Endpunkt ist https://api.chinesellmapi.com/v1/chat/completions, die Modell-ID ist uncensored, die Authentifizierung erfolgt über Bearer. Er verarbeitet beide Richtungen, Englisch nach Chinesisch und Chinesisch nach Englisch, und die folgenden Beispiele verwenden englische Quellstrings, die ins Vereinfachte Chinesisch übersetzt werden. Ändere die Richtung im System-Prompt für die umgekehrte Übersetzung.
Behalte das gemeinsame Kontextfenster von 100.000 Token im Blick, den Standardwert für max_tokens von 2.048 (erhöhe ihn für lange Dokumente, bis zu 32.000) und die Obergrenze des Request-Body von 8 MB. Keine dieser Einschränkungen betrifft UI-Strings, aber alle drei sind für ganze Dokumente relevant.
Glossareinschub
Marken, Produktnamen und rechtlich sensible Begriffe benötigen eine feste Darstellung. Die günstigste Methode ist ein Glossar-Block im System-Prompt, der den Quelld Begriff und die erforderliche Zielübersetzung auflistet. Regel explizit formulieren: das Glossar bei jeder Beugung des Quelld Begriffs wörtlich verwenden. Das Glossar kurz halten, da jede Zeile bei jeder Anfrage als Input in Rechnung gestellt wird; ein paar Dutzend Einträge sind normal, Tausende nicht.
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."))
Wenn dein Glossar groß ist, filtere es pro Anfrage: Nimm nur die Einträge auf, deren Quelld Begriff im Batch vorkommt. Ein einfacher Substring-Test auf Kleinbuchstaben reicht für eine erste Version und hält den Prompt klein. Stelle die Temperatur auf ca. 0,2; kreative Variation ist ein Fehler in der UI-Kopie.
Entscheide auch im Voraus über den Stil-Leitfaden für die Zielsprache: formelle oder lockere Anrede, ob Leerzeichen zwischen chinesischen Zeichen und eingebetteten lateinischen Wörtern oder Zahlen gesetzt werden sollen, und welche Zeichensetzung zu verwenden ist. Füge diese Entscheidungen in denselben System-Prompt, damit jeder Batch sie erbt.
Platzhalter und Markup intakt halten
Platzhalter brechen auf vorhersehbare Weise: Das Modell übersetzt den Variablennamen, lässt ein trailing %s weg oder fügt Leerzeichen innerhalb der Klammern ein. Prompt-Regeln reduzieren diese Fehler, können sie aber nicht eliminieren, also prüfe es im Code. Extrahiere Platzhalter und Tags aus Quelle und Ziel mit einem regulären Ausdruck und vergleiche sie als sortierte Listen. Die Reihenfolge kann sich zwischen Sprachen bereits ändern, also vergleiche sie als Multimengen statt als Sequenzen.
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
Wenn die Prüfung fehlschlägt, wiederhole den Vorgang einmal mit dem fehlerhaften Paar, das im Prompt erneut zitiert wird, z. B. eine Benutzernachricht, die besagt, dass die vorherige Ausgabe den Platzhalter verändert hat und korrigiert werden muss. Wenn es immer noch fehlschlägt, markiere den String zur manuellen Überprüfung, anstatt in einer Endlosschleife zu verharren. Rich Text verdient besondere Sorgfalt: Übersetze vorzugsweise die Textknoten und baue das Markup selbst wieder auf, da dies Tag-Schäden durch Konstruktion unmöglich macht.
Strukturierte Ausgabe durch Anweisungen
Das Batching von Strings in einer Anfrage erfordert eine maschinenlesbare Antwort. Die API nimmt die Standardfelder für Chat-Abschlüsse entgegen; strukturierte Ausgabe entsteht hier durch klare Anweisungen statt durch einen Schema-durchsetzenden Schalter, also schreibe den Vertrag in den Prompt, füge IDs hinzu, um Ergebnisse wieder abgleichen zu können, und parse defensiv. Modelle verpacken JSON manchmal in Code-Fences oder fügen einen freundlichen Satz hinzu, also entferne Fences vor dem Parsing und behandle einen Parse-Fehler als wiederholbares Ereignis.
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>"]))
Validiere nach dem Parsing immer: Die IDs müssen übereinstimmen, die Anzahl muss übereinstimmen und jeder Eintrag muss die Platzhalterprüfung aus dem vorherigen Abschnitt bestehen. Fünfzig Strings pro Anfrage sind eine sinnvolle Start-Batchgröße für kurze UI-Texte; erhöhe sie nur, solange die Validierungsrate hoch bleibt.
Batch-Übersetzung unter dem Ratenlimit
Jeder Schlüssel ist auf 300 Anfragen pro Minute begrenzt. Ein Lokalisierungsjob mit 50.000 Strings bei 20 Strings pro Anfrage ergibt 2.500 Anfragen, was unter zehn Minuten passt, wenn du die Anfragen gleichmäßig verteilst. Das folgende Skript kombiniert einen Semaphore für laufende Aufrufe mit einem Lock-basierten Pacer und wartet bei 429 und 503. Im Gegensatz zu einem Validierungsfehler sind diese beiden Fehler transient: 429 bedeutet langsamer werden, und 503 mit upstream_busy bedeutet, in einigen Sekunden erneut zu versuchen. Eine 402 bedeutet, dass das Guthaben erschöpft ist, was kein erneuter Versuch beheben kann.
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])
Diese Version teilt nach Zeilen auf, um die Kürze zu wahren, was für einzeilige Strings in Ordnung ist; für alles, was mehrzeilig ist, verwende die JSON-Variante aus dem vorherigen Abschnitt, damit Zeilenumbrüche innerhalb eines Strings nicht mit String-Grenzen verwechselt werden.
Die umgekehrte Richtung: Chinesischer Quelltext
Die Übersetzung von Chinesisch nach Englisch hat eigene Fehlermodi. Chinesisch lässt Subjekte und Plurale frei weg, also muss das Modell sie erraten; gib ihm Kontext. Ein String wie "已发送" könnte "Gesendet", "Wurde gesendet" oder "Du hast es gesendet" sein, je nachdem, ob er eine Schaltfläche, einen Status-Badge oder einen Toast beschriftet. Die Lösung ist ein Kontextfeld neben jedem String, bereitgestellt von deinen Entwicklern und im JSON-Batch übergeben: wo der String erscheint, seine maximale Länge und ob es eine Beschriftung oder ein Satz ist.
Messe Längenlimits explizit. Englischer UI-Text ist oft länger als das chinesische Original, und umgekehrt gilt das für chinesische Zieltexte, die zwar weniger Zeichen, aber auf dem Bildschirm breiter sind, da jedes Glyph vollbreit ist. Gib ein Zeichenbudget im Prompt an, wenn ein Button oder Tabellenkopf ein hartes Limit hat, und prüfe es im Code nach Erhalt der Antwort.
Für chinesische Quelldokumente mit Namen von Personen und Orten gib im Voraus eine Romanisierungskonvention an, wie Hanyu Pinyin ohne Tonmarken, und füge wiederkehrende Namen zum Glossar hinzu. Ohne das kann dieselbe Person unter zwei Schreibweisen innerhalb eines einzelnen Dokuments erscheinen, was genau die Inkonsistenz ist, die ein Prüfer als Erstes bemerkt.
Stichprobe und Prüfung vor dem Zusammenführen
Automatisierte Prüfungen fangen strukturelle Fehler ab, keine falschen Übersetzungen. Füge einen leichten menschlichen Schritt hinzu: Stichprobe eines festen Prozentsatzes jedes Batches, z. B. jeder zwanzigste String, plus jeder String, der eine Wiederholung benötigte, und sende diese an einen zweisprachigen Prüfer. Verfolge die Bearbeitungsrate des Prüfers pro Batch. Wenn sie steigt, verschärfe den Prompt, verringere die Batch-Größe oder füge Glossareinträge für die korrigierten Begriffe hinzu.
Bewahre den Prompt, die Glossar-Version und die Batch-Einstellungen zusammen mit jeder Ausgabedatei auf. Wenn sich ein Begriff im Glossar ändert, kannst du so nur die Strings neu übersetzen, die ihn enthalten, anstatt das gesamte Korpus neu zu durchlaufen. Ein einfacher Inhalts-Hash aus Quell-String plus Glossar-Version dient als Cache-Schlüssel, und das bedeutet, dass unveränderte Strings beim nächsten Durchlauf nichts kosten.
Behalte schließlich eine Regressionssammlung mit schwierigen Strings: solche mit verschachtelten Platzhaltern, Pluralformen, eingebettetem HTML und langen zusammengesetzten Substantiven. Führe sie aus, wenn du den Prompt änderst, und vergleiche die Ausgaben nebeneinander, bevor du die Änderung ausrollst.
Kostenschätzung für einen Lokalisierungslauf
Annahmen, klar formuliert: 30.000 Quell-Strings, 12 englische Wörter pro String, übersetzt in Batches von 20. Nimm etwa 20 Input-Token pro String plus 300 Token für den festen System-Prompt pro Anfrage und etwa 35 Output-Token pro String. Das ergibt 1.500 Anfragen, etwa 1,05 Millionen Input-Token und etwa 1,05 Millionen Output-Token. Bei $0,25 pro Million Input-Token und $1,00 pro Million Output-Token kostet der Lauf etwa $0,26 plus $1,05, also etwa $1,31. Betrachte dies als eine grobe Schätzung und ersetze die Annahmen durch Zahlen aus deinen eigenen usage-Logs.
Das Testguthaben beträgt $0,50 für sieben Tage, was ausreicht, um eine Pipeline mit einer Stichprobe aus einigen tausend Strings zu validieren. Fahre mit dem App-Quickstart für die Skriptsteuerung und die UTF-8-Verarbeitung fort oder sieh dir die Preisseite für aktuelle Tarife an.
Fragen und Antworten
Kann die API in beide Richtungen übersetzen?
Ja. Chinesisch nach Englisch und Englisch nach Chinesisch funktionieren über denselben Chat-Endpunkt; du wählst die Richtung im System-Prompt.
Wie verhindere ich, dass das Modell Platzhalter wie {name} ändert?
Formuliere die Regel im Prompt und überprüfe sie im Code, indem du Platzhalterlisten zwischen Quelle und Ziel vergleichst, bei Diskrepanzen erneut versuchst oder sie markierst.
Gibt es einen JSON-Modus-Schalter?
Strukturierte Ausgabe erhältst du durch Anweisungen. Gib die exakte Form im Prompt an, entferne übrige Code-Fences, und parse dann das Ergebnis.
Wie schnell kann ich einen großen Batch ausführen?
Jeder Schlüssel erlaubt 300 Anfragen pro Minute. Verteile die Anfragen gleichmäßig und warte bei 429- oder 503-Antworten.
Dein Schlüssel ist nur ein Formular entfernt
Erstelle ein Konto, kopiere den Schlüssel, ändere die Basis-URL. Das ist die gesamte Einrichtung.