Переводы полей моделей Django + Vue

—

от автора

Всем привет! Это вторая статья цикла о разработке приложений в нашей компании. В первой статье я рассказал про общую архитектуру некоторых наших проектов. Сегодня же опишу наши варианты решения часто встречающихся задач в рамках Django + Vue приложения.

Переводы в моделях

Скорее всего, все уже умеют работать с интернационализацией в Django приложениях: достаточно использовать методы gettext, gettext_lazy из стандартного модуля Django, собрать все переводы в .po файл, используя стандартную команду управления, написать переводы и скомпилировать .mo файл. Но что делать, когда бизнес требует переводить некоторые сущности, которые хранятся в БД?

Ради примера представим, что у нас есть небольшой проект для рисования. Фигуры, которые можно рисовать на холсте, ограничены набором объектов Shape (фигура):

class Shape(models.Model):   name = models.CharField(max_length=15)

Объекты этой модели добавляются по мере внедрения функционала рисования, и их количество не безгранично. Если вы уже ознакомились с нашей архитектурой то знаете, что такие данные мы отправляем на клиентское приложение по WS сразу после подключения пользователя. Таким образом клиент получает список вида:

[   {     id: 1,     name: "Круг",   },   {     id: 2,     name: "Квадрат",   },   ... ]

Всё было хорошо, пока не появилась необходимость добавить механизм изменения языка на клиенте. Vue-i18n работает хорошо, но он не может переводить динамические данные, т.е. данные полученные из БД.

Теоретически может

В теории мы можем добавить в модель поле slug для хранения уникального значения с названием фигуры (circle, square, …), по которому будем искать строку перевода с помощью vue-i18n: $t(main.shapes.${shape.slug}). Но у такого подхода есть минус — при добавлении каждой новой фигуры в БД необходимо обновлять словари перевода vue-i18n. Но зачем тогда хранить такие данные в БД, а не в обычном перечислении?

Для решения это задачи написано множество библиотек, одной из которых мы и воспользуемся — django-modeltranslation. Инструкции по подключению и настройке данного пакета в этой статье не будет, т.к. у него есть документация и, скорее всего, такие статьи уже есть на хабре.

Итак, регистрация модели как переводимой:

from modeltranslation.translator import TranslationOptions, register   @register(Shape) class ShapeTranslationOptions(TranslationOptions):   fields = ("name", )

После применения миграции, у таблицы, с которой связана модель, появятся дополнительные поля: name_<language_code> для каждого языка, указанного в настройках. После этого можно добавить названия для наших фигур на всех необходимых языках, перейдя в панель администратора Django.

На этом всё?

Теперь, клиентское приложение получит данные на том языке, на котором отправит запрос на получение этих данных. Таким образом, если у пользователя в браузере включен русский язык, он получит данные на русском языке. Если язык английский (например), он получит данные на своем языке (если наше приложение поддерживает английский):

[   {     id: 1,     name: "Circle",   },   {     id: 2,     name: "Square",   },   ... ]

Этого достаточно? Пользователи довольны? По большей части да. Недоволен только наш полиглот-тестировщик, который меняет язык каждые 3 секунды для тестирования интерфейса (спасибо ему за это =)).

Клиентское приложение не хранит названия фигур на всех языках, и для его отображения на нужном языке требуется обновить страницу или выполнить запрос на получение объектов заново. Ещё хуже дела обстоят в ситуации, когда кто-то добавляет или изменяет фигуру, после чего в сокет приходит сообщение:

 {    event: "shape_added",     data: {       id: 3,       name: "Треугольник",    }  }

Отправку сообщения инициировал пользователь, у которого активирован русский язык, поэтому и сообщение содержит информацию на русском языке.

Будем исправлять

Сериализатор, используемый для отправки списка объектов в клиентское приложение, выглядит следующим образом:

class ReadShapeSerializer(serializers.ModelSerializer):     class Meta:         model = Shape         fields = ("id", "name")

В момент сериализации, методы DRF запрашивают у каждого объекта атрибут name, а пакет modeltranslation сам понимает текущий активированный язык и отдает значение в зависимости от языка: либо name_ru, либо name_en. Мы написали небольшую примесь для сериализатора:

from modeltranslation.translator import translator from modeltranslation.utils import get_translation_fields   class TranslationFieldsMixin:     def get_fields(self):         opts = self.Meta         orig_fields = opts.fields         new_fields = []         trans_opts = translator.get_options_for_model(opts.model)          for field_name in orig_fields:             if field_name in trans_opts.fields:                 new_fields.extend(get_translation_fields(field_name))             else:                 new_fields.append(field_name)         self.Meta.fields = tuple(new_fields)         return super().get_fields()

Он меняет Meta.fields для сериализатора модели, убирает оттуда переводимые поля и добавляет поля с переводами:

class ReadShapeSerializer(TranslationFieldsMixin, serializers.ModelSerializer):     class Meta:         model = Shape         fields = ("id", "name_ru", "name_en")

Таким образом, просто добавив примесь в сериализатор, каждый объект будет содержать все поля с переводами:

// Получение объектов по API [   {     id: 1,     name_ru: "Круг",     name_en: "Circle",   },   {     id: 2,     name_ru: "Квадрат",     name_en: "Square",   },   ... ]  // Сокет событие {   event: "shape_added",    data: {      id: 3,      name_ru: "Треугольник",     name_en: "Triangle",   } }

Клиентское приложение

Сервер отправляет всё, а frontend-разработчики пусть страдают и пишут в каждом компоненте что-то вроде:

<div v-for="shape of shapes" :key="shape.id">   <span>{{ shape[`name${i18n.currentLanguage}`] }}</span> </div>

подумали мы и не стали так делать. Если вы смотрели демо-проект, то видели, что в клиентском приложении мы не работаем с объектами в сыром виде, для каждого объекта мы создаем экземпляр его класса и работаем с этим экземпляром:

class Shape {   constructor(data) {     this.id = data.id     this.name = data.name   } }

Для реализации возможности работы с объектами этого класса как и прежде, мы написали ещё один класс, который отвечает за получения переводимого атрибута на нужном языке:

export default class TranslatableModel {   /**    * Конструктор класса получает названия переводимых полей,     * добавляет геттер получения оригинального поля "названиеПоля"     * и метод установки этого поля.    * **/   constructor(data) {     const translationFields = this.getTranslationFields()      for (const field of translationFields) {       this.defineProperty(field)     }   }    /**    * Создает на экземпляре класса геттер и сеттер для работы с полем fieldName.    * При обращении instance.field пытается найти instance.fieldRu     * (если ru - это текущий язык). Если значение поля для текущего     * языка не указано, перебирает запасные локализации (i18n.fallbackLocale),    * для каждой из них пытается получить значение instance[field<locale>]     * пока не найдет заполненное поле.    *    * Метод сеттер принимает все данные об объекте и берет     * из них значения обрабатываемого поля со всеми вариантами перевода.    * **/   defineProperty(fieldName) {     Object.defineProperty(this, fieldName, {       get() {         for (const language of this.getUserLanguages()) {           const transFieldName = `${fieldName}${language}`           const value = this[transFieldName]           if (!isEmpty(value)) {             return value           }         }         return undefined       },       set(data) {         const languages = this.getLanguages()         for (const language of languages) {           const transFieldName = `${fieldName}${language}`           this[transFieldName] = data[transFieldName]         }       },     })   }   /**    * Пустой метод, сделанный только для того, чтобы не было вопросов,     * почему в поле записывается весь объект data:    *    * class SomeClass extends TranslatableModel {    *   constructor(data) {    *     super(data)    *     this.name = data    *   }    *    *   getTranslationFields() {    *     return ['name']    *   }    * }    *    * На самом деле в этом примере при вызове this.name = data     * вызывается метод setter для поля name, который достает    * из объекта data поля nameRu, nameEn, ... и устанавливает их как     * атрибуты объекта.    *    * **/   getField(data) {     return data   }    /**    * Метод получения списка полей, подлежащих переводу.    * **/   getTranslationFields() {     return []   }      /**    * Возвращает список поддерживаемых языков.    * **/   getLanguages() {     const languages = Object.keys(i18n.global.messages)     return languages.map(capitalize)   }      /**    * Метод генератор, по очереди возвращает следующий язык, который     * необходимо искать у экземпляра при доступе к атрибуту.    * **/   *getUserLanguages() {     yield capitalize(i18n.global.locale)     const fallbackLanguages = this.getFallbackLanguages()     for (const language of fallbackLanguages) {       yield language     }   }      /**    * Метод возвращает список запасных локализаций. Т.к. i18n.fallbackLocale     * может быть разного типа (строка, список строк, объект), приводит     * всё к списку.    * **/   getFallbackLanguages() {     const userLocale = capitalize(i18n.global.locale)     const fallbackConfig = i18n.global.fallbackLocale     let locales      if (isArray(fallbackConfig)) {       locales = fallbackConfig     } else if (isObject(fallbackConfig)) {       locales = Object.keys(fallbackConfig)     } else {       locales = [fallbackConfig]     }      return locales.filter((lang) => lang !== userLocale).map(capitalize)   } } 

Осталось унаследовать наш класс фигуры от этого базового класса, чтобы всё заработало:

class Shape extends TranslatableModel {   constructor(data) {     super(data)     this.id = data.id     this.name = this.getField(data)   }    getTranslationFields() {     return ['name']   } }

После всех преобразований, как на сервере, так и на клиенте, мы можем писать Vue компоненты как обычно, не обращая внимания на текущий активированный язык:

<div v-for="shape of shapes" :key="shape.id">   <span>{{ shape.name }}</span> </div>

Мы подготовили небольшой демонстрационный проект на codesandbox, где можно посмотреть, как это работает, и поиграть с получившимся результатом.

Спасибо за внимание!


ссылка на оригинал статьи https://habr.com/ru/articles/760520/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *