วิธีการทำให้การแปลไฟล์ .po เป็นอัตโนมัติใน CI/CD Pipeline ของคุณ

ทีมของคุณทำการผสาน (merge) ไปยัง main วันละสิบสี่ครั้ง การผสานแต่ละครั้งอาจเพิ่มสตริงที่ผู้ใช้เห็นใหม่ เช่น ป้ายกำกับปุ่ม ข้อความแสดงข้อผิดพลาด หรือคำแนะนำ (tooltip) และสตริงเหล่านั้นทั้งหมดเริ่มต้นในภาษาอังกฤษเท่านั้น ในกระบวนการเผยแพร่ (release process) ของคุณ มนุษย์จะต้องสังเกตเห็นสตริงใหม่ ส่งออก แปล วางกลับ และคอมไพล์ใหม่ มนุษย์ผู้นั้นคือคอขวด และในทีมที่ส่งมอบงานได้อย่างรวดเร็ว คอขวดนี้หมายความว่าผู้ใช้ชาวเยอรมันของคุณจะเห็น English placeholders เป็นเวลาสองรอบการทำงาน (sprint).
วิธีแก้ไขคือ ทำให้การแปลเป็นอัตโนมัติใน CI/CD pipeline ของคุณ เพื่อให้สตริงใหม่ได้รับการแปลในการผสานเดียวกันกับที่แนะนำเข้ามา โดยไม่มีมนุษย์อยู่ในเส้นทางวิกฤติสำหรับกรณีปกติ สิ่งนี้สามารถทำได้ทั้งหมดในปัจจุบัน แต่จะต้องหลีกเลี่ยงกับดักที่ทีมส่วนใหญ่ตกเข้าไป นั่นคือการเขียนโค้ดเรียกใช้ LLM ดิบเอง ซึ่งจะทำให้ %s placeholders และโครงสร้าง .po ของคุณเสียหายโดยที่คุณไม่รู้ตัว คู่มือนี้จะนำเสนอรูปแบบ CI pipeline ที่เป็นจริง โครงสร้าง GitHub Actions ที่ใช้งานได้ และการตัดสินใจในการออกแบบ เช่น idempotency, การแปลเฉพาะส่วนที่เพิ่มใหม่, review gates ซึ่งเป็นตัวแยก pipeline ที่มีประโยชน์ออกจาก pipeline ที่ส่งมอบงานที่ชำรุด
ทำไมการแปลด้วยตนเองจึงเป็นคอขวดในการเผยแพร่
คำตอบสำหรับคำถามที่ว่า "ทำไมไม่แปลก่อนการเผยแพร่แต่ละครั้ง" ก็คือเวลาไม่เคยตรงกัน นักพัฒนาเพิ่มสตริงอย่างต่อเนื่อง แต่การแปลจะเกิดขึ้นเป็นชุดโดยบุคคลอื่นตามกำหนดเวลาที่แตกต่างกัน ช่องว่างระหว่างสองจังหวะนี้คือจุดที่ Localization debt ของคุณสะสม
ปัญหาเรื่องจังหวะเวลาสองแบบ
ลองนึกภาพขั้นตอนการทำงานแบบ Manual นักพัฒนาเพิ่ม __( 'Export to CSV', 'mytextdomain' ) และผสาน (merge) ไม่มีใครสร้างไฟล์ .pot ใหม่ สองสัปดาห์ต่อมา มีคนรัน wp i18n make-pot สังเกตเห็นสตริงใหม่ที่ยังไม่ได้แปลสี่สิบสตริง (บางส่วนมาจากนักพัฒนาที่ลาพักร้อนไปแล้ว) เดาความตั้งใจของครึ่งหนึ่ง และส่งไฟล์ .po ไปยังนักแปล คำแปลกลับมา ถูกวาง (paste) เข้าไป และอาจจะ placeholders ยังคงอยู่ หรืออาจจะไม่ ในขณะเดียวกัน มีสามเวอร์ชันเผยแพร่ (release) ออกไปพร้อมกับสตริงเหล่านั้นในภาษาอังกฤษ
ทุกขั้นตอนนั้นเป็นแบบ Manual, ทำเป็นชุด (batched) และมีแนวโน้มที่จะเกิดข้อผิดพลาด เป้าหมายของระบบอัตโนมัติ CI/CD คือการรวมสิ่งเหล่านี้ให้เป็นสิ่งที่ทำงานโดยอัตโนมัติเมื่อเกิดการผสาน (merge) แปลเฉพาะส่วนที่เปลี่ยนแปลงไปจริง ๆ และแจ้งข้อผิดพลาดอย่างชัดเจนเมื่อมีสิ่งผิดปกติ – เปลี่ยนการแปลจากงานที่ต้องทำเป็นช่วง ๆ ให้กลายเป็นส่วนหนึ่งที่ไม่สามารถมองเห็นได้ของ pipeline
รูปแบบ Pipeline: Extract, Diff, Translate, Commit
ในระดับสูง งานแปลอัตโนมัติจะดำเนินการสี่ขั้นตอนเมื่อมีการผสาน (merge) ไปยัง main: สร้าง template ใหม่, ตรวจจับว่ามีอะไรใหม่, แปลเฉพาะสตริงเหล่านั้น และคอมมิตผลลัพธ์กลับ ทั้งหมดนี้ควรเป็นแบบ idempotent – การรันในกรณีที่มีการผสาน (merge) โดยไม่มีการเปลี่ยนแปลงสตริงจะต้องไม่สร้างความแตกต่าง (diff) และไม่มีข้อผิดพลาดใด ๆ
Extract และ Diff
ขั้นตอนแรกคือการ Extract สร้างไฟล์ .pot ใหม่จากซอร์สโค้ดปัจจุบัน เพื่อให้สะท้อนถึงทุกสตริงใน codebase:
wp i18n make-pot . languages/myplugin.pot
ขั้นตอนที่สองคือการ Diff นี่คือการตัดสินใจด้านการออกแบบที่สำคัญที่สุดใน pipeline ทั้งหมด คุณไม่ต้องการแปลทุกสตริงซ้ำในการผสานทุกครั้ง – ซึ่งจะทำให้เสียการเรียกใช้ API, เสี่ยงต่อการแปลสตริงที่มนุษย์แก้ไขไปแล้วซ้ำ และสร้าง review diffs ที่ใหญ่มาก แต่คุณจะผสานไฟล์ .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 ว่างเปล่า ทุกสิ่งที่เคยแปลไปแล้วจะยังคงไม่ถูกแตะต้อง คุณสมบัตินั้นคือสิ่งที่ทำให้ pipeline เป็น idempotent และสิ่งที่ทำให้ review diffs ของคุณมีขนาดเล็กและสามารถตรวจสอบได้
Translate และ Commit
ขั้นตอนที่สามคือการส่งรายการที่ว่างเปล่าเหล่านั้นไปยังขั้นตอนการแปล ขั้นตอนที่สี่คือการคอมมิตไฟล์ .po ที่อัปเดต คอมไพล์ไฟล์ .mo และสร้างไฟล์ JSON ใหม่ เราจะเชื่อมโยงทั้งหมดนั้นเข้ากับ GitHub Actions ต่อไป
โครงสร้าง GitHub Actions
นี่คือคำตอบของคำถามที่ว่า "สิ่งนี้มีหน้าตาอย่างไรในไฟล์ workflow": คืองาน (job) ที่ทำงานเมื่อมีการ push ไปยัง main ซึ่งจะเรียกใช้รอบการทำงาน extract-diff-translate-commit และเปิด pull request พร้อมผลลัพธ์ แทนที่จะคอมมิตโดยตรงไปยัง main
ไฟล์ Workflow
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 คือจุดที่การแปลเกิดขึ้นจริง สคริปต์เวอร์ชันที่ง่ายเกินไป (naive version) นั้นจะอ่านแต่ละรายการที่ว่างเปล่า ส่งไปยัง LLM API ทีละสตริง และวางการตอบกลับคืน เวอร์ชันที่ง่ายเกินไปนั้นเป็นกับดักที่แท้จริง และคุ้มค่าที่จะอธิบายให้ชัดเจนว่าทำไม
ทำไมการเรียกใช้ LLM ที่เขียนเองจึงทำให้ไฟล์ของคุณเสียหาย
คำตอบสั้นๆ คือ การเรียกใช้ LLM แบบดิบจะถือว่าสตริง .po ของคุณเป็นร้อยแก้ว แต่สตริง .po ของคุณไม่ใช่ร้อยแก้ว — เป็นข้อมูลที่มีโครงสร้างซึ่งมี placeholders, รูปพหูพจน์ (plural forms) และบริบทที่การเรียกใช้ chat-completion จะทำลายทิ้งอย่างง่ายดาย
การเสียหายของ Placeholder ที่คุณจะมองไม่เห็นในการทดสอบ
ลองส่ง Deleted %d of %s files ไปยัง chat endpoint ทั่วไป คุณอาจได้รับกลับมาเป็น Supprimé %d des %s fichiers (ซึ่งดี) หรือ Supprimé %d de % s fichiers (ซึ่งมีช่องว่างแทรกเข้ามาใน placeholder และตอนนี้ sprintf() จะเกิดข้อผิดพลาดเมื่อรันไทม์) ส่งรายการแบบพหูพจน์ (plural entry) และโมเดลอาจรวมสองรูปแบบให้เป็นหนึ่งเดียว ทำให้ภาษาที่มีหมวดหมู่พหูพจน์มากกว่าสองเสียหาย ส่งสตริงที่มี <a href="%s"> และโมเดลอาจแปล URL หรือตัดแท็กออก ความล้มเหลวเหล่านี้จะไม่ปรากฏในการทดสอบของคุณ เว้นแต่คุณจะทดสอบเอาต์พุตที่แสดงผลในทุกภาษาเฉพาะ – พวกมันจะปรากฏเป็นข้อผิดพลาดรันไทม์ในการใช้งานจริงสำหรับผู้ใช้ที่คุณไม่สามารถอ่านรายงานข้อผิดพลาดได้
กับดักการบำรุงรักษา
คุณสามารถพยายามป้องกันสิ่งนี้ด้วย prompt engineering และการประมวลผลหลัง (post-processing) แบบ regex และหลายทีมก็ทำเช่นนั้น ปัญหาคือตอนนี้คุณกำลังบำรุงรักษาเอนจินการแปลที่เปราะบางในฐานะโปรเจกต์เสริม และค้นพบกรณีขอบ (edge case) ของ placeholder ทุกกรณีที่ระบบนิเวศ Gettext ได้แก้ไขไปแล้ว เราได้รวบรวมวิธีเฉพาะที่ตัวแปรถูกบิดเบือน – และวิธีการป้องกัน – ไว้ใน การแปลไฟล์ PO โดยไม่ทำให้ code variables เสียหาย บทเรียนนั้นนำมาใช้ได้โดยตรง: คุณภาพของโมเดลไม่ใช่ปัญหาเสมอไป; การจัดการโครงสร้างรอบๆ ตัวต่างหากที่เป็นปัญหา
การนำ Cloud Translator ที่ขับเคลื่อนด้วย API เข้าสู่ CI
นี่คือจุดที่บริการแปลที่ขับเคลื่อนด้วย API เข้ามาแทนที่การคาดเดาด้วย translate-new-strings.sh ของคุณ แทนที่จะเขียนโค้ดเรียกใช้ LLM และการประมวลผลหลังแบบ regex เอง ขั้นตอน CI ของคุณจะอัปโหลดไฟล์ .po ที่เปลี่ยนแปลงไปยังบริการที่เข้าใจโครงสร้าง Gettext อยู่แล้ว และส่งคืนผลลัพธ์ที่สะอาด รูปแบบ pipeline ยังคงเหมือนเดิม — extract, diff, translate, commit — แต่ขั้นตอนกลางที่เปราะบางจะกลายเป็นการเรียกใช้ API ครั้งเดียว
การเรียกใช้ API ที่มาแทนที่สคริปต์ของคุณ
SimplePoTranslate ถูกสร้างขึ้นมาเพื่อสิ่งนี้โดยเฉพาะ มันนำเสนอ cloud API ที่เหมาะสมกับระบบอัตโนมัติและ CI ดังนั้นขั้นตอนการแปลของ workflow ของคุณจึงกลายเป็นการร้องขอที่ส่งมอบไฟล์ .po และได้รับไฟล์ที่แปลกลับมา โดยไม่ต้องวนลูปทีละสตริง Syntax Locking ของมันจะรักษา %s, %1$s, {count}, HTML และ code tokens ไว้ในตำแหน่งโดยอัตโนมัติ – บักทั้งหมดจากส่วนก่อนหน้าจะถูกจัดการโดยเอนจิน แทนที่จะเป็น regex ที่คุณต้องดูแลรักษาเอง ด้วยการรองรับ Gettext plural และ msgctxt อย่างเต็มรูปแบบ รูปพหูพจน์ (plural forms) และบริบทจะยังคงอยู่ตลอดกระบวนการ ซึ่งการเรียกใช้ chat-completion ไม่สามารถรับประกันได้
Idempotency และ Review Gate ยังคงเป็นของคุณ
การตัดสินใจด้านการออกแบบสองอย่างยังคงเป็นของคุณ ไม่ว่าคุณจะใช้เอนจินใดก็ตาม อย่างแรกคือ idempotency: ให้แปลเฉพาะรายการที่ว่างเปล่าหลังจาก msgmerge เพื่อให้การผสานที่ไม่มีการดำเนินการ (no-op merges) ไม่สร้างความแตกต่าง (diff) อย่างที่สองคือ review gate: ให้งานเปิด pull request เหมือนกับโครงสร้างข้างต้น แทนที่จะคอมมิตโดยตรงไปยัง main การแปลด้วยเครื่องนั้นยอดเยี่ยมสำหรับการทำให้สตริงพร้อมใช้งานได้อย่างรวดเร็ว แต่การตรวจสอบโดยมนุษย์ก่อนการผสานจะช่วยจับข้อผิดพลาดด้านบริบทที่เกิดขึ้นได้ยาก – และ PR จะช่วยให้คุณตรวจสอบได้โดยไม่ขัดขวางกรณีทั่วไป ทีมที่จัดการหลายภาษา (locales) หรือหลายเว็บไซต์ลูกค้าจะจดจำรูปแบบนี้ได้จาก เวิร์กโฟลว์การแปลที่เหมาะสมสำหรับเอเจนซี่ ซึ่งรูปแบบการทำให้เป็นอัตโนมัติแล้วตรวจสอบเดียวกันนี้สามารถปรับขนาดได้ในหลายสิบโปรเจกต์
บทสรุป
เพื่อให้ การแปลเป็นอัตโนมัติใน CI/CD โดยไม่ส่งมอบงานที่ชำรุด รูปแบบที่ใช้จะสอดคล้องกัน: สร้างไฟล์ .pot ใหม่เมื่อมีการผสาน (merge), msgmerge เข้าไปในแต่ละภาษาเพื่อแสดงเฉพาะสตริงใหม่, แปลเฉพาะสตริงเหล่านั้น และคอมมิตผลลัพธ์ภายใต้ review gate ของ pull request Idempotency ทำให้ diffs ของคุณสะอาด และ review gate ช่วยป้องกันการแปลที่ผิดพลาดที่เกิดขึ้นได้ยากไม่ให้เข้าสู่การใช้งานจริง
ส่วนที่ต้องทำให้ถูกต้องคือขั้นตอนการแปลเอง การเรียกใช้ LLM ที่เขียนเองจะทำให้ placeholders และรูปพหูพจน์เสียหายในลักษณะที่การทดสอบของคุณจะไม่ตรวจพบ และคุณจะใช้เวลาในการบำรุงรักษาโค้ดที่เชื่อมต่อการแปล (translation glue) มากกว่าโค้ดคุณสมบัติหลักที่มันให้บริการ เอนจินที่ขับเคลื่อนด้วย API พร้อมด้วย Syntax Locking จะขจัดข้อผิดพลาดทั้งหมดนั้นออกไป ดังนั้น pipeline ของคุณจะแปลสตริงใหม่ในการผสานเดียวกันกับที่เพิ่มเข้ามา – และผู้ใช้ที่ไม่ใช่ภาษาอังกฤษของคุณก็จะหยุดเห็น English placeholders
พร้อมที่จะทำให้การแปลใน pipeline ของคุณเป็นอัตโนมัติโดยไม่ทำให้ placeholders เสียหายแล้วหรือยัง? ลองใช้ SimplePoTranslate ฟรี — ไม่ต้องใช้บัตรเครดิต เริ่มต้นด้วยแพ็กเกจฟรี, ตรวจสอบ API กับไฟล์
.poของคุณเอง, และเชื่อมโยงเข้ากับ CI เมื่อคุณพร้อม