翻译

概况

为了使 Django 项目可以翻译,你需要在 Python 代码和模板中添加少量钩子。这些钩子被成为 translation strings 。它们告知Django:如果在终端用户语言里,这个文本有对应的翻译,那么应该使用翻译。标记字符串是你的职责,系统只会翻译它知道的字符串。

然后 Django 提供工具将翻译字符串提取到 message file 中。这个文件让翻译者方便地提供翻译字符串。一旦翻译者填写了 message file ,就必须编译它。这个过程依赖 GNU gettext 工具集。

一旦完成这些步骤,Django 将根据用户的语言偏好,动态地将 web 应用程序翻译成每种可用的语言。

Django 的国际化钩子默认是开启的,这意味着在框架的某些位置存在一些 i18n 相关的开销。如果你不使用国际化,你应该在配置文件里设置 USE_I18N = False 。然后 Django 将进行优化,以免加载国际化机制。

Note

确保你的项目已经激活了翻译(最快的办法是检查 MIDDLEWARE 是否包含 django.middleware.locale.LocaleMiddleware )。如果还没有激活,请查看“ Django 如何发现语言偏好( Django 如何发现语言偏好 )”。

在 Python 代码中进行国际化

标准翻译

使用函数 gettext() 来指定翻译字符串。按照惯例,将其作为下划线( _ )导入,以保存输入。

Note

Python 标准库 gettext 模块在全局命名空间里安装了 _() 。在 Django 里,我们没有遵循这个做法,出于下面两个原因:

  1. 有时,对于特定文件,你应该使用 gettext_lazy() 作为默认翻译方法。如果全局命名空间里没有 _() ,开发者必须考虑哪个是最合适的翻译函数。

  2. 下划线("_") 用于表示在 Python 的交互式终端和 doctest 测试中 "上一个结果" 。安装全局 _() 函数会引发冲突。导入 gettext() 替换 _() 可以避免这个问题。

哪些函数能以 _ 为别名?

由于 xgettextmakemessages 使用)的工作方式,只有带有单一字符串参数的函数才能当做 _ 引入:

在这个例子里,文本 "Welcome to my site."  被标记为翻译字符串:

from django.http import HttpResponse
from django.utils.translation import gettext as _


def my_view(request):
    output = _("Welcome to my site.")
    return HttpResponse(output)

代码里可以不使用别名。这个例子与上一个例子等同:

from django.http import HttpResponse
from django.utils.translation import gettext


def my_view(request):
    output = gettext("Welcome to my site.")
    return HttpResponse(output)

翻译适用于计算值。这个例子与前面两个例子等同:

def my_view(request):
    words = ["Welcome", "to", "my", "site."]
    output = _(" ".join(words))
    return HttpResponse(output)

翻译适用于变量。同样,这个例子和上面的等同:

def my_view(request):
    sentence = "Welcome to my site."
    output = _(sentence)
    return HttpResponse(output)

(像上述两个例子那样,使用变量或计算值的警告是: Django 的翻译字符检测实用程序 django-admin makemessages 不能找到这些字符串。更多细节在稍后的 makemessages 介绍。)

传递到 _() or gettext()  的字符串可以使用占位符,这是 Python 标准命名字符串插值语法指定的。例如:

def my_view(request, m, d):
    output = _("Today is %(month)s %(day)s.") % {"month": m, "day": d}
    return HttpResponse(output)

这是技巧使得特定语言翻译可以重新排序占位符字段。比如,一条英文翻译可能是 "Today is November 26." ,而对于西班牙翻译可能就是 "Hoy es 26 de noviembre." ——其中月份和日期占位符互相交换。

所以,当你有多个参数的时候,你应该使用命名字符串插值(比如 %(day)s )而不是位置插值(比如 %s 或 %d )。如果你使用位置插值,翻译时就不会对占位符字段重新排序。

因为字符串提取由 xgettext 命令行完成,Django 仅支持由 gettext 支持的语法。特别是,xgettext 尚未支持 Python f-strings ,而 JavaScript 模板字符串需要 gettext 0.21+ 。

为翻译者提供注释

如果你想为翻译者提供一些翻译字段的提示,你可以添加以 Translators 为前缀的注释。例如:

def my_view(request):
    # Translators: This message appears on the home page only
    output = gettext("Welcome to my site.")

这个注释将出现在 .po 文件中与所翻译词条相关的词条下方,并且也应该由大部分翻译工具显示。

Note

为了完整起见,这是 .po 文件的相应片段:

#. Translators: This message appears on the home page only
# path/to/python/file.py:123
msgid "Welcome to my site."
msgstr ""

这个办法在模板里也同样适用。查看 模板内对翻译的注释 来获取更多详情。

标记不用翻译的字符

使用 django.utils.translation.gettext_noop() 来将字符串标记不用翻译的翻译字符串。这个字符会稍后使用变量来翻译。

使用该方法的场景是:如果你有一个常量字符串,该字符串以源语言存储,它们通过系统或用户进行交换(比如数据库里的字符串),但应该最后可能的时间点进行翻译,比如当字符串展示给用户时。

多元化

使用 django.utils.translation.ngettext() 函数指定多元化信息。

ngettext() 带有三个参数:单数翻译字符串,复数翻译字符串和一些对象。

当你需要 Django 应用本地化为复数形式的数量和复杂度大于英语中使用的两种形式的语言时,该函数非常有用。('object' 表示单数,'objects' 表示 count 不同于1时的所有情况,不论其值如何)

例如:

from django.http import HttpResponse
from django.utils.translation import ngettext


def hello_world(request, count):
    page = ngettext(
        "there is %(count)d object",
        "there are %(count)d objects",
        count,
    ) % {
        "count": count,
    }
    return HttpResponse(page)

在这个例子里,对象的数量作为 count 变量传递给翻译语言。

要注意复数形式是复杂的,并且在每种语言中工作方式不同。比较 count 和 1 并不总是正确的规则。该代码看着很复杂,但会对某些语言产生不正确的结果:

from django.utils.translation import ngettext
from myapp.models import Report

count = Report.objects.count()
if count == 1:
    name = Report._meta.verbose_name
else:
    name = Report._meta.verbose_name_plural

text = ngettext(
    "There is %(count)d %(name)s available.",
    "There are %(count)d %(name)s available.",
    count,
) % {"count": count, "name": name}

不要尝试实现自定义的单数或复数逻辑,它会出错的。像在这种例子中,请考虑以下内容:

text = ngettext(
    "There is %(count)d %(name)s object available.",
    "There are %(count)d %(name)s objects available.",
    count,
) % {
    "count": count,
    "name": Report._meta.verbose_name,
}

Note

当使用 ngettext() 时,要确保为每一个变量使用一个名称。上面的例子中,请注意我们在两个翻译字符串中如何使用 name Python 变量。该例子除了上面提到的错误外,还会遇到下面的问题:

text = ngettext(
    "There is %(count)d %(name)s available.",
    "There are %(count)d %(plural_name)s available.",
    count,
) % {
    "count": Report.objects.count(),
    "name": Report._meta.verbose_name,
    "plural_name": Report._meta.verbose_name_plural,
}

当运行 django-admin compilemessages 时,你将会收到一个错误:

a format specification for argument 'name', as in 'msgstr[0]', doesn't exist in 'msgid'

上下文标记

一些词组有很多不同含义,比如 "May" ,它指五月或者表示一个动词。为了使翻译者在不同上下文中正确翻译这些词组,可以使用 django.utils.translation.pgettext() 函数,或者如果字符串需要复数形式的话,可以使用 django.utils.translation.npgettext() 函数。两者都使用上下文字符串作为第一个变量。

在生成的 .po 文件中,字符串将会频繁出现,因为相同字符串 ( 上下文将在 msgctxt 行上显示) 有不同的上下文标记,所以允许翻译者为每个字符串给出不同翻译。

例如:

from django.utils.translation import pgettext

month = pgettext("month name", "May")

或者:

from django.db import models
from django.utils.translation import pgettext_lazy


class MyThing(models.Model):
    name = models.CharField(
        help_text=pgettext_lazy("help text for MyThing model", "This is the help text")
    )

将以下方形式出现在 .po 文件中:

msgctxt "month name"
msgid "May"
msgstr ""

上下文标记也由 translateblocktranslate 模板标签支持。

惰性翻译

当访问值而不是调用它们时,使用 django.utils.translation 中翻译函数的惰性版本 (在名称中加入 lazy 前缀来识别) 来延迟翻译字符串。

这些函数存储对字符串的惰性引用 —— 并非真实的翻译。当在字符串上下文里使用字符串时,才会翻译。比如模板渲染。

当对这些函数的调用位于模块加载时执行的代码路径中时,这一点非常重要。

在定义模型、表单和模型表单的时候,很容易遇见这种情况,因为 Django 已经实现了这些,所以它们的字段实际是类级别的属性。因此,请确保以下情况下使用惰性翻译:

模型字段和相关的 verbose_namehelp_text 选项值

比如下面这个模型,为了翻译其中 name 字段的帮助提示,可以这么做:

from django.db import models
from django.utils.translation import gettext_lazy as _


class MyThing(models.Model):
    name = models.CharField(help_text=_("This is the help text"))

你可以通过使用 verbose_name 选项来将 ForeignKey, ManyToManyFieldOneToOneField 关系标记为可翻译:

class MyThing(models.Model):
    kind = models.ForeignKey(
        ThingKind,
        on_delete=models.CASCADE,
        related_name="kinds",
        verbose_name=_("kind"),
    )

就像你在 verbose_name 中所做的那样,你应该为关系提供一个小写的详细名称文本,因为 Django 会在需要时自动为其命名。

模型详细名称的值

建议始终提供显式的 verbose_nameverbose_name_plural 选项,而不是依赖 Django 查看模型的类名来确定以英文为主而且有点简单的详细名称:

from django.db import models
from django.utils.translation import gettext_lazy as _


class MyThing(models.Model):
    name = models.CharField(_("name"), help_text=_("This is the help text"))

    class Meta:
        verbose_name = _("my thing")
        verbose_name_plural = _("my things")

模型方法 description 参数用于 @display 装饰器。

对于模型方法,你可以使用 display() 装饰器的 description 参数来提供翻译给 Django 和管理站点:

from django.contrib import admin
from django.db import models
from django.utils.translation import gettext_lazy as _


class MyThing(models.Model):
    kind = models.ForeignKey(
        ThingKind,
        on_delete=models.CASCADE,
        related_name="kinds",
        verbose_name=_("kind"),
    )

    @admin.display(description=_("Is it a mouse?"))
    def is_mouse(self):
        return self.kind.type == MOUSE_TYPE

使用惰性翻译对象

gettext_lazy() 调用的结果可以在你在其他 Django 代码中使用字符串(一个 str 对象)的地方使用,但它可能无法与任意的 Python 代码一起工作。例如,以下代码将不起作用,因为 requests 库不处理 gettext_lazy 对象:

body = gettext_lazy("I \u2764 Django")  # (Unicode :heart:)
requests.post("https://example.com/send", data={"body": body})

要想避免这些问题,你可以在文本字符传递给非 Django 代码之前,将 gettext_lazy() 对象转换为文本字符。

requests.post("https://example.com/send", data={"body": str(body)})

如果你觉得 gettext_lazy 这个名字有点长,你可以定义它的别名为下划线 _ ,像这样:

from django.db import models
from django.utils.translation import gettext_lazy as _


class MyThing(models.Model):
    name = models.CharField(help_text=_("This is the help text"))

在模型和工具函数里使用 gettext_lazy()ngettext_lazy() 来标记字符串是个常用方法。但在代码的其他地方使用这些对象时,你要确保它们不会被不小心转化为字符串,因为它们要尽量晚一些被转换(以便正确的语言环境生效)。这需要使用下面所描述的帮助函数。

惰性翻译与复数

当为复数字符串 (n[p]gettext_lazy) 使用惰性翻译时,通常不知道字符串定义时 number 参数值。因此,你可以传递一个关键字名称来作为 number 参数而不是整数。然后,在插值期间,会在字典里查找 number。例如:

from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import ngettext_lazy


class MyForm(forms.Form):
    error_message = ngettext_lazy(
        "You only provided %(num)d argument",
        "You only provided %(num)d arguments",
        "num",
    )

    def clean(self):
        # ...
        if error:
            raise ValidationError(self.error_message % {"num": number})

如果字符串恰好包含一个未命名的占位符,你可以直接插入 number 参数:

class MyForm(forms.Form):
    error_message = ngettext_lazy(
        "You provided %d argument",
        "You provided %d arguments",
    )

    def clean(self):
        # ...
        if error:
            raise ValidationError(self.error_message % number)

格式化字符串:format_lazy()

format_stringstr.format() 的任何参数包含惰性转换对象时,Python的 str.format() 方法将不起作用。相反,你可以使用 django.utils.text.format_lazy() ,它会创建一个仅当结果包含在字符串里时会才运行 str.format() 的惰性对象。比如:

from django.utils.text import format_lazy
from django.utils.translation import gettext_lazy

...
name = gettext_lazy("John Lennon")
instrument = gettext_lazy("guitar")
result = format_lazy("{name}: {instrument}", name=name, instrument=instrument)

在这种情况下,在 result 中的惰性翻译仅在字符串(通常在模板渲染时间)里使用 result 时才会被转换成字符串。

延迟翻译中惰性(lazy)的其他用法

对于“你想延迟翻译,但不得不传递要翻译的字符串作为参数到其他函数”的情况,你可以在惰性调用中包装这个函数。比如:

from django.utils.functional import lazy
from django.utils.translation import gettext_lazy as _


def to_lower(string):
    return string.lower()


to_lower_lazy = lazy(to_lower, str)

然后:

lazy_string = to_lower_lazy(_("My STRING!"))

语言的本地化名称

get_language_info(lang_code)[source]

get_language_info() 函数提供关于语言的详细信息:

>>> from django.utils.translation import activate, get_language_info
>>> activate("fr")
>>> li = get_language_info("de")
>>> print(li["name"], li["name_local"], li["name_translated"], li["bidi"])
German Deutsch Allemand False

字典的 name, name_local, 以及 name_translated 属性分别包含用英语、该语言本身以及当前启用的语言的名称。bidi 属性仅对于双向(bi-directional)语言为 True 。

语言信息的来源是 django.conf.locale 模块。模板代码也可以使用类似方式访问信息。

在模板代码中国际化

Django templates 中的翻译使用两个模板标签,并且语法与 Python 代码中的语法略有不同。要使模板访问这些标签,需要在模板顶部加入 {% load i18n %} 。与所有模板标签一样,这个标签需要在所有需要翻译的模板中加载,甚至那些从其他模板继承(extend)而来的模板,也需要继承 i18n 标签。

Warning

在渲染模板的时候,翻译后的字符串不会被转义。这使你在翻译中包含 HTML ,比如为了强调,但潜在的危险字符(比如 ")也将保持不变。

translate 模板标签

{% translate %} 模板标签可以翻译一个常量字符串(包含在单引号或双引号中)或变量内容:

<title>{% translate "This is the title." %}</title>
<title>{% translate myvar %}</title>

如果存在 noop 选项,变量查找仍然会发生,但翻译会被跳过。这在将来需要翻译的内容中“存根化”时很有用:

<title>{% translate "myvar" noop %}</title>

在内部,内联翻译使用了 gettext() 调用。

如果模板变量(如上面的 myvar )传递给了一个标签,该标签将在运行时首先将这些变量解析为字符串,然后在消息目录里查找变量。

{% translate %} 中,不可能在字符串中混合模板变量。如果你的翻译需要变量字符串(占位符),请使用 {% blocktranslate %} 替代。

如果你想获取一个已翻译的字符串而不显示它,你可以使用以下语法:

{% translate "This is the title" as the_title %}

<title>{{ the_title }}</title>
<meta name="description" content="{{ the_title }}">

在实际应用中,你可以使用这个方法来获取一个可以在模板中多个地方使用的字符串,或者作为其他模板标签或过滤器的参数使用输出内容:

{% translate "starting point" as start %}
{% translate "end point" as end %}
{% translate "La Grande Boucle" as race %}

<h1>
  <a href="/" title="{% blocktranslate %}Back to '{{ race }}' homepage{% endblocktranslate %}">{{ race }}</a>
</h1>
<p>
{% for stage in tour_stages %}
    {% cycle start end %}: {{ stage }}{% if forloop.counter|divisibleby:2 %}<br>{% else %}, {% endif %}
{% endfor %}
</p>

{% translate %} 也支持使用 context 关键词的 上下文标记

{% translate "May" context "month name" %}

blocktranslate 模板标签

translate 标签相反,blocktranslate 标签允许你通过使用占位符来标记由文字和变量内容组成的复杂句子以进行翻译:

{% blocktranslate %}This string will have {{ value }} inside.{% endblocktranslate %}

要翻译模板表达式,比如访问对象属性或使用模板过滤器,你需要将表达式绑定到一个本地变量中,以便在翻译块内使用。示例:

{% blocktranslate with amount=article.price %}
That will cost $ {{ amount }}.
{% endblocktranslate %}

{% blocktranslate with myvar=value|filter %}
This will have {{ myvar }} inside.
{% endblocktranslate %}

你可以在单个 blocktranslate 标签内使用多个表达式:

{% blocktranslate with book_t=book|title author_t=author|title %}
This is {{ book_t }} by {{ author_t }}
{% endblocktranslate %}

Note

仍然支持以前的更简洁的格式:{% blocktranslate with book|title as book_t and author|title as author_t %}

其他块标签(例如 {% for %}{% if %})不能在 blocktranslate 标签内使用。

如果解决一个区块参数失败,blocktranslate 将回到默认语言,暂时停用当前活动的语言与 deactivate_all() 函数。

该标签也提供复数形式。要使用它:

  • 指定并绑定名称为 count 的计数器值。该值将用于选择正确的复数形式。

  • {% blocktranslate %}{% endblocktranslate %} 标签中,用 {% plural %} 标签指定单数和复数形式,将它们分开。

例子:

{% blocktranslate count counter=list|length %}
There is only one {{ name }} object.
{% plural %}
There are {{ counter }} {{ name }} objects.
{% endblocktranslate %}

更复杂的例子:

{% blocktranslate with amount=article.price count years=i.length %}
That will cost $ {{ amount }} per year.
{% plural %}
That will cost $ {{ amount }} per {{ years }} years.
{% endblocktranslate %}

当你同时使用复数功能和将值绑定到本地变量上时,请记住,blocktranslate 结构在内部被转换为 ngettext 调用。这意味着同样的 关于 ngettext 变量的注释 适用。

不能在 blocktranslate 内执行反向 URL 查找,应该在之前获取(并存储)它们:

{% url 'path.to.view' arg arg2 as the_url %}
{% blocktranslate %}
This is a URL: {{ the_url }}
{% endblocktranslate %}

如果你想获取一个已翻译的字符串而不显示它,你可以使用以下语法:

{% blocktranslate asvar the_title %}The title is {{ title }}.{% endblocktranslate %}
<title>{{ the_title }}</title>
<meta name="description" content="{{ the_title }}">

事实上你可以使用这个办法来获取一个在模板中多处使用的字符串,或将输出作为其他模板标签或过滤器的参数。

{% blocktranslate %} 也支持使用 context 关键字的 上下文标记

{% blocktranslate with name=user.username context "greeting" %}Hi {{ name }}{% endblocktranslate %}

{% blocktranslate %} 支持的另一个功能是 trimmed 选项。这个选项将删除 {% blocktranslate %} 标签的内容开头和结尾的换行符,替换每一行开头和结尾的空白,并使用空格字符将所有行合并为一行。这对于缩进 {% blocktranslate %} 标签的内容而不使缩进字符出现在 .po 文件中的对应条目中非常有用,从而简化了翻译过程。

例如,下面的 {% blocktranslate %} 标签:

{% blocktranslate trimmed %}
  First sentence.
  Second paragraph.
{% endblocktranslate %}

如果没有指定 trimmed 选项,将在 .po 文件中生成条目 "First sentence. Second paragraph.",而不是 "\n  First sentence.\n  Second paragraph.\n"

传递字符串给标签和过滤器

你可以通过使用熟悉的 _() 语法来翻译传递给标签和过滤器的字符串字面值作为参数:

{% some_tag _("Page not found") value|yesno:_("yes,no") %}

在这种情况下,标签和过滤器将看到翻译后的字符串,因此它们不需要知道翻译。

Note

在这个例子里,翻译基础结构将被传递字符串 "yes,no" ,而不是单独的字符 "yes""no" 。翻译后的字符串需要包含逗号,以便过滤器解析代码时知道如何分割参数。比如,一名德国翻译者可以将 "yes,no" 翻译为 "ja,nein" (保持逗号不变)。

模板内对翻译的注释

就像 Python 代码一样,对翻译者的提示可以使用注释来标注,使用 comment 标签:

{% comment %}Translators: View verb{% endcomment %}
{% translate "View" %}

{% comment %}Translators: Short intro blurb{% endcomment %}
<p>{% blocktranslate %}A multiline translatable
literal.{% endblocktranslate %}</p>

或者使用 {# ... #}  单行注释:

{# Translators: Label of a button that triggers search #}
<button type="submit">{% translate "Go" %}</button>

{# Translators: This is a text of the base template #}
{% blocktranslate %}Ambiguous translatable block of text{% endblocktranslate %}

Note

为了完整起见,这些是生成 .po 文件的相应片段:

#. Translators: View verb
# path/to/template/file.html:10
msgid "View"
msgstr ""

#. Translators: Short intro blurb
# path/to/template/file.html:13
msgid ""
"A multiline translatable"
"literal."
msgstr ""

# ...

#. Translators: Label of a button that triggers search
# path/to/template/file.html:100
msgid "Go"
msgstr ""

#. Translators: This is a text of the base template
# path/to/template/file.html:103
msgid "Ambiguous translatable block of text"
msgstr ""

在模板中选择语言

如果想在模板中选择语言,可以使用 language 模板标签:

{% load i18n %}

{% get_current_language as LANGUAGE_CODE %}
<!-- Current language: {{ LANGUAGE_CODE }} -->
<p>{% translate "Welcome to our page" %}</p>

{% language 'en' %}
    {% get_current_language as LANGUAGE_CODE %}
    <!-- Current language: {{ LANGUAGE_CODE }} -->
    <p>{% translate "Welcome to our page" %}</p>
{% endlanguage %}

第一次出现的 "Welcome to our page" 使用当前语言,后面出现的使用英语。

其他标签

下面这些标签同样需要先引入 {% load i18n %}.

get_available_languages

{% get_available_languages as LANGUAGES %} 返回一个元组列表,其中第一个元素是:term:language code,第二个元素是语言名称(翻译成当前活动的语言环境)。

get_current_language

{% get_current_language as LANGUAGE_CODE %} 返回字符串类型的当前用户首选语言. 类似: en-us. 详情参阅 Django 如何发现语言偏好.

get_current_language_bidi

{% get_current_language_bidi as LANGUAGE_BIDI %} 返回当前语言文字的阅读方向。如果是 True ,则是从右向左阅读的语言,比如希伯来语,阿拉伯语。如果是 False ,则是从左向右阅读的语言,比如英语,法语,德语等。

i18n 上下文处理器

如果启用了 django.template.context_processors.i18n 上下文处理器,则每个 RequestContext 都可以访问上面所定义的 LANGUAGES, LANGUAGE_CODELANGUAGE_BIDI

get_language_info

你还可以使用提供的模板标签和过滤器获取有关任何可用语言的信息。要获取有关单个语言的信息,使用 {% get_language_info %} 标签:

{% get_language_info for LANGUAGE_CODE as lang %}
{% get_language_info for "pl" as lang %}

然后你可以访问这些信息:

Language code: {{ lang.code }}<br>
Name of language: {{ lang.name_local }}<br>
Name in English: {{ lang.name }}<br>
Bi-directional: {{ lang.bidi }}
Name in the active language: {{ lang.name_translated }}

get_language_info_list

你也可以使用 {% get_language_info_list %} 模板标签来检索语言列表的信息(例如,LANGUAGES 中指定的有效语言)。查看 关于 set_language 重定向视图的章节 了解如何使用 {% get_language_info_list %} 来显示语言选择器。

除元组的 LANGUAGES 样式列表之外,{% get_language_info_list %} 也支持语言代码列表。如果在视图中这么做:

context = {"available_languages": ["en", "es", "fr"]}
return render(request, "mytemplate.html", context)

你可以在模板中迭代这些语言:

{% get_language_info_list for available_languages as langs %}
{% for lang in langs %} ... {% endfor %}

模板过滤器

为方便起见,还提供了一些过滤器:

  • {{ LANGUAGE_CODE|language_name }} ("German")

  • {{ LANGUAGE_CODE|language_name_local }} ("Deutsch")

  • {{ LANGUAGE_CODE|language_bidi }} (False)

  • {{ LANGUAGE_CODE|language_name_translated }} ("německy", 当活动语言为捷克语时)

国际化:在 JavaScript 代码里

向 JavaScript 添加翻译会带来一些问题:

  • JavaScript 代码无权访问 gettext 实现。

  • JavaScript 代码无权访问 .po.mo 文件;它们需要通过服务器传递。

  • JavaScript 的翻译目录应该尽可能小。

Django 为这三个问题提供了解决方案:它将翻译传递给Javascript,因此你可以从 JavaScript 内部调用 gettext 等。

解决这三个问题的主要解决办法是下面的 JavaScriptCatalog 视图,该视图生成一个JavaScript 库,该库具有模仿 gettext 接口的函数,外加一个翻译字符串数组。

JavaScriptCatalog 视图

class JavaScriptCatalog[source]

一个视图,该视图生成一个 JavaScript 代码库,该库带有一个模仿 gettext 界面的函数,外加一个翻译字符串的数组。

属性

domain

翻译域包含要添加到视图输出中的字符串。默认为 'djangojs'

packages

已安装的应用程序的程序名列表。这些 app 应包含一个 locale 目录。所有这些目录加上 LOCALE_PATHS  里的目录会合并到一个目录里。默认是 None ,意味着在 JavaScript 输出中提供所有来自 INSTALLED_APPS 的可用翻译。

默认值实例

from django.views.i18n import JavaScriptCatalog

urlpatterns = [
    path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
]

自定义包实例

urlpatterns = [
    path(
        "jsi18n/myapp/",
        JavaScriptCatalog.as_view(packages=["your.app.label"]),
        name="javascript-catalog",
    ),
]

如果你的根 URLconf 使用 i18n_patterns()JavaScriptCatalog 也必须被 i18n_patterns() 包装,这样目录才会被正确的生成。

i18n_patterns() 实例:

from django.conf.urls.i18n import i18n_patterns

urlpatterns = i18n_patterns(
    path("jsi18n/", JavaScriptCatalog.as_view(), name="javascript-catalog"),
)

后面出现的 packages 参数比前面出现的参数优先级更高。在相同文字出现冲突翻译的时候,这点很重要。

如果你使用多个 JavaScriptCatalog 视图,并且它们中的某些视图定义了相同的字符串,那么最后加载的目录中的字符串拥有优先级。

使用 JavaScript 翻译目录

要使用目录,请像这样引入动态生成的js:

<script src="{% url 'javascript-catalog' %}"></script>

这使用反向 URL 查询来查找 JavaScript 目录视图。当加载目录后,JavaScript 代码能使用以下方法:

  • gettext

  • ngettext

  • interpolate

  • get_format

  • gettext_noop

  • pgettext

  • npgettext

  • pluralidx

gettext

gettext 函数的行为类似于 Python 代码中的标准 gettext 接口:

document.write(gettext("this is to be translated"))

ngettext

ngettext 函数提供了一个用于复数化单词和短语的接口:

const objectCount = 1 // or 0, or 2, or 3, ...
const string = ngettext(
    'literal for the singular case',
    'literal for the plural case',
    objectCount
);

interpolate

interpolate 函数支持动态填充格式字符串(format string)。插值语法借鉴自 Python,因此 interpolate 函数支持位置插值和命名插值。

  • 位置插值:obj 包含一个 JavaScript 数组对象,其元素值然后按照它们在 fmt 占位符中出现的顺序依次插值。例如:

    const formats = ngettext(
      'There is %s object. Remaining: %s',
      'There are %s objects. Remaining: %s',
      11
    );
    const string = interpolate(formats, [11, 20]);
    // string is 'There are 11 objects. Remaining: 20'
    
  • 命名插值:通过将可选的布尔参数 named 设置为 true 来选择此模式。obj 包含一个 JavaScript 对象或关联数组。例如:

    const data = {
      count: 10,
      total: 50
    };
    
    const formats = ngettext(
        'Total: %(total)s, there is %(count)s object',
        'there are %(count)s of a total of %(total)s objects',
        data.count
    );
    const string = interpolate(formats, data, true);
    

尽管如此,你不应该使用字符串插值:这仍然是 JavaScript,因此代码必须进行重复的正则表达式替换。这并不像 Python 中的字符串插值那么快,所以请确保你真的需要这么做(比如,与 ngettext 结合产生合适的复数形式)。

get_format

get_format 函数可以访问配置的 i18n 格式设置,并可以检索给定设置名称的格式字符串:

document.write(get_format('DATE_FORMAT'));
// 'N j, Y'

它可以访问以下设置:

这对于保持与Python呈现的值的格式一致性非常有用。

gettext_noop

这模拟了 gettext 函数,但不执行任何操作,返回那传递给它的所有内容:

document.write(gettext_noop("this will not be translated"))

这对于在将来需要翻译的代码中删除部分代码时很有用。

pgettext

pgettext 函数的行为类似于 Python 变体(pgettext()),它提供了一个上下文相关的单词:

document.write(pgettext("month name", "May"))

npgettext

npgettext 函数也像 Python 变体 (npgettext()) 一样工作,提供 复数 的上下文翻译词汇:

document.write(npgettext('group', 'party', 1));
// party
document.write(npgettext('group', 'party', 2));
// parties

pluralidx

pluralidx 函数的工作方式类似于 pluralize 模板过滤器,它确定给定的 count 是否应该使用单词的复数形式:

document.write(pluralidx(0));
// true
document.write(pluralidx(1));
// false
document.write(pluralidx(2));
// true

在这种最简单的情况下,如果不需要自定义复数,那么会对整数 1 返回 false ,其他数返回 true

然而,在所有语言中复数化并不是这么简单。如果语言不支持复数化,那么就需要提供一个空值。

此外,如果复数的规则复杂,目录视图将渲染一个条件表达式。它将返回 true (如果是复数) 或 false (如果不是复数) 的值。

JSONCatalog 视图

class JSONCatalog[source]

为了使用其他客户端的库来控制翻译,你可能需要利用 JSONCatalog 视图。它类似于 JavaScriptCatalog ,但返回一个 JSON 响应。

查看 JavaScriptCatalog 文档来了解关于 domainpackages 属性的可能值和使用。

响应格式如下:

{
    "catalog": {
        # Translations catalog
    },
    "formats": {
        # Language formats for date, time, etc.
    },
    "plural": "..."  # Expression for plural forms, or null.
}

性能说明

各种 JavaScript/JSON i18n 视图在每次请求时从 .mo 文件生成日历。因为其输出是恒定的,至少对于给定版本的站点来说,它是缓存的一个很好的选择。

服务端缓存将减少 CPU 负载。使用 cache_page() 装饰器很容易实现。要在翻译变化时让缓存失效,需要提供一个版本相关的键前缀,如下例所示,或将视图映射到版本相关的 URL :

from django.views.decorators.cache import cache_page
from django.views.i18n import JavaScriptCatalog

# The value returned by get_version() must change when translations change.
urlpatterns = [
    path(
        "jsi18n/",
        cache_page(86400, key_prefix="jsi18n-%s" % get_version())(
            JavaScriptCatalog.as_view()
        ),
        name="javascript-catalog",
    ),
]

客户端缓存将节省带宽并让网站加载速度变快。如果你正在使用 ETags(ConditionalGetMiddleware),那么你已经被覆盖了。否则,你可以应用条件装饰器(conditional decorators)。在下面的例子中,当你重启应用程序服务时,缓存将失效。

from django.utils import timezone
from django.views.decorators.http import last_modified
from django.views.i18n import JavaScriptCatalog

last_modified_date = timezone.now()

urlpatterns = [
    path(
        "jsi18n/",
        last_modified(lambda req, **kw: last_modified_date)(
            JavaScriptCatalog.as_view()
        ),
        name="javascript-catalog",
    ),
]

You can even pre-generate the JavaScript catalog as part of your deployment procedure and serve it as a static file. This radical technique is implemented in django-statici18n.

国际化:在 URL 模式中

Django 提供两种方式来国际化 URL 模式:

Warning

使用这些特性需要为每个请求设置一个激活的语言;换句话说,你需要在 MIDDLEWARE 设置中包含 django.middleware.locale.LocaleMiddleware

URL 模式中的语言前缀

i18n_patterns(*urls, prefix_default_language=True)[source]

这个函数可以在根URLconf中使用,Django 会自动将当前激活的语言代码添加到 i18n_patterns() 中定义的所有 URL 模式中。

设置 prefix_default_languageFalse 会从默认语言( LANGUAGE_CODE )中删除前缀。这在向现有网站添加翻译时非常有用,这样当前的网址就不会改变。

URL 模式举例:

from django.conf.urls.i18n import i18n_patterns
from django.urls import include, path

from about import views as about_views
from news import views as news_views
from sitemap.views import sitemap

urlpatterns = [
    path("sitemap.xml", sitemap, name="sitemap-xml"),
]

news_patterns = (
    [
        path("", news_views.index, name="index"),
        path("category/<slug:slug>/", news_views.category, name="category"),
        path("<slug:slug>/", news_views.details, name="detail"),
    ],
    "news",
)

urlpatterns += i18n_patterns(
    path("about/", about_views.main, name="about"),
    path("news/", include(news_patterns, namespace="news")),
)

定义了这些 URL 模式后,Django 将自动将语言前缀添加到由 i18n_patterns 函数添加的 URL 模式中。示例:

>>> from django.urls import reverse
>>> from django.utils.translation import activate

>>> activate("en")
>>> reverse("sitemap-xml")
'/sitemap.xml'
>>> reverse("news:index")
'/en/news/'

>>> activate("nl")
>>> reverse("news:detail", kwargs={"slug": "news-slug"})
'/nl/news/news-slug/'

使用 prefix_default_language=FalseLANGUAGE_CODE='en',URL 将是:

>>> activate("en")
>>> reverse("news:index")
'/news/'

>>> activate("nl")
>>> reverse("news:index")
'/nl/news/'

Warning

只允许 i18n_patterns() 在根URLconf中运行。在包含的URLconf中使用它时会弹出 ImproperlyConfigured 异常。

Warning

确保没有可能与自动添加的语言前缀冲突的非前缀 URL 模式。

翻译URL模式

URL模式也可以使用 gettext_lazy() 函数标记。例如:

from django.conf.urls.i18n import i18n_patterns
from django.urls import include, path
from django.utils.translation import gettext_lazy as _

from about import views as about_views
from news import views as news_views
from sitemaps.views import sitemap

urlpatterns = [
    path("sitemap.xml", sitemap, name="sitemap-xml"),
]

news_patterns = (
    [
        path("", news_views.index, name="index"),
        path(_("category/<slug:slug>/"), news_views.category, name="category"),
        path("<slug:slug>/", news_views.details, name="detail"),
    ],
    "news",
)

urlpatterns += i18n_patterns(
    path(_("about/"), about_views.main, name="about"),
    path(_("news/"), include(news_patterns, namespace="news")),
)

在创建了翻译之后,reverse() 函数将以当前活动语言返回 URL。示例:

>>> from django.urls import reverse
>>> from django.utils.translation import activate

>>> activate("en")
>>> reverse("news:category", kwargs={"slug": "recent"})
'/en/news/category/recent/'

>>> activate("nl")
>>> reverse("news:category", kwargs={"slug": "recent"})
'/nl/nieuws/categorie/recent/'

Warning

在大多数情况下,最好只在带有语言代码前缀的模式块中使用翻译后的网址(使用 i18n_patterns() ),以避免无意中翻译的网址导致与未翻译的网址模式冲突的可能性。

在模板中反向解析URL

如果本地化的URLs在模板中解析,那么它们会始终使用当前的语言。要链接其他语言中的URL,需使用 language 模板标签。它可以在随附的模板部分中启用给定的语言:

{% load i18n %}

{% get_available_languages as languages %}

{% translate "View this category in:" %}
{% for lang_code, lang_name in languages %}
    {% language lang_code %}
    <a href="{% url 'category' slug=category.slug %}">{{ lang_name }}</a>
    {% endlanguage %}
{% endfor %}

language 标签将语言代码作为唯一的参数。

本地化:如何创建语言文件

一旦标记了应用程序的字符串文字以供以后翻译,就需要写入(或获取)翻译。这里介绍一下方法。

消息文件

首先需要为新语言创建 message file 文件。消息文件是一个纯文本文件,代表一种语言,它包含所有可用的翻译字段以及如何以给定语言表示。消息文件扩展名是 .po 文件。

Django 附带的工具 django-admin makemessages 会自动创建并保存这些文件。

Gettext 实用程序

makemessages 命令(和稍后讨论的 compilemessages )使用来自 GNU 文字工具集的命令行:xgettext, msgfmt, msgmergemsguniq

gettext 实用工具集支持的最低版本是 0.15 。

要创建或更新消息文件,请运行此命令:

django-admin makemessages -l de

...其中 de 是你要创建的消息文件的 locale name 。例如,pt_BR 是葡萄牙语,de_AT 是奥地利德语,id 是印尼语。

脚本应该从以下两个位置之一来运行:

  • 你的 Django 项目的根目录(就是包含 manage.py 的那个目录)。

  • Django app的根目录。

脚本会遍历你的项目源代码树或者应用程序源代码库,并抽出所有要被翻译的字符串(查看 Django 如何发现翻译 并确保 LOCALE_PATHS 被正确设置)。它在 locale/LANG/LC_MESSAGES 目录中创建(或更新)消息文件。以德语为例,这个文件会是 locale/de/LC_MESSAGES/django.po

在项目的根目录执行 makemessages 命令时,提取的字符串将自动分发到合适的消息文件。这就是说,从包含 locale 目录的 app 文件中提取的字符串将进入该目录下的消息文件中。从不包含任何 locale 目录的 app 文件中提取的字符串将进入 LOCALE_PATHS 中列出的第一个目录下的消息文件,如果 LOCALE_PATHS 为空,则会报错。

默认情况下,django-admin makemessages 会检查具有 .html.txt.py 文件扩展名的每个文件。如果您想覆盖此默认设置,请使用 --extension-e 选项来指定要检查的文件扩展名:

django-admin makemessages -l de -e txt

用逗号分隔多个扩展名,并/或多次使用 -e--extension

django-admin makemessages -l de -e html,txt -e xml

Warning

从 JavaScript 源码中创建消息文件时(creating message files from JavaScript source code),你需要使用特别的 djangojs 域,而不是 -e js

使用 Jinja2 模板?

makemessages 不识别Jinja2模板的语法。要从包含Jinja2模板的项目中提取字符串,需要使用 BabelMessage Extracting

以下是一个示例的 babel.cfg 配置文件:

# Extraction from Python source files
[python: **.py]

# Extraction from Jinja2 templates
[jinja2: **.jinja]
extensions = jinja2.ext.with_

确保列出所有正在使用的扩展名。否则Babel不会识别这些扩展名定义的标签,并会完全忽略包含它们的Jinja2模板。

Babel提供了与 makemessages 类似的功能,通常可以替换它,并且不会依赖 gettext 。获取更多信息,请查阅关于使用消息目录( working with message catalogs )的文档。

没有 gettext?

如果你还没有安装 gettextmakemessages 将创建一个空文件。如果是这种情况,要么安装 gettext ,要么复制英文消息文件(如果可用) (locale/en/LC_MESSAGES/django.po) 并将其作为起点,即一个空的翻译文件。

在 Windows 上工作?

如果你正在使用Windows并且需要安装 GNU gettext 程序以便 makemessages 工作,请参阅 Windows 上的 gettext 了解更多信息。

每个 .po 文件包含少量的元数据(例如翻译维护者的联系方式等等)以及大量的翻译文件 —— 要翻译的字符串以及实际翻译的字段之间的映射。

例如,如果 Djanog 程序包含一段 "Welcome to my site." 的翻译字符串,像这样:

_("Welcome to my site.")

... 然后 django-admin makemessages 将创建一个包含以下代码片段的 .po 文件 —— 一条消息:

#: path/to/python/module.py:23
msgid "Welcome to my site."
msgstr ""

快速解释:

  • msgid 是显示在源代码中需要翻译的字符串。不要改动它。

  • msgstr 是你翻译后的字符串。一开始它是空的,因此你需要填充它。确保在翻译中保留引号。

  • 为了方便,每个消息包含以前缀为 # 且位于 msgid 行上方的注释行的形式的注释行,这个注释行报货文件名和需要翻译字符串的行号。

长消息是特殊的情况。这里,在 msgstr (or msgid) 后的第一个字符串是空字符串。然后内容本身就会在接下来的几行中以每行一串的形式写出来。这些字符串被直接连接起来。不要忘了字符串中尾部的空格,否则,它们会被粘贴在一起而不留空白!

注意你的字符集

由于 gettext 工具内部的工作方式以及我们希望在 Django 核心和您的应用程序中允许非 ASCII 源字符串,您 必须 使用 UTF-8 作为您的 .po 文件的编码(在创建 .po 文件时的默认编码)。这意味着每个人都将使用相同的编码,这在 Django 处理 .po 文件时非常重要。

模糊条目

当翻译是从例如已翻译的字符串中推断出来的时候,:djadmin:`makemessages`有时会生成标记为“模糊”的翻译条目。在默认情况下,模糊条目**不会**被 :djadmin:`compilemessages`处理。

要重新检查所有源代码和模板以查找新的翻译字符串,并更新 所有 语言的消息文件,请运行以下命令:

django-admin makemessages -a

编译消息文件

创建消息文件后,以及每次修改它时,你需要把它编译成更有效的形式,以供 gettext 使用。使用 django-admin compilemessages 工具来执行此操作。

这个工具会运行在所有可用的 .po 文件上,并创建 .mo 文件,这些文件是经过优化以供 gettext 使用的二进制文件。在与您运行 django-admin makemessages 的相同目录中,可以像这样运行 django-admin compilemessages

django-admin compilemessages

这就行了。可以使用你的翻译文件了。

在 Windows 上工作?

如果你正在使用Windows并且需要安装 GNU gettext 程序以便 django-admin compilemessages 工作,请参阅 Windows 上的 gettext 了解更多信息。

.po 文件:编码和 BOM 的使用。

Django 只支持使用UTF-8编码的 .po 文件并且没有任何BOM(字节顺序标记),因此如果文本编辑器默认在文件开头添加这个标记,那么你需要重新配置它.

疑难解答: gettext() 在带有百分号的字符串中错误地检测 python-format

在某些情况下,带有百分号的字符串跟着一个空格和一个字符串转换换类型( string conversion type ) (例如 _("10% interest") ),:func:~django.utils.translation.gettext 会错误地使用 python-format 标记字符串.

如果试着使用已被错误标记的字符串来编译消息文件,会得到一个消息,例如" 'msgid' 和 'msgstr' 格式数量不匹配"( number of format specifications in 'msgid' and 'msgstr' does not match ),或者"'msgstr' 不是有效的 Python 格式字符串,与'msgid'不同 "( 'msgstr' is not a valid Python format string, unlike 'msgid').

要解决这个问题,可以通过添加第二个百分号来转义百分号:

from django.utils.translation import gettext as _

output = _("10%% interest")

或者可以使用非python格式( no-python-format ),这样所有百分号会被视为文字:

# xgettext:no-python-format
output = _("10% interest")

从 JavaScript 源码中创建消息文件

您可以像处理其他 Django 消息文件一样创建和更新消息文件,使用 django-admin makemessages 工具。唯一的区别是,您需要明确指定在 gettext 术语中称为域的内容,这里是 djangojs 域,通过提供一个 -d djangojs 参数,就像这样:

django-admin makemessages -d djangojs -l de

这会创建或更新德语的JavaScript 消息文件。更新消息文件后,像执行普通 Django 消息文件那样运行 django-admin compilemessages 即可。

Windows 上的 gettext

仅适用于需要提取消息ID或编译消息文件(.po)的场景。翻译工作本身涉及编辑这种已存在的文件,但如果你想创建你自己的消息文件,或者想要测试或编译一个已改动的消息文件,需下载一个预编译的二进制安装器(a precompiled binary installer)。

您也可以使用从其他地方获取的 gettext 二进制文件,只要 xgettext --version 命令正常工作即可。如果在 Windows 命令提示符中输入命令 xgettext --version 时弹出窗口显示 "xgettext.exe 生成了错误并将被 Windows 关闭",请不要尝试使用 Django 的翻译工具与带有 gettext 包的系统一起使用。

自定义 makemessages 命令

如果要给 xgettext 传递额外的参数,则需要创建自定义的 makemessages 命令,并覆盖它的 xgettext_options 属性:

from django.core.management.commands import makemessages


class Command(makemessages.Command):
    xgettext_options = makemessages.Command.xgettext_options + ["--keyword=mytrans"]

如果需要更多的灵活性,也可以给自定义的 makemessages 命令添加新的参数:

from django.core.management.commands import makemessages


class Command(makemessages.Command):
    def add_arguments(self, parser):
        super().add_arguments(parser)
        parser.add_argument(
            "--extra-keyword",
            dest="xgettext_keywords",
            action="append",
        )

    def handle(self, *args, **options):
        xgettext_keywords = options.pop("xgettext_keywords")
        if xgettext_keywords:
            self.xgettext_options = makemessages.Command.xgettext_options[:] + [
                "--keyword=%s" % kwd for kwd in xgettext_keywords
            ]
        super().handle(*args, **options)

杂项

set_language 重定向试图

set_language(request)[source]

方便起见,Django 附带了一个视图 django.views.i18n.set_language() ,它可以设置用户语言首选项,并且重定向到一个给定的URL,或者默认情况下,会返回到上一页。

要激活这个视图,需要在你的 URLconf 中添加下面这行代码:

path("i18n/", include("django.conf.urls.i18n")),

(注意这个例子会使视图在 /i18n/setlang/ 下可用。)

Warning

确保在 i18n_patterns() 中不包含上述 URL —— 它需要独立于语言才能正常工作。

视图期望通过 POST 方法调用,并在请求中设置 language 参数。如果 session 支持是可用的,视图会在用户的 session 中保存语言选择。它也会将语言选择保存在默认名为 django_language (名称可以通过 LANGUAGE_COOKIE_NAME 设置改变)的 cookie 中。

在设置语言选择后,Django 会检查 POSTGET 数据中的 next 参数。如果找到这个参数并且 Django 认为它是一个安全的链接(也就是说它不指向其他主机并使用安全模式),将会重定向到这个链接。否则,Django 可能会重定向到 Referer header 里的 URL,如果没设置这个 URL,则会跳转到 / ,这取决于请求的性质。

  • 如果请求接受 HTML 内容(基于其 Accept HTTP头),将始终执行回退。

  • 如果请求不接受 HTML,只有在设置了 next 参数的情况下才会进行回退。否则将返回 204 状态码(无内容)。

这里是一个模板代码的例子:

{% load i18n %}

<form action="{% url 'set_language' %}" method="post">{% csrf_token %}
    <input name="next" type="hidden" value="{{ redirect_to }}">
    <select name="language">
        {% get_current_language as LANGUAGE_CODE %}
        {% get_available_languages as LANGUAGES %}
        {% get_language_info_list for LANGUAGES as languages %}
        {% for language in languages %}
            <option value="{{ language.code }}"{% if language.code == LANGUAGE_CODE %} selected{% endif %}>
                {{ language.name_local }} ({{ language.code }})
            </option>
        {% endfor %}
    </select>
    <input type="submit" value="Go">
</form>

在这个例子中,Django 会在 redirect_to 变量中查找用户将要重定向的网址。

显式设置语言

你可能想显式地为当前会话设置语言。例如,可能是从另一个系统检索用户的语言偏好。你已经了解了 django.utils.translation.activate() 。这只适用于当前进程。要将整个会话的语言保留在 cookie 中,请在响应上设置 LANGUAGE_COOKIE_NAME

from django.conf import settings
from django.http import HttpResponse
from django.utils import translation

user_language = "fr"
translation.activate(user_language)
response = HttpResponse(...)
response.set_cookie(settings.LANGUAGE_COOKIE_NAME, user_language)

通常你想同时使用这两者:django.utils.translation.activate() 改变这个线程的语言,并设置cookie使此首选项在以后的请求中保持不变。

使用视图和模板外的翻译

虽然 Django 提供了一套丰富的i18n工具用于视图和模板,但它并不会限制 Django 特定代码的使用。Django 翻译机制可用于将任何文本翻译成 Django 支持的语言(当然,需要存在合适的翻译目录)。你可以加载翻译目录,激活它并翻译你选择的语言,但记住要切换回原始语言,因为激活语言目录是在每个线程基础上完成的,这样的改变将影响在同一线程中运行的代码。

例如:

from django.utils import translation


def welcome_translated(language):
    cur_language = translation.get_language()
    try:
        translation.activate(language)
        text = translation.gettext("welcome")
    finally:
        translation.activate(cur_language)
    return text

不论 LANGUAGE_CODE  和中间件设置的是什么,用 'de' 值调用的这个函数都会得到 "Willkommen"

django.utils.translation.get_language() 会返回当前进程所使用的语言,django.utils.translation.activate() 会为当前进程激活翻译目录,django.utils.translation.check_for_language() 会检查 Django 是否支持给定的语言。

为了协助编写更简洁的代码,这里还有一个上下文管理器 django.utils.translation.override() ,它会在输入时存储当前语言,并在退出时恢复它。有了它,上面的例子可变为:

from django.utils import translation


def welcome_translated(language):
    with translation.override(language):
        return translation.gettext("welcome")

实施说明

Django 翻译的特性

Django 翻译机制使用 Python 自带的标准 gettext 模块。如果你知道 gettext ,你可能注意到了 Django 翻译的这些特性:

  • 字符串域为 djangodjangojs 。这个字符串域用来区分在相同消息文件库(message-file library,通常为 /usr/share/locale/ )中存有数据的不同项目。django 域被用于 Python和模板翻译字符串,以及加载到全局翻译目录。djangojs 域只用于 JavaScript 的翻译目录来确保它们尽可能小。

  • Django 不会单独使用 xgettext 。它在 xgettextmsgfmt 周围使用 Python 装饰器。这主要是为了方便。

Django 如何发现语言偏好

一旦你准备好了翻译——或者,如果你想使用Django自带的翻译——那么你需要激活你的项目的翻译。

在后台,Django 有一个非常灵活的模型来决定使用哪一种语言——在全局使用,还是特定用户使用,还是二者都有。

在全局范围内设置语言偏好,需要设置 LANGUAGE_CODE 。Django 使用这个语言作为默认翻译 —— 如果通过 locale 中间件所使用的方法之一没有找到匹配的翻译,那么这是最后的尝试(见下文)。

如果你指向用你的母语运行 Django ,那么你需要做的就是设置 LANGUAGE_CODE 并确保存在相应的消息文件及其编译版本 (.mo) 。

如果你想让每个独立用户指定他们想要的语言,则还需要使用 LocaleMiddlewareLocaleMiddleware 可以基于请求中的数据启用语言选择。它为每个用户定制内容。

要使用 LocaleMiddleware,请在你的 MIDDLEWARE 配置中添加 'django.middleware.locale.LocaleMiddleware'。因为中间件的顺序很重要,所以要遵循这些准则。

  • 确保它是最先安装的中间件之一。

  • 它应该在 SessionMiddleware 之后,因为 LocaleMiddleware 使用会话数据。而且它应该在 CommonMiddleware 之前,因为 CommonMiddleware 需要激活的语言来解析请求的 URL。

  • 如果你使用 CacheMiddleware,把 LocaleMiddleware 放在它之后。

例如,你的 MIDDLEWARE 可能看起来像这样:

MIDDLEWARE = [
    "django.contrib.sessions.middleware.SessionMiddleware",
    "django.middleware.locale.LocaleMiddleware",
    "django.middleware.common.CommonMiddleware",
]

(关于中间件的更多信息,请参见 中间件文档。)

LocaleMiddleware 试图通过以下算法来确定用户的语言偏好。

  • 首先,它在请求的URL中寻找语言前缀。 只有当你在你的根 URLconf 中使用 i18n_patterns 函数时,才会这样做。参见 国际化:在 URL 模式中 了解更多关于语言前缀和如何国际化 URL 模式的信息。

  • 如果失败,它将查找 cookie。

    所使用的 cookie 的名称由 LANGUAGE_COOKIE_NAME 设定。(默认名称是 django_language)。

  • 如果失败了,它将查看 Accept-Language HTTP 头。这个头由你的浏览器发送,并告诉服务器你喜欢哪种语言,按优先级排序。Django 会尝试每一种语言,直到找到可用的翻译。

  • 如果不行,则使用全局 LANGUAGE_CODE 设置。

注意:

  • 在每一个地方,语言偏好都应该是标准的 语言格式,作为一个字符串。例如,巴西葡萄牙语是 pt-br

  • 如果基本语言可用,但指定的子语言不可用,Django 就使用基本语言。例如,如果用户指定了 de-at (奥地利德语),但 Django 只有 de 可用,Django 就使用 de

  • 只有 LANGUAGES 设置中列出的语言才能被选择。如果你想将语言选择限制在所提供的语言子集中(因为你的应用程序没有提供所有这些语言),请将 LANGUAGES 设置为语言列表。例如:

    LANGUAGES = [
        ("de", _("German")),
        ("en", _("English")),
    ]
    

    这个例子将可供自动选择的语言限制为德语和英语(以及任何子语言,如 de-chen-us)。

  • 如果你定义了一个自定义的 LANGUAGES 设置,就像上一弹所解释的那样,你可以将语言名称标记为翻译字符串——但要使用 gettext_lazy() 而不是 gettext() 来避免循环导入。

    以下是一个实例配置文件:

    from django.utils.translation import gettext_lazy as _
    
    LANGUAGES = [
        ("de", _("German")),
        ("en", _("English")),
    ]
    

一旦 LocaleMiddleware 确定了用户的偏好,它就会将这个偏好作为 request.LANGUAGE_CODE 提供给每一个 HttpRequest。在你的视图代码中可以随意读取这个值。下面是一个例子:

from django.http import HttpResponse


def hello_world(request, count):
    if request.LANGUAGE_CODE == "de-at":
        return HttpResponse("You prefer to read Austrian German.")
    else:
        return HttpResponse("You prefer to read another language.")

注意,静态(无中间件)翻译时,语言在 settings.LANGUAGE_CODE 中,而动态(中间件)翻译时,语言在 request.LANGUAGE_CODE 中。

Django 如何发现翻译

在运行时,Django 会在内存中建立一个统一的字面翻译目录。为了达到这个目的,它通过以下算法来查找翻译,关于它检查不同文件路径的顺序来加载编译的 消息文件.mo),以及同一字面意义的多个翻译的优先级。

  1. LOCALE_PATHS 中列出的目录优先级最高,先出现的比后出现的优先级高。

  2. 然后,它会在 INSTALLED_APPS 中列出的每个已安装的应用程序中寻找并使用是否存在 locale 目录。 先出现的比后出现的优先级高。

  3. 最后,Django 提供的位于 django/conf/locale 中的基本翻译将作为备用。

See also

JavaScript 资产中包含的字词的翻译是按照类似但不相同的算法来查找的。更多细节请参见 JavaScriptCatalog

如果你还设置了 :set:`FORMAT_MODULE_PATH`,你也可以把 自定义格式文件 放在 :set:`LOCALE_PATHS` 目录下。

在所有情况下,包含译文的目录名称应使用 locale name 符号来命名,如 dept_BRes_AR 等。未翻译的地域语言变体字符串使用通用语言的翻译。例如,未翻译的 pt_BR 字符串使用 pt 翻译。

这样,你可以编写包含自己翻译的应用程序,你可以在你的项目中覆盖基础翻译。或者,你可以用几个应用程序构建一个大项目,并将所有翻译放到一个大的通用消息文件中,具体到你正在编写的项目。这是你的选择。

所有的消息文件库的结构都是一样的。它们是:

  • 配置文件的 LOCALE_PATHS 中列出的所有路径都会被搜索到 <language>/LC_MESSAGES/django.(po|mo)

  • $APPPATH/locale/<language>/LC_MESSAGES/django.(po|mo)

  • $PYTHONPATH/django/conf/locale/<language>/LC_MESSAGES/django.(po|mo)

为了创建消息文件,你使用 django-admin makemessages 工具。并使用 django-admin compilemessages 来生成二进制的 .mo 文件,这些文件被 gettext 使用。

你也可以运行 django-admin compilemessages --settings=path.to.settings 使编译器处理你 :settings:`LOCALE_PATHS` 设置中的所有目录。

使用非英语基础语言

Django 一般假设可翻译项目中的原始字符串是用英语编写的。你可以选择其他语言,但你必须意识到某些限制。

  • gettext 只为原始信息提供两种复数形式,所以如果基础语言的复数规则与英语不同,你还需要提供基础语言的翻译,以包括所有的复数形式。

  • 当英文变体被激活而英文字符串缺失时,后备语言将不是项目的 LANGUAGE_CODE,而是原始字符串。例如,一个英语用户访问一个有 LANGUAGE_CODE 设置为西班牙语的网站,而原来的字符串是用俄语写的,他看到的将是俄语文本而不是西班牙语。

Back to Top