FR ▾

Chinese LLM APITraduction

Obtenir une clé API

Mis à jour le

Traduction et localisation chinois↔anglais via une API de chat

La traduction de chaînes d'interface utilisateur et de documents avec un modèle de chat est facile à démontrer mais difficile à déployer. Les échecs sont banaux : un espace réservé renommé, un terme de glossaire traduit de trois façons différentes, une réponse JSON noyée dans du texte. Ce guide traite la traduction comme un pipeline avec des vérifications à chaque étape, en utilisant l'endpoint de complétion de chat compatible OpenAI.

Pensez par étapes, pas en un seul prompt

Un job de traduction en production comporte quatre étapes : préparer les chaînes, appeler le modèle, valider le résultat et le fusionner. La plupart des problèmes de qualité proviennent du saut de l'étape de validation. Un ingénieur en localisation ne livrerait jamais un fichier de traduction sans exécuter le linter d'espaces réservés, et la même discipline s'applique lorsque le traducteur est un modèle.

L'endpoint est https://api.chinesellmapi.com/v1/chat/completions, l'ID du modèle est uncensored, authentification Bearer. Il gère les deux sens, de l'anglais au chinois et du chinois à l'anglais, et les exemples ci-dessous utilisent des chaînes source en anglais vers le chinois simplifié. Inversez le sens dans le prompt système pour l'inverse.

Gardez à l'esprit la fenêtre partagée de 100 000 tokens, le max_tokens par défaut de 2 048 (augmentez-le pour les longs documents, jusqu'à 32 000), et la limite de 8 Mo du corps de la requête. Aucun de ces éléments n'a d'impact sur les chaînes d'interface utilisateur, mais les trois sont importants pour les documents entiers.

Injection de glossaire

Les noms de marque, les noms de produits et les termes juridiquement sensibles nécessitent un rendu fixe. Le mécanisme le moins coûteux est un bloc de glossaire dans le prompt système, listant le terme source et la cible requise. Énoncez explicitement la règle : utilisez le glossaire mot pour mot dans toute déclinaison du terme source. Gardez le glossaire court, car chaque ligne est facturée en entrée à chaque appel ; quelques dizaines d'entrées sont normales, des milliers ne le sont pas.

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."))

Si votre glossaire est volumineux, filtrez-le par requête : incluez uniquement les entrées dont le terme source apparaît dans le lot. Un test simple de sous-chaîne sur du texte en minuscules suffit pour une première version et garde le prompt petit. Définissez la température autour de 0,2 ; la variation créative est un bug dans le texte de l'interface utilisateur.

Décidez également en amont du guide de style pour la langue cible : adresse formelle ou informelle, placement des espaces entre les caractères chinois et les mots ou nombres latins intégrés, et jeu de ponctuation à utiliser. Intégrez ces décisions dans le même prompt système pour que chaque lot les hérite.

Préservation des espaces réservés et du balisage

Les espaces réservés cassent de manière prévisible : le modèle traduit le nom de la variable, supprime un %s final ou ajoute des espaces entre les accolades. Les règles du prompt réduisent ces échecs mais ne les éliminent pas, donc vérifiez dans le code. Extrayez les espaces réservés et les balises de la source et de la cible avec une expression régulière, puis comparez-les comme des listes triées. L'ordre peut légitimement changer entre les langues, donc comparez-les comme des multisenes plutôt que des séquences.

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

Lorsque la vérification échoue, réessayez une fois en citant la paire défaillante dans le prompt, par exemple un message utilisateur indiquant que la sortie précédente a modifié l'espace réservé et doit être corrigée. Si cela échoue toujours, signalez la chaîne pour examen humain plutôt que de boucler. Le texte riche mérite un soin particulier : privilégiez la traduction des nœuds de texte et reconstruisez vous-même le balisage, car cela rend les dommages aux balises impossibles par construction.

Sortie structurée par instructions

Regrouper des chaînes dans une seule requête nécessite une réponse lisible par machine. L'API prend les champs standard de complétion de chat ; la sortie structurée ici vient de instructions claires plutôt que d'un interrupteur imposant un schéma, donc écrivez le contrat dans le prompt, incluez des identifiants pour réaligner les résultats et analysez de manière défensive. Les modèles enveloppent parfois le JSON dans des balises de code ou ajoutent une phrase amicale, donc supprimez les balises avant l'analyse et traitez une erreur d'analyse comme un événement réessayable.

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>"]))

Validez toujours après l'analyse : les identifiants doivent correspondre, le nombre doit correspondre et chaque élément doit passer la vérification des espaces réservés de la section précédente. Cinquante chaînes par requête sont un lot de départ raisonnable pour le texte court de l'interface ; augmentez-le uniquement tant que le taux de réussite de la validation reste élevé.

Traduction par lots sous la limite de débit

Chaque clé est limitée à 300 requêtes par minute. Un projet de localisation avec 50 000 chaînes à 20 chaînes par requête représente 2 500 requêtes, ce qui tient en moins de dix minutes si vous espacés régulièrement. Le script ci-dessous combine un sémaphore pour les appels en cours avec un régulateur basé sur un verrou, et attend en cas de 429 et 503. Contrairement à une échec de validation, ces deux erreurs sont transitoires : 429 signifie ralentir, et 503 avec upstream_busy signifie réessayer dans quelques secondes. Un 402 signifie que le solde est épuisé, ce qu'aucune réessaye ne peut réparer.

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])

Cette version divise sur les lignes pour plus de concision, ce qui convient pour les chaînes sur une seule ligne ; pour tout ce qui est multi-lignes, utilisez la variante JSON de la section précédente afin que les sauts de ligne dans une chaîne ne soient pas confondus avec des limites de chaîne.

Le sens inverse : texte source chinois

La traduction du chinois vers l'anglais a ses propres limites. Le chinois omet sujets et pluriels ; le modèle doit les deviner. Fournissez un champ contexte pour chaque chaîne (label, badge, toast) via le JSON batch.

Mesurez explicitement les limites de longueur. Le texte d'interface en anglais est souvent plus long que l'original chinois, et l'inverse est vrai pour les cibles chinoises, qui ont tendance à être plus courtes en caractères mais plus larges à l'écran car chaque glyphe est pleine largeur. Énoncez un budget de caractères dans le prompt lorsqu'un bouton ou un en-tête de tableau a une limite stricte, et vérifiez-le dans le code après la réception de la réponse.

Pour les documents sources chinois avec des noms de personnes et de lieux, spécifiez une convention de romanisation en amont, comme le Hanyu Pinyin sans marques de ton, et ajoutez les noms récurrents au glossaire. Sans cela, la même personne peut apparaître sous deux orthographes dans un seul document, ce qui est exactement l'incohérence qu'un réviseur remarquera en premier.

Échantillonnage et révision avant la fusion

Les vérifications automatiques détectent les erreurs structurelles, pas les erreurs de traduction. Ajoutez une étape humaine légère : échantillonnez un pourcentage fixe de chaque lot, par exemple une chaîne sur vingt, plus chaque chaîne ayant nécessité une réessay, et envoyez-les à un réviseur bilingue. Suivez le taux de modification du réviseur par lot. S'il augmente, serrez le prompt, réduisez la taille du lot ou ajoutez des entrées de glossaire pour les termes corrigés.

Gardez le prompt, la version du glossaire et les paramètres de lot à côté de chaque fichier de sortie. Lorsqu'un terme change dans le glossaire, vous pouvez alors ne traduire que les chaînes qui le contiennent, au lieu de relancer tout le corpus. Un hachage de contenu simple de la chaîne source plus la version du glossaire sert de clé de cache, et cela signifie que les chaînes inchangées ne coûtent rien lors de la prochaine exécution.

Enfin, gardez un ensemble de régression de chaînes difficiles : celles avec des espaces réservés imbriqués, des formes plurielles, du HTML intégré et des noms composés longs. Exécutez-le chaque fois que vous modifiez le prompt, et comparez les sorties côte à côte avant de déployer le changement.

Estimation du coût pour un job de localisation

Hypothèses, énoncées clairement : 30 000 chaînes source, 12 mots anglais chacune, traduites par lots de 20. Prenez environ 20 tokens d'entrée par chaîne plus 300 tokens de prompt système fixe par requête, et environ 35 tokens de sortie par chaîne. Cela donne 1 500 requêtes, environ 1,05 million de tokens d'entrée et environ 1,05 million de tokens de sortie. À $0,25 par million de tokens d'entrée et $1,00 par million de tokens de sortie, la tâche coûte environ $0,26 plus $1,05, soit environ $1,31. Traitez cela comme une estimation d'ordre de grandeur et remplacez les hypothèses par des chiffres de vos propres journaux usage.

Le crédit d'essai est de $0,50 pendant sept jours, ce qui suffit pour valider un pipeline sur un échantillon de quelques milliers de chaînes. Continuez avec le app quickstart pour le contrôle du script et la gestion de l'UTF-8, ou consultez la pricing page pour les tarifs actuels.

Questions et réponses

L'API peut-elle traduire dans les deux sens ?

Oui. Le chinois vers l'anglais et l'anglais vers le chinois fonctionnent tous deux via le même endpoint de chat ; vous choisissez la direction dans le prompt système.

Comment empêcher le modèle de modifier les espaces réservés comme {name} ?

Énoncez la règle dans le prompt, puis vérifiez dans le code en comparant les listes d'espaces réservés entre la source et la cible, en réessayant ou en signalant les écarts.

Y a-t-il un interrupteur pour le mode JSON ?

La sortie structurée s'obtient par des instructions. Spécifiez la forme exacte dans le prompt, supprimez les balises de code parasites, puis analysez et validez le résultat.

À quelle vitesse puis-je exécuter un gros lot ?

Chaque clé permet 300 requêtes par minute. Envoyez les requêtes régulièrement et attendez en cas de réponse 429 ou 503.

Votre clé est à un formulaire de vous

Créez un compte, copiez la clé, modifiez l’URL de base. C’est toute la configuration.

Obtenir une clé API