博客界面已有中文文案,但语言仍写死在模板里。我把界面文案交给 Django 翻译机制,加入 LocaleMiddleware、语言目录和切换表单。读者选择 English 后,导航、列表分页和空状态会一起变化。
请求先经过 LocaleMiddleware,它会读取 django_language Cookie 或 Accept-Language,解析出当前语言。模板再从翻译目录里取对应文案。
先声明支持的语言
settings.py 里加入语言列表和翻译目录。
1
2
3
4
5
6
7
8
9
10
from django.utils.translation import gettext_lazy as _
LANGUAGE_CODE = "zh-hans"
LANGUAGES = [
( "zh-hans" , "简体中文" ),
( "en" , "English" ),
]
LOCALE_PATHS = [ BASE_DIR / "locale" ]
LANGUAGE_CODE 是默认语言。项目源码里主要写中文,所以默认值保持 zh-hans。LANGUAGES 只列真正维护过的语言,不要为了好看加入一串没有翻译文件的选项。
LocaleMiddleware 的位置很关键。
1
2
3
4
5
6
7
8
9
10
MIDDLEWARE = [
"django.middleware.security.SecurityMiddleware" ,
"django.contrib.sessions.middleware.SessionMiddleware" ,
"django.middleware.locale.LocaleMiddleware" ,
"django.middleware.common.CommonMiddleware" ,
"django.middleware.csrf.CsrfViewMiddleware" ,
"django.contrib.auth.middleware.AuthenticationMiddleware" ,
"django.contrib.messages.middleware.MessageMiddleware" ,
"django.middleware.clickjacking.XFrameOptionsMiddleware" ,
]
它要放在 SessionMiddleware 之后,因为可能读取会话和 Cookie;也要放在 CommonMiddleware 之前,让后续请求处理已经知道当前语言。
模板文案改用 translate
先在模板加载 i18n。
固定文案用 translate。
1
2
3
< h1 > {% translate "文章列表" %}</ h1 >
< p > {% translate "还没有已发布文章。" %}</ p >
< a href = "?page=2" > {% translate "下一页" %}</ a >
带变量的文案用 blocktranslate。
1
2
3
{% blocktranslate with count=post.views %}
{{ count }} 次阅读
{% endblocktranslate %}
分页信息也可以一次传入两个变量。
1
2
3
{% blocktranslate with number=page_obj.number total=page_obj.paginator.num_pages %}
第 {{ number }} / {{ total }} 页
{% endblocktranslate %}
<html> 的语言属性也要动态化。
1
2
{% get_current_language as LANGUAGE_CODE %}
< html lang = "{{ LANGUAGE_CODE }}" >
这个属性会影响浏览器朗读、字体选择和拼写检查。它不是 SEO 装饰。
Python 里的提示也要翻译
后台操作返回的消息不能留在模板里翻译,因为它们由视图生成。
1
2
3
4
from django.utils.translation import gettext
messages . success ( request , gettext ( "评论已提交。" ))
messages . error ( request , gettext ( "请检查昵称、邮箱和评论内容。" ))
分类、标签和归档页标题同样处理。
1
"page_title" : gettext ( "分类: %(category)s " ) % { "category" : category }
%(category)s 是命名占位符。翻译人员可以调整语序,代码仍然通过字典填值。
语言切换表单
Django 提供 set_language 视图。根路由加入下面这段。
1
2
3
4
5
6
7
8
from django.conf.urls.i18n import i18n_patterns
from django.urls import include , path
urlpatterns = [
path ( "i18n/" , include ( "django.conf.urls.i18n" )),
path ( "admin/" , admin . site . urls ),
path ( "" , include ( "blog.urls" )),
]
导航里的切换表单提交当前语言和返回地址。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
< form method = "post" action = "{% url 'set_language' %}" >
{% csrf_token %}
< input name = "next" type = "hidden" value = "{{ request.path }}" >
< select name = "language" >
{% get_current_language as CURRENT_LANGUAGE %}
{% get_available_languages as LANGUAGES %}
{% get_language_info_list for LANGUAGES as languages %}
{% for language in languages %}
< option value = "{{ language.code }}"
{% if language . code = = CURRENT_LANGUAGE %} selected {% endif %} >
{{ language.name_local }}
</ option >
{% endfor %}
</ select >
< button type = "submit" > {% translate "切换" %}</ button >
</ form >
next 让用户切换语言后停留当前页。对列表页来说,这个体验比统一跳回首页好。
建立英文翻译目录
本项目的源码文案以中文作为 msgid,英文目录把它翻译成 English。source/locale/en/LC_MESSAGES/django.po 里的几条如下。
1
2
3
4
5
6
7
8
msgid "文章列表"
msgstr "Article list"
msgid "最新文章"
msgstr "Latest articles"
msgid "还没有已发布文章。"
msgstr "No published articles yet."
编译成 .mo 后,Django 才会加载。
1
msgfmt -o locale/en/LC_MESSAGES/django.mo locale/en/LC_MESSAGES/django.po
我在开发环境用 polib 编译了这个文件。生产部署只 .mo 也可以,但仓库里保留 .po,以后才能继续维护翻译。
测试语言切换
三条测试分别确认默认语言、英文翻译和切换表单。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
from django.test import TestCase
from django.urls import reverse
class I18NTests ( TestCase ):
def test_default_language_is_chinese ( self ):
response = self . client . get ( reverse ( "blog:list" ))
self . assertContains ( response , "文章列表" )
def test_user_can_switch_to_english ( self ):
response = self . client . post (
reverse ( "set_language" ),
{ "language" : "en" , "next" : reverse ( "blog:list" )},
follow = True ,
)
self . assertContains ( response , "Article list" )
self . assertContains ( response , "Latest articles" )
def test_language_selector_is_present ( self ):
response = self . client . get ( reverse ( "blog:list" ))
self . assertContains ( response , reverse ( "set_language" ))
self . assertContains ( response , 'value="en"' )
切换测试不只检查 Cookie,也不直接调用 translation.activate。它让请求真正穿过 set_language、LocaleMiddleware 和模板渲染。
运行测试。
1
uv run manage.py test blog.tests.test_i18n
1
2
3
Ran 3 tests in 0.026s
OK
选择 English 后,页面标题和导航会出现 Article list、Latest articles 等文案。
界面文案先处理到这里。文章标题和正文属于作者创作的内容,不该被机器翻译覆盖;分类和标签名称也属于数据。真要支持多语言内容时,再为这些表设计翻译表或 JSON 字段。
源码
GitHub 地址:https://github.com/yidao620c/simpleblog