กลับไปยังบล็อก

ทำไมการแปล Django ถึงพัง (และวิธีแก้ไข 10 สาเหตุที่พบบ่อยที่สุด)

2026-02-04 อ่าน 6 นาที
ทำไมการแปล Django ถึงพัง (และวิธีแก้ไข 10 สาเหตุที่พบบ่อยที่สุด)

คุณทำเครื่องหมายสตริงสำหรับการแปล สร้างไฟล์ .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

หยุดแก้ไขไฟล์ .po ด้วยตนเอง

TranslateBot ช่วยแปลภาษา Django อัตโนมัติด้วย AI เพียงคำสั่งเดียว ครอบคลุมทุกภาษา ค่าใช้จ่ายน้อยมาก