قمت بتعليم السلاسل النصية للترجمة، وأنشأت ملفات .po، وشغّلت compilemessages، ولا يزال تطبيقك يعرض الإنجليزية. لست وحدك. إطار عمل i18n في Django قوي، لكنه يحتوي على حواف حادة تُوقع حتى المطورين ذوي الخبرة.
يغطي هذا الدليل الأسباب العشرة الأكثر شيوعاً لفشل ترجمات Django بصمت، مع الأعراض الدقيقة والإصلاحات لكل منها.
1. نسيان تشغيل compilemessages بعد تعديل ملفات .po
قمت بتعديل ملف .po (يدوياً أو باستخدام أداة)، لكن النص المترجم لا يظهر أبداً. يستمر التطبيق في عرض السلاسل النصية الإنجليزية الأصلية.
لا يقرأ Django ملفات .po أثناء التشغيل. بل يقرأ ملفات .mo (كائن الآلة) الثنائية المُجمّعة بدلاً من ذلك. إذا عدّلت ملف .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. أو الأسوأ، أن الوسم لا يفعل شيئاً بصمت إذا كان محرك القوالب لديك مُعدّاً بشكل خاطئ.
وسوم {% trans %} و {% blocktrans %} موجودة في مكتبة وسوم قوالب i18n الخاصة بـ Django. بدون تحميلها، لا يتعرف محرك القوالب عليها.
أضف {% 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، أو إعدادات الجلسة.
يحدد LocaleMiddleware اللغة النشطة لكل طلب. بدونه، يعود Django إلى LANGUAGE_CODE الافتراضي ويتجاهل جميع آليات اختيار اللغة. موقعه في مكدس الوسيط مهم أيضاً، لأنه يحتاج إلى الوصول إلى بيانات الجلسة وتحليل 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 يتجاهل الترجمة لبعض اللغات المحلية.
يتوقع Django أن تتبع مجلدات اللغة المحلية التنسيق <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، استخدم شكل الشرطة السفلية لعلم اللغة المحلية:
python manage.py makemessages -l pt_BR
5. تخطي المدخلات الغامضة (fuzzy) بصمت أثناء التجميع
ترجمة موجودة في ملف .po، لكن Django يعرض السلسلة الإنجليزية الأصلية أثناء التشغيل لتلك المدخلة تحديداً. هذا الأمر محبط بشكل خاص لأن الترجمة موجودة فعلاً في الملف.
عندما يكتشف makemessages في Django أن سلسلة المصدر تغيرت قليلاً، يُعلّم الترجمة الحالية بـ "fuzzy" (بمعنى أنها تخمين يحتاج مراجعة بشرية). أمر compilemessages يتخطى جميع المدخلات الغامضة، معاملاً إياها كغير مترجمة. لذا تبدو المدخلة مترجمة في ملف .po، لكن ملف .mo يستبعدها تماماً.
مدخلة غامضة تبدو هكذا:
#, fuzzy
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
راجع الترجمة، حدّث msgstr إذا لزم الأمر، ثم أزل علم #, fuzzy:
msgid "Welcome to our website!"
msgstr "Welkom op onze website!"
ثم أعد التجميع:
python manage.py compilemessages
في المشاريع الكبيرة، تتراكم المدخلات الغامضة ويسهل تفويتها. أمر check_translations في TranslateBot يكتشفها تلقائياً:
python manage.py check_translations
locale/nl/LC_MESSAGES/django.po: 0 untranslated, 3 fuzzy
CommandError: Translation check failed
أضفه إلى خط أنابيب CI الخاص بك مع check_translations --makemessages ولن تشحن مدخلة غامضة مرة أخرى.
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 تُبقي عمليات العامل حية لفترات ممتدة. قد يكون ملف .mo قد تغير على القرص، لكن العملية الجارية لا تزال تحمل الترجمات القديمة في الذاكرة. بالإضافة إلى ذلك، إذا كنت تستخدم إطار عمل التخزين المؤقت في Django أو وكيلاً عكسياً مثل 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>
استخدم علم --dry-run في TranslateBot لمعاينة السلاسل غير المترجمة حالياً، حتى تتمكن من اكتشاف السلاسل التي فُقدت أثناء الاستخراج:
python manage.py translate --target-lang de --dry-run
يعرض هذا جميع المدخلات غير المترجمة عبر ملفات .po دون إجراء أي استدعاءات API أو تغييرات.
9. لا يمكن ترجمة f-strings (قيد في Django)
تلف f-string في _() وتحصل إما على خطأ في بناء الجملة، أو يستخرج makemessages سلسلة معطلة/جزئية لا يمكن ترجمتها.
يتم تقييم f-strings في Python أثناء التشغيل. أداة الاستخراج xgettext تحلل الشفرة المصدرية بشكل ثابت، لذا لا يمكنها تقييم تعبيرات Python داخل الأقواس المعقوفة {}. هذا يعني أن _(f"Hello, {name}") يتم استخراجها كسلسلة تحتوي على تعبير حرفي {name} (أو تفشل في الاستخراج تماماً)، ومدخلة .po الناتجة لن تتطابق أبداً مع السلسلة أثناء التشغيل.
استخدم تنسيق % في Django أو .format() مع عناصر نائبة مسماة بدلاً من ذلك:
# 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. يجب أن تكون السلسلة المصدر حرفية ثابتة حتى يمكن استخراجها والبحث عنها أثناء التشغيل.
يحافظ TranslateBot على جميع تنسيقات العناصر النائبة هذه (%(name)s, {0}, %s, وسوم HTML) أثناء الترجمة، لذا تبقى السلاسل المترجمة وظيفية بالكامل.
10. أخطاء تنسيق سلاسل العناصر النائبة تكسر تجميع .po
compilemessages يفشل مع خطأ، أو ملف .po لديه عدم تطابق في علم #, python-format، والمدخلة تُسقط بصمت.
عندما تحتوي سلسلة المصدر على عناصر نائبة بتنسيق Python مثل %(name)s، يُعلّم Django مدخلة .po بـ #, python-format. إذا كانت الترجمة تحتوي على عناصر نائبة مختلفة (خطأ مطبعي مثل %(nome)s، أو عنصر نائب مفقود، أو عنصر إضافي)، قد ترفض أدوات gettext المدخلة أو قد يفشل compilemessages. يحدث هذا عادةً مع الترجمات اليدوية أو مع أدوات الترجمة بالذكاء الاصطناعي التي لا تفهم دلالات العناصر النائبة.
مدخلة معطلة تبدو هكذا:
#, 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. يجب أن تتطابق العناصر النائبة مع المصدر تماماً.
تأكد من أن السلاسل المترجمة تحتوي على نفس العناصر النائبة تماماً كالمصدر. تحقق من الأخطاء المطبعية والعناصر النائبة المفقودة والعناصر النائبة الزائدة.
هذا مجال يوفر فيه TranslateBot ميزة حقيقية. منطق الحفاظ على العناصر النائبة يضمن أن جميع سلاسل التنسيق (%(name)s, {0}, %s) في المخرجات المترجمة تتطابق مع المصدر تماماً. تغطية التعامل مع العناصر النائبة مغطاة بنسبة 100% من تغطية الاختبارات، لذا يتم القضاء على أخطاء سلاسل التنسيق من الترجمة على مستوى الأداة بدلاً من اكتشافها في وقت التجميع.
إذا كنت تترجم يدوياً أو بأداة لا تتعامل مع العناصر النائبة، تحقق من ملفات .po باستخدام:
msgfmt --check-format locale/de/LC_MESSAGES/django.po
يشغّل هذا التحقق من صحة سلاسل تنسيق gettext ويُبلغ عن أي عدم تطابق.
تجميع كل شيء معاً: سير عمل دفاعي
معظم هذه المشكلات تشترك في سبب جذري: خطوات يدوية يسهل نسيانها. إليك سير عمل يمنع جميع المشكلات العشر:
# 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 الخاص بك وستفشل السلاسل غير المترجمة والمدخلات الغامضة وأخطاء التنسيق في بناء المشروع قبل وصولها إلى الإنتاج.
جدول مرجعي سريع
| السبب | العَرَض | إصلاح من سطر واحد |
|---|---|---|
عدم تشغيل compilemessages |
الترجمات موجودة لكنها لا تظهر | python manage.py compilemessages |
{% load i18n %} مفقود |
TemplateSyntaxError على {% trans %} |
أضف {% load i18n %} إلى القالب |
| LocaleMiddleware مفقود | اللغة دائماً إنجليزية افتراضياً | أضف django.middleware.locale.LocaleMiddleware إلى MIDDLEWARE |
| عدم تطابق رمز اللغة | مجلد اللغة المحلية غير موجود | استخدم pt_BR (شرطة سفلية) للمجلدات، pt-br (واصلة) للإعدادات |
| تخطي المدخلات الغامضة | الترجمة في .po لكن ليست في التطبيق |
أزل علم #, fuzzy بعد المراجعة |
| LOCALE_PATHS خطأ | ملفات .po موجودة لكن Django يتجاهلها |
عيّن LOCALE_PATHS بمسار مطلق |
| ترجمات مُخزّنة مؤقتاً | النص القديم يظهر بعد التحديث | أعد تشغيل خادم التطبيق |
| السلسلة ليست في gettext | السلسلة مفقودة من ملفات .po |
لُفها في _() أو {% trans %} |
| f-string في gettext | استخراج معطل أو عدم تطابق أثناء التشغيل | استبدل بعناصر نائبة % أو .format() |
| عدم تطابق العناصر النائبة | فشل compilemessages أو إسقاط المدخلة |
طابق العناصر النائبة تماماً بين المصدر والترجمة |
معظم هذه المشكلات تختفي عند أتمتة خطوة الترجمة وفرض الفحوصات في CI. أمر translate في TranslateBot يتعامل مع الحفاظ على العناصر النائبة والترجمة التزايدية، بينما check_translations يلتقط أي شيء يفلت (المدخلات غير المترجمة، أعلام fuzzy، ومشاكل سلاسل التنسيق) قبل وصولها إلى الإنتاج.