即便是在同一個國家也可能會有不同的文化,每個租戶的語言習慣也可能會不相同,一個詞在不同領域也可能會有不一樣的解釋。今天要來介紹在 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)
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 對照 商品詳情
展示:
租戶 example02
對照表:
Home page 對照 主頁
List of products 對照 商品清單
Product detail 對照 商品詳細
展示:
為了定期通知用戶上架的新品,我們接下來要講解如何使用 Django 寄信,下一回『 郵差來送信,使用 Django 寄送郵件 』。