Django 提供了完整的国际化 (i18n) 和本地化 (l10n) 支持,使你可以轻松创建多语言网站。国际化是指设计应用程序使其能够适应不同语言和地区的过程,而本地化则是为特定语言和地区实际实现适配的过程。
| 术语 | 描述 | Django 实现 |
|---|---|---|
| i18n | 国际化 (Internationalization) | django.utils.translation |
| l10n | 本地化 (Localization) | django.conf.locale |
| gettext | GNU 翻译系统 | .po/.mo 文件格式 |
| Locale | 语言环境 | 语言代码 + 国家代码 |
要启用 Django 的国际化功能,需要在设置文件中进行相应配置。
# 启用国际化
USE_I18N = True
# 启用本地化
USE_L10N = True
# 启用时区支持
USE_TZ = True
# 默认语言代码
LANGUAGE_CODE = 'zh-hans' # 简体中文
# 支持的语言
LANGUAGES = [
('zh-hans', '简体中文'),
('en', 'English'),
('ja', '日本語'),
('ko', '한국어'),
('fr', 'Français'),
('de', 'Deutsch'),
('es', 'Español'),
]
# 或者使用 Django 预定义的语言列表
from django.utils.translation import gettext_lazy as _
LANGUAGES = [
('zh-hans', _('Simplified Chinese')),
('en', _('English')),
('ja', _('Japanese')),
]
# 翻译文件位置
LOCALE_PATHS = [
os.path.join(BASE_DIR, 'locale'),
]
# 中间件配置
MIDDLEWARE = [
# ...
'django.middleware.locale.LocaleMiddleware', # 位置重要!
# ...
]
# 时区设置
TIME_ZONE = 'Asia/Shanghai' # 中国时区
# 其他常用时区:
# TIME_ZONE = 'UTC' # 协调世界时
# TIME_ZONE = 'US/Eastern' # 美国东部
# TIME_ZONE = 'Europe/London' # 伦敦
from django.conf.urls.i18n import i18n_patterns
from django.urls import include, path
from django.views.i18n import set_language
urlpatterns = [
# 非国际化URL
path('admin/', admin.site.urls),
path('i18n/', include('django.conf.urls.i18n')),
]
# 带语言前缀的URL
urlpatterns += i18n_patterns(
path('', include('myapp.urls')),
path('blog/', include('blog.urls')),
prefix_default_language=False, # 默认语言不加前缀
)
# 或者手动配置
urlpatterns = [
path('i18n/setlang/', set_language, name='set_language'),
path('<language>/', include('myapp.urls')),
]
# 在模板中生成语言切换URL
# {% url 'set_language' %}
# 或者
# {% load i18n %}
# {% get_available_languages as LANGUAGES %}
# {% get_current_language as CURRENT_LANGUAGE %}
在 Django 中,使用 gettext 机制来标记和翻译字符串。
from django.shortcuts import render
from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy, ngettext
from django.http import HttpResponse
def welcome_view(request):
# 基本翻译
message = _("Welcome to our website!")
# 带变量的翻译
username = request.user.username
personalized_message = _("Hello, %(username)s!") % {'username': username}
# 复数形式
item_count = 5
items_message = ngettext(
"There is %(count)d item.",
"There are %(count)d items.",
item_count
) % {'count': item_count}
context = {
'message': message,
'personalized_message': personalized_message,
'items_message': items_message,
}
return render(request, 'welcome.html', context)
# 在模型中使用(使用 gettext_lazy)
from django.db import models
from django.utils.translation import gettext_lazy as _
class Product(models.Model):
name = models.CharField(_("product name"), max_length=100)
description = models.TextField(_("product description"))
class Meta:
verbose_name = _("product")
verbose_name_plural = _("products")
def __str__(self):
return self.name
# 在表单中使用
from django import forms
from django.utils.translation import gettext_lazy as _
class ContactForm(forms.Form):
name = forms.CharField(label=_("Your name"), max_length=100)
email = forms.EmailField(label=_("Email address"))
message = forms.CharField(
label=_("Message"),
widget=forms.Textarea,
help_text=_("Please enter your message here.")
)
# 在异常消息中使用
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
def validate_even(value):
if value % 2 != 0:
raise ValidationError(
_('%(value)s is not an even number'),
params={'value': value},
)
gettext() - 标准翻译函数,立即翻译
gettext_lazy() - 延迟翻译,用于模型等地方
ngettext() - 处理复数形式的翻译
pgettext() - 带上下文的翻译
npgettext() - 带上下文的复数翻译
from django.utils.translation import pgettext, pgettext_lazy
# 相同单词在不同上下文中的翻译
# "Book" 作为名词(书)和动词(预订)
noun_book = pgettext("noun", "Book")
verb_book = pgettext("verb", "Book")
# 在模型中使用
class Reservation(models.Model):
status = models.CharField(
choices=[
('booked', pgettext_lazy("reservation status", "Booked")),
('confirmed', pgettext_lazy("reservation status", "Confirmed")),
],
max_length=20
)
在 Django 模板中,使用 {% trans %} 和 {% blocktrans %} 标签来标记可翻译文本。
{% load i18n %}
<!-- 基本翻译 -->
<h1>{% trans "Welcome to our store" %}</h1>
<p>{% trans "We offer the best products at competitive prices." %}</p>
<!-- 带变量的翻译 -->
<p>
{% blocktrans with user_name=user.get_full_name %}
Hello, {{ user_name }}! Welcome back.
{% endblocktrans %}
</p>
<!-- 带过滤器的变量 -->
<p>
{% blocktrans with count=cart.item_count|default:0 %}
Your cart contains {{ count }} items.
{% endblocktrans %}
</p>
<!-- 复数形式 -->
<p>
{% blocktrans count counter=products|length %}
There is {{ counter }} product available.
{% plural %}
There are {{ counter }} products available.
{% endblocktrans %}
</p>
<!-- 在属性中翻译 -->
<input type="submit" value="{% trans 'Search' %}">
<img src="logo.png" alt="{% trans 'Company logo' %}">
<!-- 翻译带HTML的内容 -->
{% blocktrans %}
<p>Please read our <a href="/terms/">terms and conditions</a>.</p>
{% endblocktrans %}
<!-- 使用 trimmed 选项 -->
{% blocktrans trimmed %}
This is a long text that spans multiple lines
in the template but should be treated as a single
string for translation purposes.
{% endblocktrans %}
{% load i18n %}
<!-- 简单的语言切换表单 -->
<form action="{% url 'set_language' %}" method="post">
{% csrf_token %}
<input name="next" type="hidden" value="{{ request.path }}">
<select name="language" onchange="this.form.submit()">
{% get_current_language as CURRENT_LANGUAGE %}
{% get_available_languages as AVAILABLE_LANGUAGES %}
{% for lang_code, lang_name in AVAILABLE_LANGUAGES %}
<option value="{{ lang_code }}"
{% if lang_code == CURRENT_LANGUAGE %}selected{% endif %}>
{{ lang_name }}
</option>
{% endfor %}
</select>
</form>
<!-- 链接形式的语言切换器 -->
<div class="language-switcher">
{% get_current_language as CURRENT_LANGUAGE %}
{% get_available_languages as AVAILABLE_LANGUAGES %}
{% for lang_code, lang_name in AVAILABLE_LANGUAGES %}
{% if lang_code != CURRENT_LANGUAGE %}
<a href="{% url 'set_language' %}?language={{ lang_code }}&next={{ request.path }}">
{{ lang_name }}
</a>
{% endif %}
{% endfor %}
</div>
<!-- 带图标的语言切换器 -->
<div class="btn-group">
{% get_current_language as CURRENT_LANGUAGE %}
{% get_available_languages as LANGUAGES %}
{% for code, name in LANGUAGES %}
<a class="btn btn-sm {% if code == CURRENT_LANGUAGE %}btn-primary{% else %}btn-outline-primary{% endif %}"
href="/{{ code }}{{ request.get_full_path|slice:'3:' }}">
{{ name }}
</a>
{% endfor %}
</div>
<!-- 显示当前语言 -->
<div class="current-language">
{% trans "Current language" %}: {% get_language_info for CURRENT_LANGUAGE as lang %}{{ lang.name_local }}
</div>
Welcome to our website!
You have 5 items in your cart.
Search
欢迎访问我们的网站!
您的购物车中有 5 件商品。
搜索
Django 使用 gettext 工具链来管理翻译文件,包括提取、编译和更新翻译。
# 创建消息文件(首次)
django-admin makemessages -l zh_Hans
django-admin makemessages -l ja
django-admin makemessages -l fr
# 创建所有语言的消息文件
django-admin makemessages -a
# 忽略特定目录
django-admin makemessages -l zh_Hans --ignore=venv/* --ignore=static/*
# 使用不同的扩展名(用于JavaScript)
django-admin makemessages -l zh_Hans -d djangojs
# 编译消息文件
django-admin compilemessages
# 编译特定语言
django-admin compilemessages -l zh_Hans
# 更新现有消息文件
django-admin makemessages -a --keep-pot
# 检查翻译完整性
django-admin makemessages --all --check
# 创建翻译目录结构
mkdir -p locale/zh_Hans/LC_MESSAGES
mkdir -p locale/ja/LC_MESSAGES
myproject/
├── manage.py
├── myproject/
│ ├── settings.py
│ ├── urls.py
│ └── ...
├── myapp/
│ ├── __init__.py
│ ├── models.py
│ ├── views.py
│ └── ...
└── locale/ # 翻译文件目录
├── zh_Hans/
│ └── LC_MESSAGES/
│ ├── django.po # 文本翻译文件
│ └── django.mo # 编译后的二进制文件
├── ja/
│ �└── LC_MESSAGES/
│ ├── django.po
│ └── django.mo
└── fr/
└── LC_MESSAGES/
├── django.po
└── django.mo
#: myapp/views.py:23
msgid "Welcome to our website!"
msgstr "欢迎访问我们的网站!"
#: myapp/models.py:45
msgid "product"
msgstr "产品"
#: myapp/models.py:46
msgid "products"
msgstr "产品"
#: templates/base.html:15
#, python-format
msgid "Hello, %(username)s!"
msgstr "你好,%(username)s!"
#: templates/shop.html:32
#, python-format
msgid "Your cart contains %(count)d items."
msgstr "您的购物车中有 %(count)d 件商品。"
#: templates/products.html:12
msgid "Search"
msgstr "搜索"
# 复数形式
#: myapp/views.py:67
#, python-format
msgid "There is %(count)d item."
msgid_plural "There are %(count)d items."
msgstr[0] "有 %(count)d 件商品。"
msgstr[1] "有 %(count)d 件商品。"
# 带上下文的翻译
#: myapp/forms.py:89
msgctxt "button"
msgid "Submit"
msgstr "提交"
#: myapp/forms.py:90
msgctxt "form"
msgid "Submit"
msgstr "表单提交"
Django 自动根据用户的语言环境格式化日期、时间、数字和货币。
{% load l10n %}
<!-- 本地化日期 -->
<p>{{ some_date }}</p> <!-- 自动本地化 -->
<p>{{ some_date|localize }}</p> <!-- 显式本地化 -->
<p>{% localize on %}{{ some_date }}{% endlocalize %}</p>
<!-- 禁用本地化 -->
<p>{% localize off %}{{ some_date }}{% endlocalize %}</p>
<p>{{ some_date|unlocalize }}</p>
<!-- 数字格式化 -->
<p>{{ some_number }}</p> <!-- 自动本地化 -->
<p>{{ some_float|floatformat:2 }}</p> <!-- 保留两位小数 -->
<!-- 日期格式化过滤器 -->
<p>{{ some_date|date:"SHORT_DATE_FORMAT" }}</p>
<p>{{ some_date|date:"DATE_FORMAT" }}</p>
<p>{{ some_date|date:"DATETIME_FORMAT" }}</p>
<p>{{ some_date|date:"Y-m-d" }}</p> <!-- 固定格式 -->
<!-- 时间格式化 -->
<p>{{ some_time|time:"TIME_FORMAT" }}</p>
<p>{{ some_time|time:"H:i" }}</p> <!-- 固定格式 -->
<!-- 自然时间 -->
{% load humanize %}
<p>{{ some_date|naturalday }}</p> <!-- "今天", "昨天" -->
<p>{{ some_date|naturaltime }}</p> <!-- "2分钟前" -->
<!-- 数字人性化显示 -->
<p>{{ large_number|intcomma }}</p> <!-- 1,234,567 -->
<p>{{ file_size|filesizeformat }}</p> <!-- "1.5 MB" -->
Date: 12/25/2023
Time: 3:30 PM
Number: 1,234.56
Currency: $1,234.56
日期: 2023年12月25日
时间: 15:30
数字: 1,234.56
货币: ¥1,234.56
Datum: 25.12.2023
Uhrzeit: 15:30
Zahl: 1.234,56
Währung: 1.234,56 €
Date: 25/12/2023
Heure: 15:30
Nombre: 1 234,56
Monnaie: 1 234,56 €
from django.utils import formats, timezone
from django.utils.translation import get_language
def some_view(request):
# 获取当前语言
current_language = get_language()
# 本地化日期显示
now = timezone.now()
localized_date = formats.date_format(now, 'SHORT_DATE_FORMAT')
localized_datetime = formats.date_format(now, 'DATETIME_FORMAT')
# 本地化数字
from django.utils.formats import number_format
localized_number = number_format(1234.56, decimal_pos=2)
# 格式化货币(需要自定义或使用第三方库)
def format_currency(amount, currency_code):
# 简单的货币格式化
formats = {
'USD': '${:,.2f}',
'EUR': '€{:,.2f}',
'CNY': '¥{:,.2f}',
'JPY': '¥{:,.0f}',
}
format_str = formats.get(currency_code, '{:,.2f}')
return format_str.format(amount)
context = {
'localized_date': localized_date,
'localized_datetime': localized_datetime,
'localized_number': localized_number,
'usd_price': format_currency(1234.56, 'USD'),
'cny_price': format_currency(1234.56, 'CNY'),
}
return render(request, 'template.html', context)
Django 提供了强大的时区支持,可以正确处理不同时区的用户。
from django.utils import timezone
from datetime import datetime, timedelta
def timezone_examples(request):
# 获取当前时间(时区感知)
now_aware = timezone.now()
# 获取当前时间(时区非感知)
now_naive = datetime.now()
# 转换时区
utc_time = now_aware.astimezone(timezone.utc)
local_time = timezone.localtime(now_aware) # 转换为当前时区
# 创建时区感知日期时间
from pytz import timezone as pytz_timezone
beijing_tz = pytz_timezone('Asia/Shanghai')
custom_time = timezone.make_aware(
datetime(2023, 12, 25, 15, 30),
beijing_tz
)
# 时区运算
one_hour_later = now_aware + timedelta(hours=1)
one_day_ago = now_aware - timedelta(days=1)
# 在模板中使用的上下文
context = {
'now_aware': now_aware,
'local_time': local_time,
'utc_time': utc_time,
'custom_time': custom_time,
'one_hour_later': one_hour_later,
'one_day_ago': one_day_ago,
}
return render(request, 'timezone_example.html', context)
# 在模型中使用时区
class Event(models.Model):
title = models.CharField(max_length=200)
start_time = models.DateTimeField()
end_time = models.DateTimeField()
def is_active(self):
"""检查事件是否正在进行"""
now = timezone.now()
return self.start_time <= now <= self.end_time
def time_until_start(self):
"""距离开始还有多久"""
now = timezone.now()
if now < self.start_time:
return self.start_time - now
return None
def duration(self):
"""事件持续时间"""
return self.end_time - self.start_time
# 在表单中处理时区
from django import forms
from django.utils import timezone
class EventForm(forms.ModelForm):
class Meta:
model = Event
fields = ['title', 'start_time', 'end_time']
widgets = {
'start_time': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
'end_time': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
}
def clean(self):
cleaned_data = super().clean()
start_time = cleaned_data.get('start_time')
end_time = cleaned_data.get('end_time')
if start_time and end_time:
# 确保开始时间在当前时间之后
if start_time < timezone.now():
raise forms.ValidationError("开始时间必须在当前时间之后")
# 确保结束时间在开始时间之后
if end_time <= start_time:
raise forms.ValidationError("结束时间必须在开始时间之后")
return cleaned_data
{% load tz %}
<!-- 设置时区 -->
{% timezone "Europe/Paris" %}
<p>巴黎时间: {{ event.start_time }}</p>
{% endtimezone %}
{% timezone "Asia/Shanghai" %}
<p>北京时间: {{ event.start_time }}</p>
{% endtimezone %}
<!-- 使用用户时区 -->
{% timezone user.timezone %}
<p>您的时区: {{ event.start_time }}</p>
{% endtimezone %}
<!-- UTC 时间 -->
{% timezone "UTC" %}
<p>UTC 时间: {{ event.start_time }}</p>
{% endtimezone %}
<!-- 时区转换 -->
<p>本地时间: {% localtime on %}{{ event.start_time }}{% endlocaltime %}</p>
<p>UTC 时间: {% localtime off %}{{ event.start_time }}{% endlocaltime %}</p>
<!-- 时区信息 -->
{% get_current_timezone as TIME_ZONE %}
<p>当前时区: {{ TIME_ZONE }}</p>
<!-- 相对时间 -->
{% load humanize %}
<p>开始时间: {{ event.start_time|naturaltime }}</p>
<p>{{ event.start_time|timesince }} 之前</p>
<p>{{ event.end_time|timeuntil }} 之后</p>
<!-- 事件状态 -->
{% if event.is_active %}
<span class="badge bg-success">进行中</span>
{% elif event.start_time > now %}
<span class="badge bg-warning">
还有 {{ event.time_until_start|timeuntil }}
</span>
{% else %}
<span class="badge bg-secondary">已结束</span>
{% endif %}
<!-- 持续时间 -->
<p>持续时间: {{ event.duration }}</p>
# 时区设置
USE_TZ = True
TIME_ZONE = 'Asia/Shanghai' # 默认时区
# 常用时区:
# TIME_ZONE = 'UTC'
# TIME_ZONE = 'US/Eastern'
# TIME_ZONE = 'Europe/London'
# TIME_ZONE = 'Asia/Tokyo'
# 在用户模型中存储时区偏好
class User(AbstractUser):
timezone = models.CharField(
max_length=50,
default='Asia/Shanghai',
choices=[
('UTC', 'UTC'),
('Asia/Shanghai', 'Asia/Shanghai'),
('Asia/Tokyo', 'Asia/Tokyo'),
('Europe/London', 'Europe/London'),
('US/Eastern', 'US/Eastern'),
]
)
# 中间件设置用户时区
class TimezoneMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
if request.user.is_authenticated:
timezone.activate(pytz.timezone(request.user.timezone))
else:
timezone.deactivate()
return self.get_response(request)
# 不好的做法
msg = _("Welcome") + ", " + username
# 好的做法
msg = _("Welcome, %(username)s") % {'username': username}
gettext_lazy 避免不必要的翻译trimmed 选项减少翻译文件大小
# 使用缓存
from django.core.cache import cache
def get_cached_translation(key):
cache_key = f'translation_{get_language()}_{key}'
return cache.get_or_set(
cache_key,
lambda: gettext(key),
3600 # 缓存1小时
)
# 项目结构
project/
├── locale/
│ ├── zh_Hans/
│ │ └── LC_MESSAGES/
│ │ ├── django.po
│ │ └── app1.po
│ └── ja/
│ └── LC_MESSAGES/
│ ├── django.po
│ └── app1.po
└── apps/
├── app1/
└── app2/
# 翻译测试
from django.test import TestCase
from django.utils.translation import activate, deactivate
class TranslationTests(TestCase):
def test_chinese_translation(self):
activate('zh-hans')
response = self.client.get('/')
self.assertContains(response, "欢迎")
deactivate()
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 翻译不显示 | 未编译消息文件或中间件顺序错误 | 运行 compilemessages,检查中间件顺序 |
| 时区错误 | USE_TZ 设置不正确或时区未激活 | 确保 USE_TZ=True,使用时区中间件 |
| 复数形式不正确 | 语言复数规则配置错误 | 检查 .po 文件中的复数形式定义 |
| 格式本地化不工作 | USE_L10N 未启用或格式配置错误 | 启用 USE_L10N,检查格式设置 |
# 调试信息
from django.utils.translation import get_language
from django.utils import timezone
print(f"当前语言: {get_language()}")
print(f"当前时区: {timezone.get_current_timezone()}")
print(f"USE_I18N: {settings.USE_I18N}")
print(f"USE_L10N: {settings.USE_L10N}")
print(f"USE_TZ: {settings.USE_TZ}")