Actualizado
Traducción y localización chino↔inglés a través de una API de chat
Traducir cadenas de interfaz y documentos con un modelo de chat es fácil de demostrar pero difícil de implementar. Los fallos son cotidianos: un marcador renombrado, un término del glosario traducido de tres formas distintas, una respuesta JSON envuelta en prosa. Esta guía trata la traducción como una tubería con comprobaciones en cada etapa, usando el endpoint de completados de chat compatible con OpenAI.
Piensa por etapas, no en un solo prompt
Un trabajo de traducción en producción tiene cuatro etapas: preparar las cadenas, llamar al modelo, validar el resultado y fusionarlo de nuevo. La mayoría de los problemas de calidad provienen de omitir la etapa de validación. Un ingeniero de localización nunca enviaría un archivo de traducción sin ejecutar el linter de marcadores, y la misma disciplina aplica cuando el traductor es un modelo.
El endpoint es https://api.chinesellmapi.com/v1/chat/completions, id del modelo uncensored, autenticación bearer. Maneja ambas direcciones, inglés a chino y chino a inglés, y los ejemplos a continuación usan cadenas de origen en inglés que van a chino simplificado. Cambia la dirección en el prompt del sistema para la inversa.
Ten en cuenta la ventana compartida de 100.000 tokens, el valor predeterminado de max_tokens de 2.048 (aumentalo para documentos largos, hasta 32.000) y el límite de 8 MB en el cuerpo de la petición. Ninguno de estos afecta a las cadenas de la interfaz, pero los tres son importantes para documentos completos.
Inyección de glosario
Los nombres de marca, sustantivos de producto y términos legalmente sensibles necesitan una representación fija. El mecanismo más barato es un bloque de glosario en el prompt del sistema, que enumera el término de origen y el destino requerido. Establece la regla explícitamente: usa el glosario literalmente en cualquier inflexión del término de origen. Mantén el glosario corto, porque cada línea se factura como entrada en cada llamada; unas pocas docenas de entradas son normales, miles no lo son.
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 tu glosario es grande, filtéralo por petición: incluye solo las entradas cuyo término de origen aparezca en el lote. Una prueba de subcadena simple en texto minúsculo es suficiente para una primera versión y mantiene el prompt pequeño. Establece la temperatura alrededor de 0,2; la variación creativa es un error en el texto de la interfaz.
También decide el manual de estilo para el idioma de destino de antemano: tratamiento formal o informal, si poner espacios entre caracteres chinos y palabras o números latinos incrustados, y qué conjunto de puntuación usar. Pon estas decisiones en el mismo prompt del sistema para que cada lote las herede.
Mantener marcadores y marcado intactos
Los marcadores de posición se rompen de formas predecibles: el modelo traduce el nombre de la variable, elimina un %s final o añade espacios dentro de las llaves. Las reglas del prompt reducen estos fallos, pero no los eliminan, así que verifícalo en el código. Extrae los marcadores de posición y las etiquetas del código fuente y del destino con una expresión regular, y compáralos como listas ordenadas. El orden puede cambiar legítimamente entre idiomas, así que compáralos como multiconjuntos en lugar de secuencias.
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
Cuando la comprobación falle, reintenta una vez citando el par fallido en el prompt, por ejemplo un mensaje de usuario indicando que la salida anterior cambió el marcador y debe corregirse. Si sigue fallando, marca la cadena para revisión humana en lugar de bucle. El texto enriquecido merece cuidado extra: prefiere traducir los nodos de texto y reconstruir el marcado tú mismo, ya que eso hace imposible el daño de etiquetas por construcción.
Salida estructurada mediante instrucciones
Lotea cadenas en una petición que necesita una respuesta legible por máquina. La API toma los campos estándar de completado de chat; la salida estructurada aquí proviene de instrucciones claras en lugar de un interruptor que fuerza un esquema, así que escribe el contrato en el prompt, incluye ids para que puedas realinear los resultados y analiza de forma defensiva. Los modelos a veces envuelven JSON en etiquetas de código o añaden una frase amigable, así que elimina las etiquetas antes de analizar y trata un error de análisis como un evento reintenable.
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>"]))
Valida siempre después de analizar: los ids deben coincidir, el conteo debe coincidir y cada elemento debe pasar la comprobación de marcadores de la sección anterior. Cincuenta cadenas por petición es un tamaño de lote inicial sensato para texto de interfaz corto; aumentalo solo mientras la tasa de éxito de la validación se mantenga alta.
Traducción por lotes bajo el límite de peticiones
Cada clave está limitada a 300 peticiones por minuto. Un trabajo de localización con 50.000 cadenas a 20 cadenas por petición son 2.500 peticiones, lo que cabe en menos de diez minutos si distribuyes uniformemente. El script a continuación combina un semáforo para llamadas en curso con un regulador basado en bloqueo, y reduce la velocidad ante 429 y 503. A diferencia de un fallo de validación, esos dos errores son transitorios: 429 significa reducir la velocidad, y 503 con upstream_busy significa reintentar en unos segundos. Un 402 significa que el saldo se agotó, lo que ningún reintento puede arreglar.
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])
Esta versión divide por líneas para brevedad, lo cual está bien para cadenas de una sola línea; para cualquier cosa multilínea, usa la variante JSON de la sección anterior para que los saltos de línea dentro de una cadena no se confundan con límites de cadena.
La dirección inversa: texto de origen chino
Traducir chino a inglés tiene sus propios modos de fallo. El chino omite sujetos y plurales libremente, así que el modelo debe adivinarlos; dale contexto. Una cadena como "已发送" podría ser "Sent", "Has been sent" o "You sent it", dependiendo de si etiqueta un botón, un distintivo de estado o un toast. La solución es un campo de contexto junto a cada cadena, suministrado por tus desarrolladores y pasado en el lote JSON: dónde aparece la cadena, su longitud máxima y si es una etiqueta o una oración.
Mide los límites de longitud explícitamente. El texto de interfaz en inglés suele ser más largo que el original chino, y lo contrario es cierto para los objetivos chinos, que tienden a ser más cortos en caracteres pero más anchos en pantalla porque cada glifo es de ancho completo. Establece un presupuesto de caracteres en el prompt cuando un botón o encabezado de tabla tenga un límite estricto, y verifícalo en código después de recibir la respuesta.
Para documentos de origen chinos con nombres de personas y lugares, especifica una convención de romanización de antemano, como Hanyu Pinyin sin marcas de tono, y añade nombres recurrentes al glosario. Sin eso, la misma persona puede aparecer con dos ortografías dentro de un solo documento, lo cual es exactamente la inconsistencia que un revisor notará primero.
Muestreo y revisión antes de fusionar
Las comprobaciones automáticas detectan errores estructurales, no malas traducciones. Añade un paso humano ligero: muestrea un porcentaje fijo de cada lote, por ejemplo cada vigésima cadena, más cada cadena que necesitó un reintento, y envíalas a un revisor bilingüe. Rastrea la tasa de edición del revisor por lote. Si sube, ajusta el prompt, reduce el tamaño del lote o añade entradas de glosario para los términos que se están corrigiendo.
Mantén el prompt, la versión del glosario y la configuración del lote junto a cada archivo de salida. Cuando un término cambie en el glosario, puedes entonces retraducir solo las cadenas que lo contienen, en lugar de volver a ejecutar todo el corpus. Un hash de contenido simple de la cadena de origen más la versión del glosario funciona como clave de caché, y significa que las cadenas sin cambios nunca cuestan nada en la siguiente ejecución.
Finalmente, mantén un conjunto de regresión de cadenas difíciles: aquellas con marcadores anidados, formas plurales, HTML incrustado y sustantivos compuestos largos. Ejecútalo cada vez que cambies el prompt y compara las salidas lado a lado antes de implementar el cambio.
Estimación de costos para una ejecución de localización
Suposiciones, stated plainly: 30.000 cadenas de origen, 12 palabras en inglés cada una, traducidas en lotes de 20. Toma aproximadamente 20 tokens de entrada por cadena más 300 tokens de prompt del sistema fijos por petición, y aproximadamente 35 tokens de salida por cadena. Eso da 1.500 peticiones, aproximadamente 1,05 millones de tokens de entrada y aproximadamente 1,05 millones de tokens de salida. A $0,25 por millón de tokens de entrada y $1,00 por millón de tokens de salida, la ejecución cuesta aproximadamente $0,26 más $1,05, unos $1,31. Trata esto como una estimación de orden de magnitud y reemplaza las suposiciones con cifras de tus propios registros de usage.
El crédito de prueba es de $0,50 durante siete días, lo cual es suficiente para validar una tubería en una muestra de unas pocas miles de cadenas. Continúa con el quickstart de la app para control de scripts y manejo de UTF-8, o consulta la página de precios para las tarifas actuales.
Preguntas y respuestas
¿Puede la API traducir en ambas direcciones?
Sí. El chino al inglés y el inglés al chino funcionan a través del mismo endpoint de chat; eliges la dirección en el prompt del sistema.
¿Cómo evito que el modelo cambie marcadores como {name}?
Establece la regla en el prompt y luego verifica en código comparando las listas de marcadores entre origen y destino, reintentando o marcando las discrepancias.
¿Hay un interruptor de modo JSON?
La salida estructurada se obtiene mediante instrucciones. Especifica la forma exacta en el prompt, elimina las etiquetas de código sobrantes y luego analiza y valida el resultado.
¿Qué tan rápido puedo ejecutar un lote grande?
Cada clave permite 300 peticiones por minuto. Distribuye las peticiones de forma uniforme y espera o reintenta con retraso ante respuestas 429 o 503.
Tu clave está a un formulario de distancia
Crea una cuenta, copia la clave y cambia la URL base. Eso es todo el proceso de configuración.