Django 国际化和本地化

Django 国际化简介

Django 提供了完整的国际化 (i18n) 和本地化 (l10n) 支持,使你可以轻松创建多语言网站。国际化是指设计应用程序使其能够适应不同语言和地区的过程,而本地化则是为特定语言和地区实际实现适配的过程。

1
标记字符串
在代码中标记可翻译文本
2
提取翻译
生成翻译文件
3
翻译内容
编辑翻译文件
4
编译消息
生成二进制翻译文件
5
配置语言
设置语言偏好
6
本地化格式
适配日期、数字格式
国际化组件
  • 翻译系统 - 文本内容翻译
  • 本地化格式 - 日期、时间、数字
  • 时区支持 - 时间显示和处理
  • 格式本地化 - 货币、数字格式
  • 复数形式 - 处理不同语言的复数规则
核心概念
术语 描述 Django 实现
i18n 国际化 (Internationalization) django.utils.translation
l10n 本地化 (Localization) django.conf.locale
gettext GNU 翻译系统 .po/.mo 文件格式
Locale 语言环境 语言代码 + 国家代码

基本配置

要启用 Django 的国际化功能,需要在设置文件中进行相应配置。

设置文件配置
settings.py
# 启用国际化
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' # 伦敦
URL 配置
urls.py
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 %}
LocaleMiddleware 位置:
  • 应该在 SessionMiddleware 之后
  • 应该在 CommonMiddleware 之前
  • 确保在 CacheMiddleware 之后(如果使用)
支持的语言示例
中文 (zh-hans)
English (en)
日本語 (ja)
Polski (pl)
Italiano (it)

翻译字符串

在 Django 中,使用 gettext 机制来标记和翻译字符串。

Python 代码中的翻译

views.py
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() - 带上下文的复数翻译

复数形式示例
0
There are no items.
1
There is 1 item.
2
There are 2 items.
5
There are 5 items.
翻译最佳实践:
  • 使用完整的句子而不是拼接字符串
  • 为翻译字符串提供足够的上下文
  • 避免在翻译字符串中使用技术术语
  • 测试所有语言的布局和文本长度
翻译上下文
使用上下文消除歧义
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>
翻译示例
源文本 (English)

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
.po 文件格式
locale/zh_Hans/LC_MESSAGES/django.po
#: 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" -->
格式对比示例
English (en-US)

Date: 12/25/2023

Time: 3:30 PM

Number: 1,234.56

Currency: $1,234.56

中文 (zh-CN)

日期: 2023年12月25日

时间: 15:30

数字: 1,234.56

货币: ¥1,234.56

Deutsch (de-DE)

Datum: 25.12.2023

Uhrzeit: 15:30

Zahl: 1.234,56

Währung: 1.234,56 €

Français (fr-FR)

Date: 25/12/2023

Heure: 15:30

Nombre: 1 234,56

Monnaie: 1 234,56 €

Python 代码中的本地化
views.py
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>
时区设置
settings.py
# 时区设置
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,检查格式设置
调试技巧
  • 使用 Django 调试工具栏检查翻译
  • 检查当前激活的语言和时区
  • 验证 .po 文件语法
  • 测试所有支持的语言
  • 使用伪翻译验证字符串覆盖
# 调试信息
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}")
生产环境注意事项:
  • 确保所有翻译文件已编译
  • 设置正确的默认语言和时区
  • 配置适当的语言检测机制
  • 监控翻译覆盖率和质量