《Django By Example》第九章 上 中文 翻译 (个人学习,渣翻)

全文链接

第一章 创建一个blog应用
第二章 使用高级特性来增强你的blog
第三章 扩展你的blog应用
第四章上 创建一个社交网站
第四章下 创建一个社交网站
第五章 在你的网站中分享内容
第六章 跟踪用户动作
第七章 建立一个在线商店
第八章 管理付款和订单
第九章上 扩展你的商店
第九章下 扩展你的商店
第十章上 创建一个在线学习平台
第十章下 创建一个在线学习平台
第十一章 缓存内容
第十二章 构建一个API

书籍出处:https://www.packtpub.com/web-development/django-example
原作者:Antonio Melé

(审校@夜夜月:本章分上下两部分,这里是上半部。)

(译者@ucag 注:哈哈哈,第九章终于来啦。这是在线商店的最后一章,下一章将会开始一个新的项目。所以这一章的内容会偏难,最难的是推荐引擎的编写,其中的算法可能还需要各位好好的推敲,如果看了中文的看不懂,大家可以去看看英文原版的书以及相关的文档。最近我也在研究机器学习,有兴趣的大家可以一起交流哈~)

(审校@夜夜月:大家好,我是来打酱油的~,粗校,主要更正了一些错字和格式,精校正进行到四章样子。)

第九章(上)

扩展你的商店

在上一章中,你学习了如何把支付网关整合进你的商店。你处理了支付通知,学会了如何生成 CSV 和 PDF 文件。在这一章中,你会把优惠券添加进你的商店中。你将学到国际化(internationalization)和本地化(localization)是如何工作的,你还会创建一个推荐引擎。
在这一章中将会包含一下知识点:

  • 创建一个优惠券系统来应用折扣
  • 把国际化添加进你的项目中
  • 使用 Rosetta 来管理翻译
  • 使用 django-parler 来翻译模型(model)
  • 建立一个产品推荐引擎

创建一个优惠券系统

很多的在线商店会送出很多优惠券,这些优惠券可以在顾客的采购中兑换为相应的折扣。在线优惠券通常是由一串给顾客的代码构成,这串代码在一个特定的时间段内是有效的。这串代码可以被兑换一次或者多次。

我们将会为我们的商店创建一个优惠券系统。优惠券将会在顾客在某一个特定的时间段内输入时生效。优惠券没有任何兑换数的限制,他们也可用于购物车的总金额中。对于这个功能,我们将会创建一个模型(model)来储存优惠券代码,优惠券有效的时间段,以及折扣的力度。

myshop 项目内使用如下命令创建一个新的应用:

python manage.py startapp coupons

编辑 myshopsettings.py 文件,像下面这样把把应用添加到 INSTALLED_APPS 中:

INSTALLED_APPS = (
      # ...
      'coupons',
)

新的应用已经在我们的 Django 项目中激活了。

创建优惠券模型(model)

让我们开始创建 Coupon 模型(model)。编辑 coupons 应用中的 models.py 文件,添加以下代码:

from django.db import models
from django.core.validators import MinValueValidator,\
                                    MaxValueValidator
class Coupon(models.Model):
    code = models.CharField(max_length=50,
                            unique=True)
    valid_from = models.DateTimeField()
    valid_to = models.DateTimeField()
    discount = models.IntegerField(
                validators=[MinValueValidator(0),
                            MaxValueValidator(100)])
    active = models.BooleanField()
    
    def __str__(self):
        return self.code

我们将会用这个模型(model)来储存优惠券。 Coupon 模型(model)包含以下几个字段:

  • code:用户必须要输入的代码来将优惠券应用到他们购买的商品中
  • valid_from:表示优惠券会在何时生效的时间和日期值
  • valid_to:表示优惠券会在何时过期
  • discount:折扣率(这是一个百分比,所以它的值的范围是 0 到 1000)。我们使用验证器来限制接收的最小值和最大值
  • active:表示优惠券是否激活的布尔值

执行下面的命令来为 coupons 生成首次迁移:

python manage.py makemigrations

输出应该包含以下这几行:

Migrations for 'coupons':
    0001_initial.py:
        - Create model Coupon

之后我们执行下面的命令来应用迁移:

python manage.py migrate

你可以看见包含下面这一行的输出:

Applying coupons.0001_initial... OK

迁移现在已经被应用到了数据库中。让我们把 Coupon 模型(model)添加到管理站点。编辑 coupons 应用的 admin.py 文件,添加以下代码:

from django.contrib import admin
from .models improt Coupon

class CouponAdmin(admin.ModelAdmin):
    list_display = ['code', 'valid_from', 'valid_to',
                    'discount', 'active']
    list_filter = ['active', 'valid_from', 'valid_to']
    search_fields = ['code']
admin.site.register(Coupon, CouponAdmin)

Coupon 模型(model)现在已经注册进了管理站点中。确保你已经用命令 python manage.py runserver 打开了开发服务器。访问 http://127.0.0.1:8000/admin/coupons/add 。你可以看见下面的表单:

django-9-1

填写表单创建一个在当前日期有效的新优惠券,确保你点击了 Active 复选框,然后点击 Save按钮。

把应用优惠券到购物车中

我们可以保存新的优惠券以及检索目前的优惠券。现在我们需要一个方法来让顾客可以应用他们的优惠券到他们购买的产品中。花点时间来想想这个功能该如何实现。应用一张优惠券的流程如下:

1. 用户将产品添加进购物车
2. 用户在购物车详情页的表单中输入优惠代码
3. 当用户输入优惠代码然后提交表单时,我们查找一张和所给优惠代码相符的有效优惠券。我们必须检查用户输入的优惠券代码, `active` 属性为 `True` ,当前时间位于 `valid_from 和 `valid_to` 之间。
4. 如果查找到了相应的优惠券,我们把它保存在用户会话中,然后展示包含折扣了的购物车以及更新总价。
5. 当用户下单时,我们把优惠券保存到所给的订单中。

coupons 应用路径下创建一个新的文件,命名为 forms.py 文件,添加以下代码:

from django import forms

class CouponApplyForm(forms.Form):
    code = forms.CharField()

我们将会用这个表格来让用户输入优惠券代码。编辑 coupons 应用中的 views.py 文件,添加以下代码:

from django.shortcuts import render, redirect
from django.utils import timezone
from django.views.decorators.http import require_POST
from .models import Coupon
from .forms import CouponApplyForm

@require_POST
def coupon_apply(request):
    now = timezone.now()
    form = CouponApplyForm(request.POST)
    if form.is_valid():
        code = form.cleaned_data['code']
        try:
            coupon = Coupon.objects.get(code__iexact=code,
                                    valid_from__lte=now,
                                    valid_to__gte=now,
                                    active=True)
            request.session['coupon_id'] = coupon.id
        except Coupon.DoesNotExist:
            request.session['coupon_id'] = None
    return redirect('cart:cart_detail')

coupon_apply 视图(view)验证优惠券然后把它保存在用户会话(session)中。我们使用 require_POST 装饰器来限制这个视图(view)仅接受 POST 请求。在视图(view)中,我们执行了以下几个任务:

1.我们用上传的数据实例化了 `CouponApplyForm` 然后检查表单是否合法。
2. 如果表单是合法的,我们就从表单的 `cleaned_data` 字典中获取 `code` 。我们尝试用所给的代码检索 `Coupon` 对象。我们使用 `iexact` 字段来对照查找大小写不敏感的精确匹配项。优惠券在当前必须是激活的(`active=True`)以及必须在当前日期内是有效的。我们使用 Django 的 `timezone.now()` 函数来获得当前的时区识别时间和日期(time-zone-aware) 然后我们把它和 `valid_from` 和 `valid_to` 字段做比较,对这两个日期分别执行 `lte` (小于等于)运算和 `gte` (大于等于)运算来进行字段查找。
3. 我们在用户的会话中保存优惠券的 `id``。
4. 我们把用户重定向到 `cart_detail` URL 来展示应用了优惠券的购物车。

我们需要一个 coupon_apply 视图(view)的 URL 模式。在 coupon 应用路径下创建一个新的文件,命名为 urls.py ,添加以下代码:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^apply/$', views.coupon_apply, name='apply'),
]

然后,编辑 myshop 项目的主 urls.py 文件,引入 coupons 的 URL 模式:

url(r'^coupons/', include('coupons.urls', namespace='coupons')),

记得把这个放在 shop.urls 模式之前。

现在编辑 cart 应用的 cart.py,包含以下导入:

from coupons.models import Coupon

把下面这行代码添加进 Cart 类的 __init__() 方法中来从会话中初始化优惠券:

# store current applied coupon
self.coupon_id = self.session.get('coupon_id')

在这行代码中,我们尝试从当前会话中得到 coupon_id 会话键,然后把它保存在 Cart 对象中。把以下方法添加进 Cart 对象中:

@property
def coupon(self):
    if self.coupon_id:
        return Coupon.objects.get(id=self.coupon_id)
    return None

def get_discount(self):
    if self.coupon:
        return (self.coupon.discount / Decimal('100')) \
                * self.get_total_price()
    return Decimal('0')

def get_total_price_after_discount(self):
    return self.get_total_price() - self.get_discount()

下面是这几个方法:

  • coupon():我们定义这个方法作为 property 。如果购物车包含 coupon_id 函数,就会返回一个带有给定 idCoupon 对象
  • get_discount():如果购物车包含 coupon ,我们就检索它的折扣比率,然后返回购物车中被扣除折扣的总和。
  • get_total_price_after_discount():返回被减去折扣之后的总价。

Cart 类现在已经准备好处理应用于当前会话的优惠券了,然后将它应用于相应折扣中。

让我们在购物车详情视图(view)中引入优惠券系统。编辑 cart 应用的 views.py ,然后把下面这一行添加到顶部:

from coupons.forms import CouponApplyForm

之后,编辑 cart_detail 视图(view),然后添加新的表单:

def cart_detail(request):
    cart = Cart(request)
    for item in cart:
        item['update_quantity_form'] = CartAddProductForm(
                    initial={'quantity': item['quantity'],
                    'update': True})
    coupon_apply_form = CouponApplyForm()
    return render(request,
            'cart/detail.html',
            {'cart': cart,
            'coupon_apply_form': coupon_apply_form})

编辑 cart 应用的 acrt/detail.html 文件,找到下面这几行:

<tr class="total">
    <td>Total</td>
    <td colspan="4"></td>
    <td class="num">${{ cart.get_total_price }}</td>
</tr>

把它们换成以下几行:

{% if cart.coupon %}
<tr class="subtotal">
    <td>Subtotal</td>
    <td colspan="4"></td>
    <td class="num">${{ cart.get_total_price }}</td>
</tr>
<tr>
<td>
    "{{ cart.coupon.code }}" coupon
    ({{ cart.coupon.discount }}% off)
    </td>
    <td colspan="4"></td>
    <td class="num neg">
    - ${{ cart.get_discount|floatformat:"2" }}
    </td>
</tr>
{% endif %}
<tr class="total">
    <td>Total</td>
    <td colspan="4"></td>
    <td class="num">
    ${{ cart.get_total_price_after_discount|floatformat:"2" }}
    </td>
</tr>

这段代码用于展示可选优惠券以及折扣率。如果购物车中有优惠券,我们就在第一行展示购物车的总价作为 小计。然后我们在第二行展示当前可应用于购物车的优惠券。最后,我们调用 cart 对象的 get_total_price_after_discount() 方法来展示折扣了的总价格。

在同一个文件中,在 </table> 标签之后引入以下代码:

<p>Apply a coupon:</p>
<form action="{% url "coupons:apply" %}" method="post">
    {{ coupon_apply_form }}
    <input type="submit" value="Apply">
    {% csrf_token %}
</form>

我们将会展示一个表单来让用户输入优惠券代码,然后将它应用于当前的购物车当中。

访问 http://127.0.0.1:8000 ,向购物车当中添加一个商品,然后在表单中输入你创建的优惠代码来应用你的优惠券。你可以看到购物车像下面这样展示优惠券折扣:

django-9-2

让我们把优惠券添加到购物流程中的下一步。编辑 orders 应用的 orders/order/create.html 模板(template),找到下面这几行:

<ul>
{% for item in cart %}
<li>
    {{ item.quantity }}x {{ item.product.name }}
    <span>${{ item.total_price }}</span>
</li>
{% endfor %}
</ul>

把它替换为下面这几行:

<ul>
{% for item in cart %}
    <li>
    {{ item.quantity }}x {{ item.product.name }}
    <span>${{ item.total_price }}</span>
    </li>
{% endfor %}
{% if cart.coupon %}
<li>
"{{ cart.coupon.code }}" ({{ cart.coupon.discount }}% off)
<span>- ${{ cart.get_discount|floatformat:"2" }}</span>
</li>
{% endif %}
</ul>

订单汇总现在已经包含使用了的优惠券,如果有优惠券的话。现在找到下面这一行:

<p>Total: ${{ cart.get_total_price }}</p>

把他们换成以下一行:

<p>Total: ${{ cart.get_total_price_after_discount|floatformat:"2" }}</p>

这样做之后,总价也将会在减去优惠券折扣被计算出来。
访问 http://127.0.0.1:8000/orders/create/ 。你会看到包含使用了优惠券的订单小计:

django-9-3

用户现在可以在购物车当中使用优惠券了。尽管,当用户结账时,我们依然需要在订单中储存优惠券信息。

在订单中使用优惠券

我们会储存每张订单中使用的优惠券。首先,我们需要修改 Order 模型(model)来储存相关联的 Coupon 对象,如果有这个对象的话。

编辑 orders 应用的 models.py 文件,添加以下代码:

from decimal import Decimal
from django.core.validators import MinValueValidator, \
                                    MaxValueValidator
from coupons.models import Coupon

然后,把下列字段添加进 Order 模型(model)中:

coupon = models.ForeignKey(Coupon,
                            related_name='orders',
                            null=True,
                            blank=True)
discount = models.IntegerField(default=0,
                        validators=[MinValueValidator(0),
                                MaxValueValidator(100)])

这些字段让用户可以在订单中储存可选的优惠券信息,以及优惠券的相应折扣。折扣被存在关联的 Coupon 对象中,但是我们在 Order 模型(model)中引入它以便我们在优惠券被更改或者删除时好保存它。

因为 Order 模型(model)已经被改变了,我们需要创建迁移。执行下面的命令:

python manage.py makemigrations

你可以看到如下输出:

Migrations for 'orders':
    0002_auto_20150606_1735.py:
        - Add field coupon to order
        - Add field discount to order

用下面的命令来执行迁移:

python manage.py migrate orders

你必须要确保新的迁移已经被应用了。 Order 模型(model)的字段变更现在已经同步到了数据库中。

回到 models.py 文件中,按照如下更改 Order 模型(model)的 get_total_cost() 方法:

def get_total_cost(self):
    total_cost = sum(item.get_cost() for item in self.items.all())
    return total_cost - total_cost * \
        (self.discount / Decimal('100'))

Order 模型(model)的 get_total_cost() 现在已经把使用了的折扣包含在内了,如果有折扣的话。

编辑 orders 应用的 views.py 文件,更改 order_create 视图(view)以便在创建新的订单时保存相关联的优惠券和折扣。找到下面这一行:

order = form.save()

把它替换为下面这几行:

order = form.save(commit=False)
if cart.coupon:
    order.coupon = cart.coupon
    order.discount = cart.coupon.discount
order.save()

在新的代码中,我们使用 OrderCrateForm 表单的 save() 方法创建了一个 Order 对象。我们使用 commit=False 来避免将它保存在数据库中。如果购物车当中有优惠券,我们就会保存相关联的优惠券和折扣。然后才把 order 对象保存到数据库中。

确保用 python manage.py runserver 运行了开发服务器。使用 ./ngrok http:8000 命令来启动 Ngrok 。

在你的浏览器中打开 Ngrok 提供的 URL , 然后用用你创建的优惠券完成一次购物。当你完成一次成功的支付后,有可以访问 http://127.0.0.1:8000/admin/orders/order/ ,检查 order 对象是否包含了优惠券和折扣,如下:

django-9-4

你也可以更改管理界面的订单详情模板(template)和订单的 PDF 账单,以便用展示购物车的方式来展示使用了的优惠券。

下面。我们将会为我们的项目添加国际化(internationalization)。

添加国际化(internationalization)和本地化(localization)

Django 提供了完整的国际化和本地化的支持。这使得你可以把你的项目翻译为多种语言以及处理地区特性的 日期,时间,数字,和时区 的格式。让我们一起来搞清楚国际化和本地化的区别。国际化(通常简写为:i18n)是为让软件适应潜在的不同语言和多种语言的使用做的处理,这样它就不是以某种特定语言或某几种语言为硬编码的软件了。本地化(简写为:l10n)是对软件的翻译以及使软件适应某种特定语言的处理。Django 自身已经使用自带的国际化框架被翻译为了50多种语言。

使用 Django 国际化##

国际化框架让你可以容易的在 Python 代码和模板(template)中标记需要翻译的字符串。它依赖于 GNU 文本获取集来生成和管理消息文件。消息文件是一个表示一种语言的纯文本文件。它包含某一语言的一部分或者所有的翻译字符串。消息文件有 .po 扩展名。

一旦翻译完成,信息文件就会被编译一遍快速的连接到被翻译的字符串。编译后的翻译文件的扩展名为 .mo

国际化和本地化设置

Django 提供了几种国际化的设置。下面是几种最有关联的设置:

  • USE_I18N:布尔值。用于设定 Django 翻译系统是否启动。默认值为 True
    -USE_L10N:布尔值。表示本地格式化是否启动。当被激活式,本地格式化被用于展示日期和数字。默认为 False
  • USE_TZ:布尔值。用于指定日期和时间是否是时区别(timezone-aware)。
  • LANGUAGE_CODE:项目的默认语言。在标准的语言 ID 格式中,比如,en-us 是美式英语,en-gb 是英式英语。这个设置要 USE_I18NTrue 才能生效。你可以在这个网站找到一个合法的语言 ID 表:http://www.i18nguy.com/unicode/language-identifiers.html
  • LANGUAGES:包含项目可用语言的元组。它们由包含 语言代码语言名字 的双元组构成的。你可以在 django.conf.global_settions 里看到可用语言的列表。当你选择你的网站将会使用哪一种语言时,你就把 LANGUAGES 设置为那个列表的子列表。
  • LOCAL_PATHS:Django 寻找包含翻译的信息文件的路径列表。
  • TIME_ZONE:代表项目时区的字符串。当你使用 startproject 命令创建新项目时它被设置为 UTC 。你也可以把它设置为其他的时区,比如 Europe/Madrid

这是一些可用的国际化和本地化的设置。你可以在这个网站找到全部的(设置)列表: https://docs.djangoproject.com/en/1.8/ref/settings/#globalization-i18n-l10n

国际化管理命令

Django 使用 manage.py 或者 django-admin.py 工具包管理翻译,包含以下命令:

  • makemessages:运行于源代码树中,寻找所有被用于翻译的字符串,然后在 locale 路径中创建或更新 .po 信息文件。每一种语言创建一个单一的 .po 文件。
  • compilemessages: 把扩展名为 .po 的信息文件编译为用于检索翻译的 .mo 文件。

你需要文本获取工具集来创建,更新,以及编译信息文件。大部分的 Linux 发行版都包含文本获取工具集。如果你在使用 Mac OS X,用 Honebrew (http://brew.sh)是最简单的安装它的方法,使用以下命令 :brew install gettext。你或许也需要将它和命令行强制连接 brew link gettext --force 。对于 Windows ,安装步骤如下 : https://docs.djangoproject.com/en/1.8/topics/i18n/translation/#gettext-on-windows

怎么把翻译添加到 Django 项目中

让我们看看国际化项目的过程。我们需要像下面这样做:

1. 我们在 Python 代码和模板(template)中中标记需要翻译的字符串。
2. 我们运行 `makemessages` 命令来创建或者更新包含所有翻译字符串的信息文件。
3. 我们翻译包含在信息文件中的字符串,然后使用  `compilemessages` 编译他们。

Django 如何决定当前语言

Django 配备有一个基于请求数据的中间件,这个中间件用于决定当前语言是什么。这个中间件是位于 django.middleware.locale.localMiddlewareLocaleMiddleware ,它执行下面的任务:

1. 如果使用 `i18_patterns` —— 这是你使用的被翻译的 URL 模式,它在被请求的 URL 中寻找一个语言前缀来决定当前语言。
2. 如果没有找到语言前缀,就会在当前用户会话中寻找当前的 `LANGUAGE_SESSION_KEY` 。
3. 如果在会话中没有设置语言,就会寻找带有当前语言的 cookie 。这个 cookie 的定制名可在 `LANGUAGE_COOKIE_NAME` 中设置。默认地,这个 cookie 的名字是 `django_language` 。
4. 如果没有找到 cookie,就会在请求 HTTP 头中寻找 `Accept-Language` 。
5. 如果 `Accept-Language` 头中没有指定语言,Django 就使用在 `LANGUAGE_CODE` 设置中定义的语言。

默认的,Django 会使用 LANGUAGE_OCDE 中设置的语言,除非你正在使用 LocaleMiddleware 。上述操作进会在使用这个中间件时生效。

为我们的项目准备国际化

让我们的项目准备好使用不同的语言吧。我们将要为我们的商店创建英文版和西班牙语版。编辑项目中的 settings.py 文件,添加下列 LANGUAGES 设置。把它放在 LANGUAGE_OCDE 旁边:

LANGUAGES = (
        ('en', 'English'),
        ('es', 'Spanish'),
)

LANGUAGES 设置包含两个由语言代码和语言名的元组构成,比如 en-usen-gb ,或者一般的设置为 en 。这样设置之后,我们指定我们的应用只会对英语和西班牙语可用。如果我们不指定 LANGUAGES 设置,网站将会对所有 Django 的翻译语言有效。

确保你的 LANGUAGE_OCDE 像如下设置:

LANGUAGE_OCDE = 'en'

django.middleware.locale.LocaleMiddleware 添加到 MIDDLEWARE_CLASSES 设置中。确保这个设置在 SessionsMiddleware 之后,因为 LocaleMiddleware 需要使用会话数据。它同样必须放在 CommonMiddleware 之前,因为后者需要一种激活了的语言来解析请求 URL 。MIDDLEWARE_CLASSES 设置看起来应该如下:

MIDDLEWARE_CLASSES = (
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ...
)

中间件的顺序非常重要,因为每个中间件所依赖的数据可能是由其他中间件处理之后的才获得的。中间件按照 MIDDLEWARE_CLASSES 的顺序应用在每个请求上,以及反序应用于响应中。

在主项目路径下,在 manage.py 文件同级,创建以下文件结构:

locale/
    en/
    es/

locale 路径是放置应用信息文件的地方。再次编辑 settings.py ,然后添加以下设置:

LOCALE_PATHS = (
    os.path.join(BASE_DIR, 'locale/'),
)

LOCALE_PATHS 设置指定了 Django 寻找翻译文件的路径。第一个路径有最高优先权。

当你在你的项目路径下使用 makemessages 命令时,信息文件将会在 locale 路径下生成。尽管,对于包含 locale 路径的应用,信息文件就会保存在这个应用的 locale 路径中。

翻译 Python 代码

我们翻译在 Python 代码中的字母,你可以使用 django.utils.translation 中的 gettext() 函数来标记需要翻译的字符串。这个函数翻译信息然后返回一个字符串。约定俗成的用法是导入这个函数后把它命名为 _ (下划线)。

你可以在这个网站找到所有关于翻译的文档 : https://docs.djangoproject.com/en/1.8/topics/i18n/translation/

标准翻译

下面的代码展示了如何标记一个翻译字符串:

from django.utils.translation import gettext as _
output = _('Text to be translated.')

惰性翻译(Lazy translation)

Django 对所有的翻译函数引入了惰性(lazy)版,这些惰性函数都带有后缀 _lazy() 。当使用惰性函数时,字符串在值被连接时就会被翻译,而不是在被调用时翻译(这也是它为什么被惰性翻译的原因)。惰性函数迟早会派上用场,特别是当标记字符串在模型(model)加载的执行路径中时。

使用 gettext_lazy() 而不是 gettext() ,字符串就会在连接到值的时候被翻译而不会在函数调用的时候被翻译。Django 为所有的翻译都提供了惰性版本。

翻译引入的变量

标记的翻译字符串可以包含占位符来引入翻译中的变量。下面就是一个带有占位符的翻译字字符串的例子:

from django.utils.translation import gettext as _
month = _('April')
day = '14'
output = _('Today is %(month)s %(day)s') % {'month': month,
                                            'day': day}

通过使用占位符,你可以重新排序文字变量。举个例子,以前的英文版本是 'Today is April 14' ,但是西班牙的版本是这样的 'Hoy es 14 de Abril' 。当你的翻译字符串有多于一个参数时,我们总是使用字符串插值来代替位置插值

翻译中的复数形式

对于复数形式,你可以使用 gettext()gettext_lazy() 。这些函数基于一个可以表示对象数量的参数来翻译单数和复数形式。下面这个例子展示了如何使用它们:

output = ngettext('there is %(count)d product',
                'there are %(count)d products',
                count) % {'count': count}

现在你已经基本知道了如何在你的 Python 代码中翻译字符了,现在是把翻译应用到项目中的时候了。

翻译你的代码

编辑项目中的 settings.py ,导入 gettext_lazy() 函数,然后像下面这样修改 LANGUAGES 的设置:

from django.utils.translation import gettext_lazy as _

LANGUAGES = (
    ('en', _('English')),
    ('es', _('Spanish')),
)

我们使用 gettext_lazy() 而不是 gettext() 来避免循环导入,这样就可以在语言名被连接时就翻译它们。

在你的项目路径下执行下面的命令:

django-admin makemessages --all

你可以看到如下输出:

processing locale es
processing locale en

看下 locale/ 路径。你可以看到如下文件结构:

en/
    LC_MESSAGES/
        django.po
es/
    LC_MESSAGES/
        django.po

.po 文件已经为每一个语言创建好了。用文本编辑器打开 es/LC_MESSAGES/django.po 。在文件的末尾,你可以看到如下几行:

#: settings.py:104
msgid "English"
msgstr ""

#: settings.py:105
msgid "Spanish"
msgstr ""

每一个翻译字符串前都有一个显示该文件详情的注释以及它在哪一行被找到。每个翻译都包含两个字符串:

  • msgid:在源代码中的翻译字符串
  • msgstr:对应语言的翻译,默认为空。这是你输入所给字符串翻译的地方。

按照如下,根据所给的 msgidmsgtsr 中填写翻译:

#: settings.py:104
msgid "English"
msgstr "Inglés"

#: settings.py:105
msgid "Spanish"
msgstr "Español"

保存修改后的信息文件,打开 shell ,运行下面的命令:

django-admin compilemessages

如果一切顺利,你可以看到像如下的输出:

processing file django.po in myshop/locale/en/LC_MESSAGES
processing file django.po in myshop/locale/es/LC_MESSAGES

输出给出了被编译的信息文件的相关信息。再看一眼 locale 路径的 myshop 。你可以看到下面的文件:

en/
    LC_MESSAGES/
        django.mo
        django.po
es/
    LC_MESSAGES/
        django.mo
        django.po

你可以看到每个语言的 .mo 的编译文件已经生成了。

我们已经翻译了语言本身的名字。现在让我们翻译展示在网站中模型(model)字段的名字。编辑 orders 应用的 models.py ,为 Order 模型(model)添加翻译的被标记名:

from django.utils.translation import gettext_lazy as _

class Order(models.Model):
    first_name = models.CharField(_('first name'),
                                max_length=50)
    last_name = models.CharField(_('last name'),
                                max_length=50)
    email = models.EmailField(_('e-mail'),)
    address = models.CharField(_('address'),
                                max_length=250)
    postal_code = models.CharField(_('postal code'),
                                max_length=20)
    city = models.CharField(_('city'),
                        max_length=100)
#...

我们添加为当用户下一个新订单时展示的字段添加了名字,它们分别是 first_name ,last_name, email, address, postal_code ,city。记住,你也可以使用每个字段的 verbose_name 属性。

orders 应用路径内创建以下路径:

locale/
    en/
    es/

通过创建 locale 路径,这个应用的翻译字符串就会储存在这个路径下的信息文件里,而不是在主信息文件里。这样做之后,你就可以为每个应用生成独自的翻译文件。

在项目路径下打开 shell ,运行下面的命令:

django-admin makemessages --all

你可以看到如下输出:

processing locale es
processing locale en

用文本编辑器打开 es/LC_MESSAGES/django.po 文件。你将会看到每一个模型(model)的翻译字符串。为每一个所给的 msgid 字符串填写 msgstr 的翻译:

#: orders/models.py:10
msgid "first name"
msgstr "nombre"

#: orders/models.py:12
msgid "last name"
msgstr "apellidos"

#: orders/models.py:14
msgid "e-mail"
msgstr "e-mail"

#: orders/models.py:15
msgid "address"
msgstr "dirección"

#: orders/models.py:17
msgid "postal code"
msgstr "código postal"

#: orders/models.py:19
msgid "city"
msgstr "ciudad"

在你添加完翻译之后,保存文件。

在文本编辑器内,你可以使用 Poedit 来编辑翻译。 Poedit 是一个用来编辑翻译的软件,它使用 gettext 。它有 Linux ,Windows,Mac OS X 版,你可以在这个网站下载 Poedit : http://poedit.net/

让我们来翻译项目中的表单吧。 orders 应用的 OrderCrateForm 还没有被翻译,因为它是一个 ModelForm ,使用 Order 模型(model)的 verbose_name 属性作为每个字段的标签。我们将会翻译 cartcoupons 应用的表单。

编辑 cart 应用路径下的 forms.py 文件,给 CartAddProductFormquantity 字段添加一个 label 属性,然后按照如下标记这个需要翻译的字段:

from django import forms
from django.utils.translation import gettext_lazy as _

PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)]

class CartAddProductForm(forms.Form):
    quantity = forms.TypedChoiceField(
                        choices=PRODUCT_QUANTITY_CHOICES,
                        coerce=int,
                        label=_('Quantity'))
    update = forms.BooleanField(required=False,
                        initial=False,
                        widget=forms.HiddenInput)

编辑 coupons 应用的 forms.py ,按照如下翻译 CouponApplyForm

from django import forms
from django.utils.translation import gettext_lazy as _

class CouponApplyForm(forms.Form):
    code = forms.CharField(label=_('Coupon'))

翻译模板(templates)

Django 提供了 {% trans %}{% blocktrans %} 模板(template)标签来翻译模板(template)中的字符串。为了使用翻译模板(template)标签,你需要在你的模板(template)顶部添加 {% load i18n %} 来载入她们。

{% trans %}模板(template)标签

{% trans %}模板(template)标签让你可以标记需要翻译的字符串,常量,或者是参数。在内部,Django 对所给文本执行 gettext() 。这是如何标记模板(template)中的翻译字符串:

{% trans "Text to be translated" %}

你可使用 as 来储存你在模板(template)内使用的全局变量里的被翻译内容。下面这个例子保存了一个变量中叫做 greeting 的被翻译文本:

{% trans "Hello!" as greeting %}
<h1>{{ greeting }}</h1>

{% trans %} 标签对于简单的翻译字符串是很有用的,但是它不能包含变量的翻译内容。

{% blocktrans %}模板(template)标签

{% blocktrans %} 模板(template)标签让你可以标记含有占位符的变量和字符的内容。线面这个例子展示了如何使用 {% blocktrans %} 标签来标记包含一个 name 变量的翻译内容:

{% blocktrans %}Hello {{ name }}!{% endblocktrans %}

你可以用 with 来引入模板(template)描述,比如连接对象的属性或者应用模板(template)的变量过滤器。你必须为他们用占位符。你不能在 blocktrans 内连接任何描述或者对象属性。下面的例子展示了如何使用 with 来引入一个应用了 capfirst 过滤器的对象属性:

{% blocktrans with name=user.name|capfirst %}
Hello {{ name }}!
{% endblocktrans %}

当你的字符串中含有变量时使用 {% blocktrans %} 代替 {% trans %}

翻译商店模板(template)

编辑 shop 应用的 shop/base.html 。确保你已经在顶部载入了 i18n 标签,然后按照如下标记翻译字符串:

{% load i18n %}
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>
{% block title %}{% trans "My shop" %}{% endblock %}
</title>
<link href="{% static "css/base.css" %}" rel="stylesheet">
</head>
<body>
<div id="header">
<a href="/" class="logo">{% trans "My shop" %}</a>
</div>
<div id="subheader">
<div class="cart">
{% with total_items=cart|length %}
{% if cart|length > 0 %}
{% trans "Your cart" %}:
<a href="{% url "cart:cart_detail" %}">
{% blocktrans with total_items_plural=total_
items|pluralize
total_price=cart.get_total_price %}
{{ total_items }} item{{ total_items_plural }},
${{ total_price }}
{% endblocktrans %}
</a>
{% else %}
{% trans "Your cart is empty." %}
{% endif %}
{% endwith %}
</div>
</div>
<div id="content">
{% block content %}
{% endblock %}
</div>
</body>
</html>

注意展示在购物车小计的 {% blocktrans %} 标签。购物车小计在之前是这样的:

{{ total_items }} item{{ total_items|pluralize }},
${{ cart.get_total_price }}

我们使用 {% blocktrans with ... %} 来使用 total_ items|pluralize (模板(template)标签生效的地方)和 cart_total_price (连接对象方法的地方)的占位符:

{% blocktrans with total_items_plural=total_items|pluralize
total_price=cart.get_total_price %}
{{ total_items }} item{{ total_items_plural }},
${{ total_price }}
{% endblocktrans %}

下面,编辑 shop 应用的 shop/product/detail.html 模板(template)然后在顶部载入 i18n 标签,但是它必须位于 {% extends %} 标签的下面:

{% load i18n %}

找到下面这一行:

<input type="submit" value="Add to cart">

把它替换为下面这一行:

<input type="submit" value="{% trans "Add to cart" %}">

现在翻译 orders 应用模板(template)。编辑 orders 应用的 orders/order/create.html 模板(template),然后标记翻译文本:

{% extends "shop/base.html" %}
{% load i18n %}
{% block title %}
{% trans "Checkout" %}
{% endblock %}
{% block content %}
<h1>{% trans "Checkout" %}</h1>
<div class="order-info">
<h3>{% trans "Your order" %}</h3>
<ul>
{% for item in cart %}
<li>
{{ item.quantity }}x {{ item.product.name }}
<span>${{ item.total_price }}</span>
</li>
{% endfor %}
{% if cart.coupon %}
<li>
{% blocktrans with code=cart.coupon.code
discount=cart.coupon.discount %}
"{{ code }}" ({{ discount }}% off)
{% endblocktrans %}
<span>- ${{ cart.get_discount|floatformat:"2" }}</span>
</li>
{% endif %}
</ul>
<p>{% trans "Total" %}: ${{
cart.get_total_price_after_discount|floatformat:"2" }}</p>
</div>
<form action="." method="post" class="order-form">
{{ form.as_p }}
<p><input type="submit" value="{% trans "Place order" %}"></p>
{% csrf_token %}
</form>
{% endblock %}

看看本章中下列文件中的代码,看看字符串是如何被标记的:

  • shop 应用:shop/product/list.html
  • orders 应用:orders/order/created.html
  • cart 应用:cart/detail.html

让位我们更新信息文件来引入新的翻译字符串。打开 shell ,运行下面的命令:

django-admin makemessages --all

.po 文件已经在 myshop 项目的 locale 路径下,你将看到 orders 应用现在已经包含我们标记的所有需要翻译的字符串。

编辑项目和orderes 应用中的 .po 翻译文件,然后引入西班牙语翻译。你可以参考本章中翻译了的 .po 文件:

在项目路径下打开 shell ,然后运行下面的命令:

cd orders/
django-admin compilemessages
cd ../

我们已经编译了 orders 应用的翻译。

运行下面的命令,这样应用中不包含 locale 路径的翻译就被包含进了项目的信息文件中:

django-admin compilemessages

(审校@夜夜月:因为第九章过长,所以分成上下两章,目前上半章结束。)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容