تم التحديث
ترجمة وتوطين النصوص الصينية↔الإنجليزية عبر واجهة برمجة تطبيقات الدردشة
إن ترجمة سلاسل واجهة المستخدم والمستندات باستخدام نموذج دردشة أمر سهل عرضه كنموذج أولي، لكنه صعب في النشر. الفشل شائع: تغيير اسم عنصر نائبي، ترجمة مصطلح القاموس بثلاث طرق مختلفة، أو إخراج JSON مغطى بنص عادي. يعالج هذا الدليل الترجمة كخط أنابيب مع فحوصات في كل مرحلة، باستخدام نقطة إكمال الدردشة المتوافقة مع OpenAI.
فكّر بمراحل، وليس في موجّه واحد
تتكون مهمة الترجلة الإنتاجية من أربع مراحل: تحضير السلاسل، استدعاء النموذج، التحقق من النتيجة، ودمجها مرة أخرى. تأتي معظم مشاكل الجودة من تخطي مرحلة التحقق. لن يسلم مهندس التوطين ملف ترجمة دون تشغيل مدقق العناصر النائبة، وينطبق نفس الانضباط عندما يكون المترجم نموذجاً.
نقطة النهاية هي https://api.chinesellmapi.com/v1/chat/completions، ومعرف النموذج uncensored، ومصادقة Bearer. يتعامل مع الاتجاهين، من الإنجليزية إلى الصينية ومن الصينية إلى الإنجليزية، وتستخدم الأمثلة أدناه سلاسل نصية إنجليزية كمدخلات إلى الصينية المبسطة. غيّر اتجاه النص التوجيهي للنظام للاتجاه المعاكس.
خذ في الاعتبار نافذة السياق المشتركة البالغة 100,000 رمز، وقيمة max_tokens الافتراضية وهي 2,048 (ارفعها للمستندات الطويلة حتى 32,000)، وحد حجم جسم الطلب البالغ 8 MB. لا تؤثر هذه القيود على سلاسل واجهة المستخدم، لكنها مهمة للمستندات الكاملة.
حقن القاموس
تحتاج أسماء العلامات التجارية، وأسماء المنتجات والمصطلحات الحساسة قانونياً إلى ترجمة ثابتة. أرخص آلية هي كتلة قاموس في نظام الموجّه، تذكر المصطلح المصدر والترجمة المطلوبة. حدد القاعدة صراحة: استخدم القاموس حرفياً في أي صرف للمصطلح المصدر. اجعل القاموس قصيراً، لأن كل سطر يُحسب كمدخل في كل استدعاء؛ بضع عشرات من الإدخالات طبيعية، بينما الآلاف ليست كذلك.
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."))
إذا كانت قائمة المصطلحات لديك كبيرة، قم بتصفيتها حسب الطلب: ضمّن فقط الإدخالات التي يظهر مصطلحها المصدر في الدفعة. اختبار النص المنخفض البسيط كافٍ للإصدار الأولي ويحافظ على صغر حجم الموجّه. اضبط درجة الحرارة حول 0.2؛ فالاختلافات الإبداعية تُعدّ خطأً في نص واجهة المستخدم.
قرر أيضاً دليل الأسلوب للغة الهدف مسبقاً: الخطاب الرسمي أو غير الرسمي، وضع المسافات بين الأحرف الصينية والكلمات أو الأرقام اللاتينية المضمنة، ومجموعة علامات الترقيم المستخدمة. ضع هذه القرارات في نظام الموجّه نفسه لتورثها كل دفعة.
الحفاظ على العناصر النائبة والعلامات سليمة
تفشل العناصر النائبة بطرق متوقعة: يترجم النموذج اسم المتغير، ويتجاهل %s في النهاية، أو يضيف مسافات داخل الأقواس المعقوسة. تقلل قواعد الموجّه من هذه الأخطاء لكنها لا تقضي عليها، لذا تحقق في الكود. استخرج العناصر النائبة والعلامات من المصدر والهدف باستخدام تعبير عادي، ثم قارنها كقوائم مرتبة. قد يتغير الترتيب بشكل شرعي بين اللغات، لذا قارنها كمجموعات متعددة بدلاً من التسلسلات.
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
عند فشل الفحص، أعد المحاولة مرة واحدة مع اقتباس الزوج الفاشل في الموجّه، مثلاً رسالة مستخدم تقول إن الإخراج السابق غيّر العنصر النائي ويجب تصحيحه. إذا فشل مرة أخرى، علّم السلسلة للمراجعة البشرية بدلاً من التكرار. يستحق النص الغني عناية إضافية: يفضل ترجمة عقد النص وإعادة بناء العلامات بنفسك، لأن ذلك يجعل تلف العلامات مستحيلاً بالتصميم.
إخراج منظم عبر التعليمات
تتطلب معالجة السلاسل في طلب واحد ردًا قابلاً للقراءة الآلية. تأخذ واجهة برمجة التطبيقات حقول إكمال الدردشة القياسية؛ الإخراج المنظم هنا يأتي من تعليمات واضحة بدلاً من زر يفرض مخططاً، لذا اكتب العقد في الموجّه، وأدرج المعرفات لإعادة محاذاة النتائج، وقم بالتحليل بحذر. أحياناً تغلف النماذج JSON بعلامات كود أو تضيف جملة ودودة، لذا أزل العلامات قبل التحليل واعتبر خطأ التحليل حدثاً قابلاً لإعادة المحاولة.
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>"]))
قم دائمًا بالتحقق بعد التحليل: يجب أن تتطابق المعرفات، ويجب أن يتطابق العدد، ويجب أن يمر كل عنصر عبر فحص العنصر النائي من القسم السابق. خمسون سلسلة لكل طلب هي دفعة بداية معقولة للنصوص القصيرة لواجهة المستخدم؛ زد العدد فقط بينما يظل معدل نجاح التحقق مرتفعًا.
ترجمة الدفعات ضمن حد المعدل
يُقتصر كل مفتاح على 300 طلب في الدقيقة. مهمة التعريب التي تحتوي على 50,000 سلسلة بمعدل 20 سلسلة لكل طلب تتطلب 2,500 طلب، وهو ما يتسع لأقل من عشر دقائق إذا تم توزيعها بالتساوي. يجمع النص البرمجي أدناه بين مفتاح الوصول (semaphore) للمكالمات النشطة وقاسٍ يعتمد على القفل، ويتراجع عند حدوث أخطاء 429 و503. على عكس فشل التحقق، هذان الخطآن مؤقتان: يعني 429 إبطاء السرعة، ويعني 503 مع upstream_busy إعادة المحاولة بعد بضع ثوانٍ. يعني 402 استنفاد الرصيد، وهو ما لا يمكن إصلاحه بإعادة المحاولة.
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])
ينقسم هذا الإصدار على الأسطر للإيجاز، وهو أمر جيد للسلاسل أحادية السطر؛ لأي نص متعدد الأسطر، استخدم متغير JSON من القسم السابق حتى لا تُخطأ الفواصل داخل السلسلة كحدود للسلسلة.
الاتجاه المعاكس: نص المصدر الصيني
ترجمة الصينية إلى الإنجليزية لها أنماط فشل خاصة بها. تتخلى الصينية عن الفاعلين والجمع بحرية، لذا يجب على النموذج تخمينها؛ زوده بالسياق. يمكن أن تكون السلسلة "已发送" كـ "Sent" أو "Has been sent" أو "You sent it"، اعتماداً على ما إذا كانت تسمي زراً، أو شارة حالة، أو إشعاراً. الحل هو حقل سياق بجانب كل سلسلة، يوفره المطورون الخاصون بك ويتم تمريره في دفقة JSON: أين تظهر السلسلة، أقصى طول لها، وما إذا كانت تسمية أو جملة.
قيّم حدود الطول صراحة. غالباً ما يكون نص واجهة المستخدم الإنجليزي أطول من الأصل الصيني، والعكس صحيح بالنسبة للأهداف الصينية، التي تميل لأن تكون أقصر من حيث الأحرف لكنها أوسع على الشاشة لأن كل حرف بعرض كامل. حدد ميزانية الأحرف في الموجّه عندما يكون للزر أو رأس الجدول حد صارم، وتحقق منه في الكود بعد وصول الاستجابة.
بالنسبة للمستندات الصينية التي تحتوي على أسماء الأشخاص والأماكن، حدد نظام تعريب مسبقاً، مثل Hanyu Pinyin بدون علامات النطق، وأضف الأسماء المتكررة إلى القاموس. بدون ذلك، قد يظهر نفس الشخص تحت تهجئتين مختلفتين في مستند واحد، وهو بالضبط عدم الاتساق الذي سيلاحظه المراجع أولاً.
العينة والمراجعة قبل الدمج
تكتشف الفحوصات الآلية الأخطاء الهيكلية، لا أخطاء الترجمة. أضف خطوة مراجعة بشرية خفيفة: عيّن نسبة ثابتة من كل دفعة، على سبيل المثال كل عشرين سلسلة، بالإضافة إلى كل سلسلة تطلبت إعادة محاولة، وأرسلها إلى مراجع ثنائي اللغة. تتبع معدل تعديلات المراجع لكل دفعة. إذا ارتفع، شدد الموجّه، أو قلّص حجم الدفعة، أو أضف إدخالات لقائمة المصطلحات للمصطلحات التي يتم تصحيحها.
احتفظ بالموجّه، وإصدار القاموس وإعدادات الدفعة بجانب كل ملف إخراج. عندما يتغير مصطلح في القاموس، يمكنك إعادة ترجمة السلاسل التي تحتوي عليه فقط، بدلاً من إعادة تشغيل المجموعة الكاملة. يعمل التجزئة البسيطة لمحتوى السلسلة المصدر بالإضافة إلى إصدار القاموس كمفتاح تخزين مؤقت، وهذا يعني أن السلاسل غير المتغيرة لا تكلف شيئاً في التشغيل التالي.
أخيراً، احتفظ بمجموعة اختبارات الانحدار للسلاسل الصعبة: تلك التي تحتوي على عناصر نائية متداخلة، وأشكال الجمع، وHTML مضمن، وأسماء مركبة طويلة. شغّلها كلما غيّرت الموجّه، وقارن المخرجات جنباً إلى جنب قبل نشر التغيير.
تقدير التكلفة لتشغيل التوطين
الافتراضات، مُصاغة بوضوح: 30,000 سلسلة مصدر، 12 كلمة إنجليزية لكل منها، تُترجم في دفعات من 20. يستغرق الأمر حوالي 20 رمزًا (token) مدخلًا لكل سلسلة بالإضافة إلى 300 رمزًا (token) من الموجّه النظامي الثابت لكل طلب، وحوالي 35 رمزًا (token) مخرجًا لكل سلسلة. هذا يعطي 1,500 طلب، وحوالي 1.05 مليون رمز مدخل وحوالي 1.05 مليون رمز مخرج. بسعر $0.25 لكل مليون رمز مدخل و$1.00 لكل مليون رمز مخرج، تكلف التشغيل حوالي $0.26 بالإضافة إلى $1.05، أي حوالي $1.31. تعامل مع هذا كتقدير تقريبي واستبدل الافتراضات بأرقام من سجلات usage الخاصة بك.
رصيد التجربة هو 0.50 دولار لمدة سبعة أيام، وهو كافٍ للتحقق من صحة خط أنابيب على عينة من بضعة آلاف من السلاسل. تابع باستخدام app quickstart للتحكم في السكربت ومعالجة UTF-8، أو راجع pricing page للحصول على الأسعار الحالية.
أسئلة وأجوبة
هل يمكن للواجهة ترجمة في الاتجاهين؟
نعم. تعمل الترجمة من الصينية إلى الإنجليزية ومن الإنجليزية إلى الصينية عبر نقطة النهاية نفسها؛ اختر الاتجاه في نظام الموجّه.
كيف أمنع النموذج من تغيير العناصر النائبة مثل {name}؟
حدد القاعدة في الموجّه، ثم تحقق في الكود بمقارنة قوائم العناصر النائبة بين النص المصدر والنص الهدف، وأعد المحاولة أو علّم حالات عدم التطابق.
هل يوجد مفتاح تبديل لوضع JSON؟
يتم الحصول على الإخراج المنظم عبر التعليمات. حدد الشكل الدقيق في الموجّه، وأزل علامات الكود الزائدة، ثم قم بتحليل النتيجة والتحقق من صحتها.
ما السرعة التي يمكنني بها تشغيل دفعة كبيرة؟
يسمح كل مفتاح بـ 300 طلب في الدقيقة. وزّع الطلبات بانتظام، واستخدم التراجع عند استجابات 429 أو 503.
مفتاحك على بُعد نموذج واحد
أنشئ حسابًا، وانسخ المفتاح، ثم غيّر عنوان URL الأساسي. هذه هي عملية الإعداد كاملةً.