คุณทำเครื่องหมายสตริงสำหรับการแปล สร้างไฟล์ .po แล้ว รัน compilemessages แล้ว แต่แอปของคุณยังคงแสดงภาษาอังกฤษ คุณไม่ได้อยู่คนเดียว เฟรมเวิร์ก i18n ของ Django นั้นทรงพลัง แต่มีจุดที่คมซึ่งสามารถดักจับแม้แต่นักพัฒนาที่มีประสบการณ์
คู่มือนี้ครอบคลุม 10 สาเหตุที่พบบ่อยที่สุดที่ทำให้การแปลของ Django ล้มเหลวแบบเงียบ ๆ พร้อมอาการที่แน่ชัดและวิธีแก้ไขสำหรับแต่ละข้อ
1. ลืมรัน compilemessages หลังจากแก้ไขไฟล์ .po
คุณแก้ไขไฟล์ .po (ด้วยตนเองหรือด้วยเครื่องมือ) แต่ข้อความที่แปลไม่เคยปรากฏ แอปยังคงแสดงสตริงภาษาอังกฤษดั้งเดิม
Django ไม่อ่านไฟล์ .po ในขณะรันไทม์ แต่จะอ่านไฟล์ไบนารี .mo (machine object) ที่คอมไพล์แล้วแทน ถ้าคุณแก้ไขไฟล์ .po โดยไม่คอมไพล์ใหม่ Django จะไม่รู้ว่ามีอะไรเปลี่ยนแปลง
รัน compilemessages หลังจากทุกการเปลี่ยนแปลงไฟล์ .po:
python manage.py compilemessages
ถ้าคุณทำการแปลอัตโนมัติด้วย TranslateBot ให้เพิ่ม compilemessages เป็นขั้นตอนสุดท้ายในเวิร์กโฟลว์ของคุณ:
python manage.py makemessages -a --no-obsolete
python manage.py translate
python manage.py compilemessages
2. ขาด {% load i18n %} ในเทมเพลต
คุณใช้ {% trans "Hello" %} ในเทมเพลต แต่ Django แสดง TemplateSyntaxError หรือแย่กว่านั้น แท็กไม่ทำอะไรเลยถ้า template engine ถูกกำหนดค่าผิด
แท็ก {% trans %} และ {% blocktrans %} อยู่ในไลบรารีแท็กเทมเพลต i18n ของ Django ถ้าไม่โหลด template engine จะไม่รู้จัก
เพิ่ม {% load i18n %} ที่ด้านบนของทุกเทมเพลตที่ใช้แท็กแปล:
{% load i18n %}
<h1>{% trans "Welcome to our site" %}</h1>
<p>{% blocktrans with name=user.name %}Hello, {{ name }}!{% endblocktrans %}</p>
นี่เป็นข้อกำหนดต่อเทมเพลต แม้ว่าเทมเพลตแม่จะโหลด i18n แล้ว เทมเพลตลูกที่ใช้แท็กแปลก็ต้องมีการประกาศ {% load i18n %} ของตัวเอง
3. ไม่มี LocaleMiddleware ใน MIDDLEWARE หรืออยู่ในตำแหน่งที่ผิด
Django แสดงเนื้อหาเป็นภาษาเริ่มต้นเสมอ ไม่ว่าเบราว์เซอร์จะส่งเฮดเดอร์ Accept-Language อะไร หรือมี URL prefix หรือการตั้งค่าเซสชันอย่างไร
LocaleMiddleware กำหนดภาษาที่ใช้งานสำหรับแต่ละคำขอ ถ้าไม่มี Django จะใช้ LANGUAGE_CODE เป็นค่าเริ่มต้นและเพิกเฉยต่อกลไกเลือกภาษาทั้งหมด ตำแหน่งของมันใน middleware stack ก็สำคัญเช่นกัน เพราะต้องเข้าถึงข้อมูลเซสชันและการแก้ไข URL
เพิ่ม LocaleMiddleware ในการตั้งค่า MIDDLEWARE ของคุณ หลังจาก SessionMiddleware และ CommonMiddleware:
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware",
"django.contrib.sessions.middleware.SessionMiddleware",
"django.middleware.common.CommonMiddleware",
"django.middleware.locale.LocaleMiddleware", # Must be after SessionMiddleware
"django.middleware.csrf.CsrfViewMiddleware",
"django.contrib.auth.middleware.AuthenticationMiddleware",
"django.contrib.messages.middleware.MessageMiddleware",
]
นอกจากนี้ให้แน่ใจว่า django.conf.urls.i18n ถูกรวมอยู่ในการกำหนดค่า URL ถ้าคุณใช้การสลับภาษาผ่าน URL:
from django.conf.urls.i18n import i18n_patterns
urlpatterns = i18n_patterns(
path("", include("myapp.urls")),
)
4. รหัสภาษาไม่ตรงกัน (เช่น pt-br กับ pt_BR)
มีการแปลในไฟล์ .po ของคุณ compilemessages สำเร็จ แต่ Django เพิกเฉยต่อการแปลสำหรับบาง locale
Django คาดหวังให้ไดเรกทอรี locale ใช้รูปแบบ <language>_<COUNTRY> โดยใช้ขีดล่างเป็นตัวคั่นและรหัสประเทศเป็นตัวพิมพ์ใหญ่ เช่น pt_BR สำหรับภาษาโปรตุเกสบราซิล ถ้าไดเรกทอรีของคุณชื่อ pt-br, pt-BR หรือ ptBR Django จะหาไม่เจอ เช่นเดียวกันกับการตั้งค่า LANGUAGES: รหัสที่นั่นใช้ขีดกลาง (pt-br) แต่ระบบไฟล์ใช้ขีดล่าง (pt_BR)
ตรวจสอบว่าโครงสร้างไดเรกทอรีของคุณตรงกับที่ Django คาดหวัง:
locale/
pt_BR/
LC_MESSAGES/
django.po
django.mo
และในการตั้งค่า ให้ใช้รูปแบบที่มีขีดกลาง:
LANGUAGES = [
("en", "English"),
("pt-br", "Brazilian Portuguese"),
("zh-hans", "Simplified Chinese"),
]
เมื่อรัน makemessages ให้ใช้รูปแบบขีดล่างสำหรับ flag locale:
python manage.py makemessages -l pt_BR
5. รายการ Fuzzy ถูกข้ามอย่างเงียบ ๆ ระหว่างการคอมไพล์
มีการแปลในไฟล์ .po แต่ Django แสดงสตริงภาษาอังกฤษดั้งเดิมในขณะรันไทม์สำหรับรายการนั้นโดยเฉพาะ นี่น่าหงุดหงิดเป็นพิเศษเพราะการแปลอยู่ตรงนั้นในไฟล์
เมื่อ makemessages ของ Django ตรวจพบว่าสตริงต้นทางเปลี่ยนแปลงเล็กน้อย จะทำเครื่องหมายการแปลที่มีอยู่ว่า "fuzzy" (หมายความว่าเป็นการเดาที่ต้องการการตรวจสอบจากมนุษย์) คำสั่ง compilemessages จะข้ามรายการ fuzzy ทั้งหมดโดยถือว่าไม่ได้แปล ดังนั้นรายการดูเหมือนแปลแล้วในไฟล์ .po แต่ไฟล์ .mo ไม่รวมไว้เลย
รายการ fuzzy มีลักษณะดังนี้:
#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
ตรวจสอบการแปล อัปเดต msgstr ถ้าจำเป็น แล้วลบ flag #, fuzzy:
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
แล้วคอมไพล์ใหม่:
python manage.py compilemessages
ในโปรเจกต์ที่ใหญ่ขึ้น รายการ fuzzy จะสะสมและพลาดได้ง่าย คำสั่ง check_translations ของ TranslateBot จะจับสิ่งเหล่านี้โดยอัตโนมัติ:
python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed
เพิ่ม check_translations --makemessages ใน CI pipeline ของคุณแล้วคุณจะไม่ส่งรายการ fuzzy อีกต่อไป
6. LOCALE_PATHS ไม่ได้กำหนดค่าหรือชี้ไปยังไดเรกทอรีที่ผิด
makemessages สร้างไฟล์ .po ในตำแหน่งหนึ่ง แต่ Django ค้นหาในที่อื่น การแปลมีอยู่บนดิสก์แต่ไม่เคยถูกโหลด
Django ค้นหาไฟล์แปลตามลำดับเฉพาะ: ไดเรกทอรี LOCALE_PATHS ก่อน จากนั้นไดเรกทอรี locale/ ของแต่ละแอป และสุดท้ายไดเรกทอรี locale/ ของโปรเจกต์ ถ้า LOCALE_PATHS ไม่ได้ตั้งค่าหรือชี้ไปยังเส้นทางที่ผิด Django อาจหาไฟล์ .po ของคุณไม่เจอเลย
ตั้งค่า LOCALE_PATHS ในการตั้งค่าเป็นเส้นทางแบบสัมบูรณ์:
from pathlib import Path
BASE_DIR = Path(__file__).resolve().parent.parent
LOCALE_PATHS = [
BASE_DIR / "locale",
]
ตรวจสอบว่าไดเรกทอรีมีอยู่และมีโครงสร้างที่คาดหวัง:
locale/
de/
LC_MESSAGES/
django.po
django.mo
nl/
LC_MESSAGES/
django.po
django.mo
ข้อผิดพลาดที่พบบ่อยคือการตั้ง LOCALE_PATHS เป็น locale/ (สัมพัทธ์) แทนที่จะเป็นเส้นทางแบบสัมบูรณ์ Django ไม่แก้ไขเส้นทางสัมพัทธ์จากรูทของโปรเจกต์ มันขึ้นอยู่กับไดเรกทอรีทำงานของกระบวนการ ซึ่งมักไม่ใช่สิ่งที่คุณคาดหวัง
7. แคชให้บริการการแปลที่เก่า
คุณอัปเดตและคอมไพล์การแปลแล้ว แต่ข้อความเก่ายังคงปรากฏ การรีสตาร์ทเซิร์ฟเวอร์แก้ปัญหาได้
แค็ตตาล็อกการแปลของ Django จะถูกโหลดหนึ่งครั้งต่อกระบวนการ ในการใช้งานจริง เซิร์ฟเวอร์ WSGI/ASGI เช่น Gunicorn หรือ Uvicorn จะรักษากระบวนการ worker ให้ทำงานอยู่เป็นเวลานาน ไฟล์ .mo อาจเปลี่ยนแปลงบนดิสก์แล้ว แต่กระบวนการที่ทำงานอยู่ยังคงมีการแปลเก่าในหน่วยความจำ นอกจากนี้ ถ้าคุณใช้เฟรมเวิร์กแคชของ Django หรือ reverse proxy เช่น Nginx หรือ Cloudflare การตอบสนองที่แคชไว้จะให้บริการเนื้อหาเก่าจนกว่าจะหมดอายุ
รีสตาร์ทเซิร์ฟเวอร์แอปพลิเคชันหลังจากดีพลอยการแปลใหม่:
# Gunicorn
kill -HUP $(cat /tmp/gunicorn.pid)
# Systemd
sudo systemctl restart myapp
# Docker
docker compose restart web
สำหรับเฟรมเวิร์กแคชของ Django ให้ล้างแคชหลังจากอัปเดตการแปล:
from django.core.cache import cache
cache.clear()
ในการพัฒนา runserver จะรีโหลดอัตโนมัติเมื่อไฟล์ Python เปลี่ยนแปลง แต่ไม่เฝ้าดูไฟล์ .mo คุณต้องรีสตาร์ทด้วยตนเองหลังจากรัน compilemessages
8. สตริงที่ไม่ได้ครอบด้วย gettext (_() หรือ {% trans %})
makemessages ไม่สกัดสตริงบางตัว ดังนั้นจึงไม่เคยปรากฏในไฟล์ .po และไม่เคยถูกแปล นี่เป็นปัญหาพื้นฐานที่สุดและยังเป็นสิ่งที่ง่ายที่สุดที่จะมองข้ามในโค้ดเบสขนาดใหญ่
คำสั่ง makemessages ของ Django ใช้ xgettext เพื่อสแกนโค้ดต้นทางของคุณเพื่อหาตัวทำเครื่องหมายการแปล ถ้าสตริงไม่ได้ครอบด้วย gettext() (มักใช้นามแฝงว่า _()), gettext_lazy(), {% trans %} หรือ {% blocktrans %} จะมองไม่เห็นในกระบวนการสกัด
ครอบทุกสตริงที่ผู้ใช้จะเห็น:
# Python code
from django.utils.translation import gettext_lazy as _
class Article(models.Model):
class Meta:
verbose_name = _("article")
verbose_name_plural = _("articles")
# Views
from django.utils.translation import gettext as _
def my_view(request):
message = _("Your changes have been saved.")
return HttpResponse(message)
<!-- Templates -->
{% load i18n %}
<h1>{% trans "Welcome" %}</h1>
<p>{% blocktrans with count=items|length %}You have {{ count }} items.{% endblocktrans %}</p>
ใช้ flag --dry-run ของ TranslateBot เพื่อดูตัวอย่างสตริงที่ยังไม่ได้แปลในปัจจุบัน เพื่อจับสตริงที่พลาดระหว่างการสกัด:
python manage.py translate --target-lang de --dry-run
คำสั่งนี้แสดงรายการทั้งหมดที่ยังไม่ได้แปลในไฟล์ .po ของคุณโดยไม่มีการเรียก API หรือการเปลี่ยนแปลงใด ๆ
9. f-string ไม่สามารถแปลได้ (ข้อจำกัดของ Django)
คุณครอบ f-string ด้วย _() แล้วได้ syntax error หรือ makemessages สกัดสตริงที่เสียหาย/ไม่สมบูรณ์ที่ไม่สามารถแปลได้
f-string ของ Python จะถูกประเมินในขณะรันไทม์ เครื่องมือสกัด xgettext จะแยกวิเคราะห์โค้ดต้นทางแบบคงที่ จึงไม่สามารถประเมินนิพจน์ Python ภายในวงเล็บปีกกา {} ได้ ซึ่งหมายความว่า _(f"Hello, {name}") จะถูกสกัดเป็นสตริงที่มีนิพจน์ {name} แบบตัวอักษร (หรือล้มเหลวในการสกัดทั้งหมด) และรายการ .po ที่ได้จะไม่ตรงกับสตริงรันไทม์
ใช้การจัดรูปแบบ % ของ Django หรือ .format() กับ placeholder ที่มีชื่อแทน:
# Wrong -- f-string cannot be extracted
message = _(f"Hello, {user.name}! You have {count} new messages.")
# Correct -- named placeholders
message = _("Hello, %(name)s! You have %(count)d new messages.") % {
"name": user.name,
"count": count,
}
# Also correct -- .format() with positional args
message = _("Hello, {0}! You have {1} new messages.").format(user.name, count)
นี่ไม่ใช่ข้อจำกัดของ TranslateBot หรือเครื่องมือ แต่เป็นพื้นฐานของวิธีการทำงานของ gettext สตริงต้นทางต้องเป็น literal แบบคงที่เพื่อให้สามารถสกัดและค้นหาได้ในขณะรันไทม์
TranslateBot รักษารูปแบบ placeholder ทั้งหมดเหล่านี้ (%(name)s, {0}, %s, แท็ก HTML) ระหว่างการแปล ดังนั้นสตริงที่แปลแล้วจึงยังคงใช้งานได้อย่างสมบูรณ์
10. ข้อผิดพลาด Placeholder Format String ทำให้การคอมไพล์ .po ล้มเหลว
compilemessages ล้มเหลวพร้อมข้อผิดพลาด หรือไฟล์ .po มี flag #, python-format ไม่ตรงกัน และรายการถูกตัดออกอย่างเงียบ ๆ
เมื่อสตริงต้นทางมี placeholder รูปแบบ Python เช่น %(name)s Django จะทำเครื่องหมายรายการ .po ด้วย #, python-format ถ้าการแปลมี placeholder ที่แตกต่าง (พิมพ์ผิดเช่น %(nome)s, placeholder ที่หายไป หรือมีเกิน) เครื่องมือ gettext อาจปฏิเสธรายการหรือ compilemessages อาจล้มเหลว สิ่งนี้มักเกิดขึ้นกับการแปลด้วยมือหรือกับเครื่องมือแปล AI ที่ไม่เข้าใจความหมายของ placeholder
รายการที่เสียหายมีลักษณะดังนี้:
#, python-format
msgid "Hello, %(name)s! You have %(count)d new messages."
msgstr "Hallo, %(naam)s! Je hebt %(count)d nieuwe berichten."
ที่นี่ %(naam)s ควรเป็น %(name)s Placeholder ต้องตรงกับต้นทางอย่างแน่นอน
ตรวจสอบว่าสตริงที่แปลมี placeholder เดียวกันกับต้นทางอย่างแน่นอน ตรวจหาการพิมพ์ผิด placeholder ที่หายไป และ placeholder ที่มีเกิน
นี่เป็นด้านที่ TranslateBot ให้ข้อได้เปรียบที่แท้จริง ตรรกะการรักษา placeholder ของมันรับประกันว่า format string ทั้งหมด (%(name)s, {0}, %s) ในผลลัพธ์ที่แปลจะตรงกับต้นทางอย่างแน่นอน การจัดการ placeholder ได้รับการครอบคลุมด้วย test coverage 100% ดังนั้นข้อผิดพลาด format string จากการแปลจะถูกกำจัดที่ระดับเครื่องมือแทนที่จะถูกจับในเวลาคอมไพล์
ถ้าคุณแปลด้วยตนเองหรือใช้เครื่องมือที่ไม่จัดการ placeholder ให้ตรวจสอบไฟล์ .po ด้วย:
msgfmt --check-format locale/de/LC_MESSAGES/django.po
คำสั่งนี้รันการตรวจสอบ format string ของ gettext และรายงานความไม่ตรงกัน
รวมทุกอย่างเข้าด้วยกัน: เวิร์กโฟลว์แบบป้องกัน
ปัญหาเหล่านี้ส่วนใหญ่มีสาเหตุร่วมกัน: ขั้นตอนที่ทำด้วยมือซึ่งง่ายต่อการลืม นี่คือเวิร์กโฟลว์ที่ป้องกันปัญหาทั้ง 10 ข้อ:
# 1. Extract strings (catches #8 -- any new gettext-wrapped strings)
python manage.py makemessages -a --no-obsolete
# 2. Translate (catches #1, #5, #8, #9, #10 -- handles untranslated,
# fuzzy, and placeholder issues automatically)
python manage.py translate
# 3. Compile (catches #1 -- generates .mo files)
python manage.py compilemessages
# 4. Verify in CI (catches everything that slipped through)
python manage.py check_translations --makemessages
เพิ่มขั้นตอนที่ 4 ใน CI pipeline ของคุณแล้วสตริงที่ไม่ได้แปล รายการ fuzzy และข้อผิดพลาดรูปแบบจะทำให้ build ล้มเหลวก่อนถึง production
ตารางอ้างอิงด่วน
| สาเหตุ | อาการ | แก้ไขบรรทัดเดียว |
|---|---|---|
ไม่มี compilemessages |
การแปลมีอยู่แต่ไม่ปรากฏ | python manage.py compilemessages |
ขาด {% load i18n %} |
TemplateSyntaxError บน {% trans %} |
เพิ่ม {% load i18n %} ในเทมเพลต |
| ขาด LocaleMiddleware | ภาษาเป็นภาษาอังกฤษเริ่มต้นเสมอ | เพิ่ม django.middleware.locale.LocaleMiddleware ใน MIDDLEWARE |
| รหัสภาษาไม่ตรงกัน | ไม่พบไดเรกทอรี locale | ใช้ pt_BR (ขีดล่าง) สำหรับไดเรกทอรี, pt-br (ขีดกลาง) สำหรับการตั้งค่า |
| รายการ fuzzy ถูกข้าม | การแปลใน .po แต่ไม่ในแอป |
ลบ flag #, fuzzy หลังตรวจสอบ |
| LOCALE_PATHS ผิด | ไฟล์ .po มีอยู่แต่ Django เพิกเฉย |
ตั้งค่า LOCALE_PATHS เป็นเส้นทางแบบสัมบูรณ์ |
| การแปลถูกแคช | ข้อความเก่าปรากฏหลังอัปเดต | รีสตาร์ทเซิร์ฟเวอร์แอปพลิเคชัน |
| สตริงไม่อยู่ใน gettext | สตริงหายไปจากไฟล์ .po |
ครอบด้วย _() หรือ {% trans %} |
| f-string ใน gettext | การสกัดเสียหายหรือรันไทม์ไม่ตรงกัน | แทนที่ด้วย placeholder % หรือ .format() |
| placeholder ไม่ตรงกัน | compilemessages ล้มเหลวหรือรายการถูกตัด |
จับคู่ placeholder อย่างแน่นอนระหว่างต้นทางและการแปล |
ปัญหาเหล่านี้ส่วนใหญ่หายไปเมื่อคุณทำขั้นตอนการแปลให้เป็นอัตโนมัติและบังคับใช้การตรวจสอบใน CI คำสั่ง translate ของ TranslateBot จัดการการรักษา placeholder และการแปลแบบเพิ่มทีละน้อย ในขณะที่ check_translations จับทุกอย่างที่หลุดรอด (รายการที่ไม่ได้แปล, flag fuzzy และปัญหา format string) ก่อนถึง production