كيفية أتمتة ترجمة ملفات .po في مسار CI/CD الخاص بك

يقوم فريقك بدمج التغييرات إلى الفرع main أربعة عشر مرة في اليوم. قد يضيف كل عملية دمج من هذه سلسلة نصية جديدة يراها المستخدم - تسمية زر، رسالة خطأ، تلميح. وتبدأ كل سلسلة نصية من هذه باللغة الإنجليزية فقط. في مكان ما ضمن عملية الإصدار الخاصة بك، يجب على شخص ما ملاحظة السلاسل الجديدة، وتصديرها، وترجمتها، ولصقها مرة أخرى، وإعادة تجميعها. هذا الشخص يمثل عنق زجاجة، وفي فريق سريع التسليم، يعني عنق الزجاجة هذا أن المستخدمين الألمان يرون عناصر نائبة باللغة الإنجليزية لمدة دورتين (sprints).
الحل هو أتمتة الترجمة في مسار CI/CD الخاص بك بحيث تتم ترجمة السلاسل الجديدة في نفس عملية الدمج التي أدخلتها - لا يوجد عنصر بشري في المسار الحرج للحالات الروتينية. وهذا قابل للتحقيق بالكامل اليوم، ولكن فقط إذا تجنبت الفخ الذي تقع فيه معظم الفرق: وهو الاستخدام اليدوي لاستدعاءات LLM الخام التي تفسد بهدوء عناصرك النائبة %s وبنية ملفات .po. يشرح هذا الدليل نمط مسار CI واقعي، وهيكل GitHub Actions عملي، وقرارات التصميم - مثل الاستقلالية (idempotency)، والترجمة فقط للجديد، وبوابات المراجعة - التي تميز المسار الذي يساعد عن المسار الذي ينتج إصدارات معطلة.
لماذا تعتبر الترجمة اليدوية عنق زجاجة للإصدار
الجواب على سؤال "لماذا لا نترجم ببساطة قبل كل إصدار" هو أن التوقيت لا يتوافق أبدًا. تتم إضافة السلاسل النصية باستمرار من قبل المطورين، ولكن الترجمة تتم على دفعات بواسطة شخص مختلف وفق جدول زمني مختلف. الفجوة بين هذين الإيقاعين هي حيث تتراكم ديون التوطين لديك.
مشكلة الإيقاعين
تخيل سير العمل اليدوي. يضيف مطور __( 'Export to CSV', 'mytextdomain' ) ويقوم بالدمج. لا أحد يعيد إنشاء ملف .pot. بعد أسبوعين، يقوم شخص ما بتشغيل wp i18n make-pot، ويلاحظ أربعين سلسلة نصية جديدة غير مترجمة (بعضها من مطورين غادروا لقضاء إجازة منذ ذلك الحين)، ويخمّن الغرض من نصفها، ويرسل ملف .po إلى مترجم. تعود الترجمة، وتُلصق في مكانها، وربما تكون العناصر النائبة قد نجت وربما لم تنجُ. في هذه الأثناء، تم إصدار ثلاثة إصدارات مع تلك السلاسل باللغة الإنجليزية.
كل خطوة هناك يدوية، مجمعة، وعرضة للخطأ. الهدف من أتمتة CI/CD هو تحويل هذا إلى شيء يعمل تلقائيًا عند الدمج، ويترجم فقط ما تغير بالفعل، ويفشل بصوت عالٍ عندما يبدو شيء خاطئًا - محولًا الترجمة من مهمة دورية إلى جزء غير مرئي من المسار.
نمط المسار: استخراج، مقارنة، ترجمة، التزام
على مستوى عالٍ، تقوم مهمة الترجمة الآلية بتشغيل أربع خطوات عند الدمج في الفرع main: إعادة إنشاء القالب، اكتشاف ما هو جديد، ترجمة تلك السلاسل فقط، وإعادة التزام النتائج. يجب أن يكون كل شيء مستقلاً (idempotent) - تشغيله على دمج بدون تغييرات في السلاسل يجب أن ينتج فرقًا صفريًا وضوضاء صفرية.
استخراج والمقارنة
الخطوة الأولى هي الاستخراج. أعد إنشاء ملف .pot من المصدر الحالي ليعكس كل سلسلة نصية في قاعدة التعليمات البرمجية:
wp i18n make-pot . languages/myplugin.pot
الخطوة الثانية هي المقارنة (diff). هذا هو أهم قرار تصميمي في المسار بأكمله. لا ترغب في إعادة ترجمة كل سلسلة نصية في كل دمج - فهذا يهدر استدعاءات API، ويخاطر بإعادة ترجمة سلاسل قام البشر بتصحيحها بالفعل، وينتج فروقات مراجعة ضخمة. بدلاً من ذلك، تقوم بدمج ملف .pot الجديد في كل ملف .po موجود وترجمة الإدخالات الفارغة فقط (السلاسل الجديدة حقًا):
# Merge new template into each locale, preserving existing translations.
# Newly-added strings appear with empty msgstr; --backup=none keeps the tree clean.
for po in languages/*.po; do
msgmerge --update --backup=none "$po" languages/myplugin.pot
done
بعد msgmerge، فقط السلاسل الجديدة تمامًا ستحتوي على msgstr فارغ. كل ما تمت ترجمته سابقًا يبقى دون تغيير. هذه الخاصية هي ما يجعل المسار مستقلاً (idempotent) وما يحافظ على فروقات المراجعة لديك صغيرة وقابلة للمراجعة.
ترجمة والتزام
الخطوة الثالثة ترسل تلك الإدخالات الفارغة فقط إلى خطوة الترجمة. الخطوة الرابعة تلتزم بملف .po المحدث، وتُجمِّع ملف .mo، وتُعيد إنشاء أي ملفات JSON. سنقوم بربط كل ذلك في GitHub Actions لاحقًا.
هيكل GitHub Actions
إليك الإجابة على سؤال "كيف يبدو هذا في ملف سير العمل بالفعل": مهمة تُشغل عند الدفع (push) إلى الفرع main تقوم بتشغيل دورة الاستخراج-المقارنة-الترجمة-الالتزام وتفتح طلب سحب (pull request) بالنتائج بدلاً من الالتزام مباشرةً إلى الفرع main.
ملف سير العمل
name: Translate on merge
on:
push:
branches: [ main ]
jobs:
translate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install WP-CLI and gettext
run: |
sudo apt-get update && sudo apt-get install -y gettext
curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar
sudo mv wp-cli.phar /usr/local/bin/wp && sudo chmod +x /usr/local/bin/wp
- name: Regenerate template and merge into locales
run: |
wp i18n make-pot . languages/myplugin.pot
for po in languages/*.po; do
msgmerge --update --backup=none "$po" languages/myplugin.pot
done
- name: Translate only new (empty) strings
env:
TRANSLATE_API_KEY: ${{ secrets.TRANSLATE_API_KEY }}
run: ./scripts/translate-new-strings.sh languages/
- name: Compile .mo and JSON
run: |
wp i18n make-mo languages/
wp i18n make-json languages/ --no-purge
- name: Open pull request with translations
uses: peter-evans/create-pull-request@v6
with:
branch: chore/auto-translations
title: "chore: auto-translate new strings"
commit-message: "chore: auto-translate new strings"
أين يختبئ العمل الحقيقي
خطوة translate-new-strings.sh هي حيث تحدث الترجمة الفعلية. النسخة الساذجة من هذا السكريبت تقرأ كل إدخال فارغ، وترسله إلى واجهة برمجة تطبيقات LLM سلسلة نصية واحدة تلو الأخرى، وتلصق الاستجابة مرة أخرى. تلك النسخة الساذجة هي الفخ بالضبط، ويجدر بنا أن نكون واضحين بشأن السبب.
لماذا تفسد استدعاءات LLM اليدوية ملفاتك
الإجابة المختصرة: استدعاءات LLM الخام تتعامل مع سلاسل .po الخاصة بك كنص نثري، وسلاسل .po ليست نصًا نثريًا - إنها بيانات منظمة تحتوي على عناصر نائبة، وصيغ جمع، وسياق يقوم استدعاء إكمال الدردشة بتدميرها بسعادة.
فساد العناصر النائبة الذي لن تراه في الاختبارات
أرسل Deleted %d of %s files إلى نقطة نهاية دردشة عامة وقد تحصل على Supprimé %d des %s fichiers (جيد) أو Supprimé %d de % s fichiers (تسربت مسافة إلى العنصر النائب، والآن يرمي sprintf() خطأً في وقت التشغيل). أرسل إدخالًا بصيغة جمع وقد يدمج النموذج صيغتين في واحدة، مما يؤدي إلى كسر اللغات التي تحتوي على أكثر من فئتين للجمع. أرسل سلسلة نصية تحتوي على <a href="%s"> وقد يترجم النموذج عنوان URL أو يسقط الوسم. لا تظهر أي من هذه الإخفاقات في اختباراتك ما لم تختبر على وجه التحديد المخرجات المعروضة في كل لغة محلية - بل تظهر كأخطاء وقت التشغيل في بيئة الإنتاج للمستخدمين الذين لا يمكنك قراءة تقارير الأخطاء منهم.
فخ الصيانة
يمكنك محاولة الدفاع ضد هذا باستخدام هندسة الأوامر (prompt engineering) والمعالجة اللاحقة بالتعابير النمطية (regex)، وتفعل ذلك العديد من الفرق. المشكلة هي أنك الآن تقوم بصيانة محرك ترجمة هش كمشروع جانبي، وتعيد اكتشاف كل حالة خاصة للعناصر النائبة التي حلها نظام Gettext البيئي بالفعل. لقد قمنا بفهرسة الطرق المحددة التي تتشوه بها المتغيرات - وكيفية منعها - في ترجمة ملفات PO دون كسر متغيرات التعليمات البرمجية. الدرس هنا ينطبق مباشرة: جودة النموذج نادرًا ما تكون هي المشكلة؛ بل المعالجة الهيكلية المحيطة به هي المشكلة.
دمج مترجم سحابي يعتمد على API في CI
هنا، تحل خدمة ترجمة تعتمد على واجهة برمجة تطبيقات (API) محل تخمينات سكريبت translate-new-strings.sh. بدلاً من الاستخدام اليدوي لاستدعاءات LLM والمعالجة اللاحقة بالتعابير النمطية (regex)، تقوم خطوة CI الخاصة بك بتحميل ملف .po المتغير إلى خدمة تفهم بالفعل بنية Gettext وتُرجع مخرجات نظيفة. يبقى شكل المسار كما هو - استخراج، مقارنة، ترجمة، التزام - ولكن الخطوة الوسطى الهشة تصبح استدعاء API واحدًا.
استدعاء API الذي يحل محل السكريبت الخاص بك
تم تصميم SimplePoTranslate لهذا الغرض تحديدًا. إنه يقدم واجهة برمجة تطبيقات سحابية (cloud API) مناسبة للأتمتة وCI، لذا تصبح خطوة الترجمة في سير عملك طلبًا يسلم ملف .po ويعيد ملفًا مترجمًا، بدون حلقات لكل سلسلة. يقوم قفل بناء الجملة (Syntax Locking) الخاص به بتثبيت %s، %1$s، {count}، HTML، ورموز التعليمات البرمجية تلقائيًا في مكانها - يتم التعامل مع فئة الأخطاء بأكملها من القسم السابق بواسطة المحرك بدلاً من التعابير النمطية (regex) التي تقوم بصيانتها. مع دعم Gettext الكامل للجمع (plural) وmsgctxt، تبقى صيغ الجمع والسياق سليمة خلال العملية ذهابًا وإيابًا، وهو ما لا يمكن لاستدعاء إكمال الدردشة (chat-completion) ضمانه.
الاستقلالية (Idempotency) وبوابة المراجعة تبقى من مسؤوليتك
لا يزال هناك قراران تصميميان يخصانك بغض النظر عن المحرك الذي تستخدمه. أولاً، الاستقلالية (idempotency): استمر في ترجمة الإدخالات الفارغة فقط بعد msgmerge، بحيث لا تنتج عمليات الدمج التي لا تتضمن تغييرات أي فروقات. ثانيًا، بوابة مراجعة: اجعل المهمة تفتح طلب سحب (pull request)، كما يفعل الهيكل أعلاه، بدلاً من الالتزام مباشرةً بالفرع main. تعتبر الترجمة الآلية ممتازة لتفعيل السلاسل النصية بسرعة، ولكن نظرة بشرية قبل الدمج تلتقط الأخطاء السياقية النادرة - ويمنحك طلب السحب (PR) تلك النظرة دون إعاقة الحالة الشائعة. ستتعرف الفرق التي تتعامل مع العديد من اللغات المحلية أو مواقع العملاء المتعددة على هذا النمط من سير عمل التوطين المثالي للوكالات، حيث يتوسع نفس نمط الأتمتة ثم المراجعة عبر عشرات المشاريع.
الخلاصة
لأتمتة الترجمة في CI/CD دون إنتاج إصدارات معطلة، يكون النمط ثابتًا: أعد إنشاء ملف .pot عند الدمج، وادمج msgmerge في كل لغة محلية لإظهار السلاسل الجديدة فقط، وترجم تلك السلاسل فقط، والتزم بالنتيجة خلف بوابة مراجعة طلب سحب (pull request). الاستقلالية (Idempotency) تحافظ على فروقاتك نظيفة؛ وبوابة المراجعة تمنع الترجمات السيئة النادرة من الوصول إلى الإنتاج.
الجزء الذي يجب إتقانه هو خطوة الترجمة نفسها. استدعاءات LLM اليدوية ستفسد العناصر النائبة وصيغ الجمع بطرق لن تلتقطها اختباراتك، وستقضي وقتًا أطول في صيانة ربط الترجمة (translation glue) بدلاً من تعليمات الميزة التي تخدمها. يزيل محرك يعتمد على API مزود بقفل بناء الجملة (Syntax Locking) وضع الفشل هذا بالكامل، لذا يقوم مسار عملك بترجمة السلاسل الجديدة في نفس عملية الدمج التي أدخلتها - ويتوقف المستخدمون غير الناطقين بالإنجليزية عن رؤية العناصر النائبة الإنجليزية.
هل أنت مستعد لأتمتة الترجمة في مسار عملك دون كسر العناصر النائبة؟ جرب SimplePoTranslate مجانًا — لا يلزم وجود بطاقة ائتمان. ابدأ بالطبقة المجانية، وتحقق من صلاحية واجهة برمجة التطبيقات (API) مقابل ملفات
.poالخاصة بك، وادمجها في CI عندما تكون جاهزًا.