Обзор ситуации

инфографика о проблеме gettext_lazy и ключах JSON

Когда вы разрабатываете на Django, вы, возможно, часто используете gettext_lazy для поддержки многоязычности. Однако в какой-то момент gettext_lazy, который вы привыкли использовать, может начать вызывать ошибку при создании ответа JSON.

В этой статье я подробно объясню почему gettext_lazy вызывает проблемы при сериализации JSON и как это можно исправить.

Нормальная работа vs ошибка: сравнение ситуаций

Кейс Случай с нормальной работой Случай с ошибкой
LANGUAGE_MAP.get(...) возвращаемое значение значение словаря ключ словаря
Сериализуемость возможно (без проблем) невозможно (ключ __proxy__ не может быть преобразован в JSON)

Суть проблемы: почему возникает такая разница?

Объект, возвращаемый gettext_lazy, имеет тип __proxy__. Этот объект выглядит как строка, но на самом деле не является настоящей строкой.

Django's JsonResponse или json.dumps() Python следуют следующим правилам:

# ✅ Возможно: можно использовать lazy объект в качестве value (автоматическое преобразование в str)
json.dumps({'language': _('English')})

# ❌ Не удалось: нельзя использовать lazy объект в качестве key
json.dumps({_('English'): 'language'})

То есть, для использования в качестве ключа словаря значение должно быть настоящей строкой. Объект __proxy__ не автоматически преобразуется в строку, когда используется как ключ, что и вызывает ошибку.

Почему нет проблем с использованием в качестве value, а с ключом проблемы возникают?

Объект gettext_lazy не вызывает проблем, когда используется в качестве value словаря. Это связано с тем, что Django или Python автоматически преобразует value в строку в процессе сериализации JSON.

Однако в случае использования в качестве key стандарт JSON требует, чтобы ключ был обязательно строкой. В этом случае объект gettext_lazy не преобразуется автоматически, что и приводит к ошибке сериализации.

В заключение:

  • При использовании в качестве value происходит внутреннее преобразование в str, поэтому проблем нет.
  • При использовании в качестве key автоматическое преобразование не происходит, что приводит к ошибкам.

Способы решения

Способ 1: Переключение на gettext (без использования Lazy)

Это самый надежный способ. Используйте значение, которое уже преобразовано в строку, без использования lazy.

from django.utils.translation import gettext as _  # не lazy!

LANGUAGE_MAP = {
    "en": _("English"),
    "ko": _("Korean"),
    "ja": _("Japanese"),
}
  • Плюсы: дополнительная обработка не требуется
  • Минусы: перевод определяется на момент первого импорта, поэтому может отличаться от некоторых динамических требований к переводу

Способ 2: Принудительное применение str() сразу перед сериализацией JSON

Если вы хотите продолжать использовать lazy объекты, преобразуйте ключ в str() перед вставкой в JSON.

lang = str(LANGUAGE_MAP.get(lang_code, lang_code.upper()))
  • Плюсы: почти не нужно менять существующий код
  • Минусы: нужно помнить о каждом преобразовании str

Дополнительно: Обработка перевода на стороне клиента

Можно также обойтись без выполнения многоязычного перевода на сервере и обрабатывать его на фронтенде.

  • Сервер отправляет только код языка
  • Клиентский JavaScript управляет таблицей переводов.

Этот метод можно выбрать в зависимости от проекта.

Итак, в заключение

  • gettext_lazy никогда не следует использовать в качестве ключа словаря
  • Обязательно преобразуйте в строку перед сериализацией JSON
  • Самый безопасный способ - сразу преобразовать в строку с помощью gettext
  • Если необходимо сохранить существующий код, не забудьте про str(...)

Комментарий Джесси

Эта проблема возникает из-за тонкой границы между многоязычной обработкой в Django и обработкой ответов JSON.

В будущем вместо "почему это не работает, хотя раньше работало?" подумайте, что "раньше вам просто повезло" и проверьте код.

Момент, когда уровень разработчика действительно повышается, - это когда вы начинаете понимать такие 'тонкости'. Поняв этот принцип, вы сможете значительно укрепить как многоязычную обработку Django, так и ответы JSON!