iT邦幫忙

2022 iThome 鐵人賽

DAY 21
0

Day 21 國際化租屋,Django 多租戶多語系

即便是在同一個國家也可能會有不同的文化,每個租戶的語言習慣也可能會不相同,一個詞在不同領域也可能會有不一樣的解釋。今天要來介紹在 Django 多租戶架構下的多語系,除了能區分不同租戶的翻譯對照表之外,還能在管理介面直接修改,我們馬上開始吧!

調整多語系目錄結構

由於 Django 的多語系是讀取編譯後的檔案,我們將要建立一個新多語系目錄,並且在其中使用租戶的 schema 名稱建立各自的多語系目錄。

docker exec --workdir /opt/app/web example_tenant_web \
mkdir tenant_locale

docker exec --workdir /opt/app/web example_tenant_web \
cp -pr locale tenant_locale/example01

docker exec --workdir /opt/app/web example_tenant_web \
cp -pr locale tenant_locale/example02

新目錄結構如下


.
├── core
├── customers
├── locale
│   └── zh_Hant
│       └── LC_MESSAGES
│           └── django.po
├── main
├── manage.py
├── media
├── products
└── tenant_locale
    ├── example01
    │   └── zh_Hant
    │       └── LC_MESSAGES
    │           ├── django.mo
    │           └── django.po
    └── example02
        └── zh_Hant
            └── LC_MESSAGES
                ├── django.mo
                └── django.po

建立 翻譯對照表

在 core 新增模型 LocaleSetting 與 TranslateSetting 用來定義多語系的翻譯對照表

為了避免在同一個對照表建立重複的翻譯詞,使用 clean 函數進行重複判斷。

# core/models.py

from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
from core import helpers

# ...

class LocaleSetting(models.Model):
    id = models.CharField('語言代碼', max_length=10, primary_key=True)
    language = models.CharField('系統語言', max_length=50)

    class Meta:
        verbose_name = '翻譯對照表'
        verbose_name_plural = '翻譯對照表'

    def __str__(self):
        return "%s" % (self.id)

class TranslateSetting(models.Model):
    locale = models.ForeignKey('core.LocaleSetting', on_delete=models.CASCADE, related_name='translatesetting_set')
    raw_string = models.CharField('原始文字', max_length=255)
    translated_string = models.CharField('翻譯後文字', default='', null=True, max_length=255, blank=True)

    def clean(self):
        if trans_obj := TranslateSetting.objects.filter(raw_string=self.raw_string, locale=self.locale):
            if self.id != trans_obj.first().id:
                raise ValidationError(_('Raw string already exists!'))

    class Meta:
        verbose_name = '翻譯文字'
        verbose_name_plural = '翻譯文字'
        ordering = ['raw_string']

    def __str__(self):
        return ""

建立資料庫遷移檔案

docker exec --workdir /opt/app/web example_tenant_web \
    python3.10 manage.py makemigrations
...

Migrations for 'core':
  core/migrations/0005_localesetting_translatesetting.py
    - Create model LocaleSetting
    - Create model TranslateSetting

執行資料庫遷移

docker exec --workdir /opt/app/web example_tenant_web \
    python3.10 manage.py migrate

... 

=== Starting migration
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, customers, products, sessions, sites
Running migrations:
  Applying core.0005_localesetting_translatesetting...
 OK
=== Starting migration
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, customers, products, sessions, sites
Running migrations:
  Applying core.0005_localesetting_translatesetting...
 OK
=== Starting migration
Operations to perform:
  Apply all migrations: admin, auth, contenttypes, core, customers, products, sessions, sites
Running migrations:
  Applying core.0005_localesetting_translatesetting...
 OK

新增 翻譯對照表管理介面

# core/admin.py

import os
from django.conf import settings
from django.contrib import admin
from django.db import connection
from django.utils.translation import to_locale

# Register your models here.
from core.models import Setting, LocaleSetting, TranslateSetting

class SettingAdmin(admin.ModelAdmin):
    # ...

class TranslateSettingInline(admin.TabularInline):
    model = TranslateSetting
    fields = ('raw_string', 'translated_string')

class LocaleSettingAdmin(admin.ModelAdmin):
    search_fields = ['id', 'language']
    fields = ('id', 'language')
    list_display = ('id', 'language')
    inlines = [TranslateSettingInline, ]

admin.site.register(Setting, SettingAdmin) 
admin.site.register(LocaleSetting, LocaleSettingAdmin)

https://ithelp.ithome.com.tw/upload/images/20221003/20151656ZlHX2t9cwi.png

自動編譯語言檔

save_related 在儲存頁面時觸發,可以取得 inline 表送出後的中的所有值,我們將根據欄位內容重新寫一個 django.po 檔。
to_locale 為 Django 自帶的函數,可以將 zh-hant 轉換為 zh_Hant 格式,再加上當前的 schema 名稱來尋找多語系目錄的路徑後進行檔案覆蓋,而因為 django 的編譯指令需要指定語言代碼,這裡使用 gettext 的指令 msgfmt 來進行語言檔編譯。

# core/admin.py

import os
from django.conf import settings
from django.contrib import admin
from django.db import connection
from django.utils.translation import to_locale

class SettingAdmin(admin.ModelAdmin):
    # ...

class TranslateSettingInline(admin.TabularInline):
     # ...

class LocaleSettingAdmin(admin.ModelAdmin):
    search_fields = ['id', 'language']
    fields = ('id', 'language')
    list_display = ('id', 'language')
    inlines = [TranslateSettingInline, ]

    def save_related(self, request, form, formsets, change):
        super(LocaleSettingAdmin, self).save_related(request, form, formsets, change)
        locale_id = (to_locale(form.instance.id))
        locale_path = os.path.join(settings.BASE_DIR, f'tenant_locale/{connection.schema_name}/{locale_id}/LC_MESSAGES/')
        message_file = 'msgid ""\n' + \
        'msgstr ""\n' + \
        '"Project-Id-Version: PACKAGE VERSION\\n"\n' + \
        '"Report-Msgid-Bugs-To: \\n"\n' + \
        '"POT-Creation-Date: 2022-10-01 15:59+0800\\n"\n' + \
        '"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\\n"\n' + \
        '"Last-Translator: FULL NAME <EMAIL@ADDRESS>\\n"\n' + \
        '"Language-Team: LANGUAGE <LL@li.org>\\n"\n' + \
        '"Language: \\n"\n' + \
        '"MIME-Version: 1.0\\n"\n' + \
        '"Content-Type: text/plain; charset=UTF-8\\n"\n' + \
        '"Content-Transfer-Encoding: 8bit\\n"\n' + \
        '"Plural-Forms: nplurals=1; plural=0;\\n"\n\n'
        for trans_obj in form.instance.translatesetting_set.all():
            message_file += f'msgid "{trans_obj.raw_string}"\n'
            message_file += f'msgstr "{trans_obj.translated_string}"\n\n'
        with open(f"{locale_path}/django.po" , "w") as f:
            f.write(message_file)
        os.system(f"cd {locale_path} && msgfmt django.po -o django.mo")

讀取多語系

Django 本身會對語言檔進行快取,而這會造成在不同租戶無法正常取得各自的多語系檔案,我們需要在 middleware 將語言檔快取清除後重新取得多語系設定。

在 core 建立一個 middleware.py 並定義一個 TranslationsMiddleware 類

# core/middleware.py 

class TranslationsMiddleware(MiddlewareMixin):
    def process_request(self, request):
        from django.utils import translation
        from django.utils.translation import trans_real, get_language
        from django.conf import settings
        import gettext
        if settings.USE_I18N:
            try:
                # Reset gettext.GNUTranslation cache.
                gettext._translations = {}

                # Reset Django by-language translation cache.
                trans_real._translations = {}

                # Delete Django current language translation cache.
                trans_real._default = None

                settings.LOCALE_PATHS = (os.path.join(settings.BASE_DIR, f'tenant_locale/{connection.schema_name}'), )
                translation.activate(get_language())
            except AttributeError:
                pass

在 settings.py 的 MIDDLEWARE 加入 TranslationsMiddleware。

# main/settings.py

MIDDLEWARE = (
     ...
     'core.middleware.TranslationsMiddleware',
)

到這裡,多租戶多語系的設定就完成了!

頁面展示

租戶 examle01

對照表:
Home page 對照 首頁
List of products 對照 商品列表
Product detail 對照 商品詳情

https://ithelp.ithome.com.tw/upload/images/20221003/20151656Yg5j0xjCMN.png

展示:

https://ithelp.ithome.com.tw/upload/images/20221003/20151656q7XevoSvIY.png

租戶 example02

對照表:
Home page 對照 主頁
List of products 對照 商品清單
Product detail 對照 商品詳細

https://ithelp.ithome.com.tw/upload/images/20221003/20151656kM1MFhJZhC.png

展示:

https://ithelp.ithome.com.tw/upload/images/20221003/20151656xYVmfkhXq3.png

為了定期通知用戶上架的新品,我們接下來要講解如何使用 Django 寄信,下一回『 郵差來送信,使用 Django 寄送郵件 』。


上一篇
Day 20 搭上國際航空,切換語系
下一篇
Day 22 郵差來送信,使用 Django 寄送郵件
系列文
全能住宅改造王,Django 多租戶架構的應用 —— 實作一個電商網站30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言