扩展商店功能

Posted 一个处女座的测试

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了扩展商店功能相关的知识,希望对你有一定的参考价值。

第九章 扩展商店功能

在上一章里,为电商站点集成了支付功能,然后可以生成PDF发票发送给用户。在本章,我们将为商店添加优惠码功能。此外,还会学习国际化和本地化的设置和建立一个推荐商品的系统。

本章涵盖如下要点:

  • 建立一个优惠券系统,可以实现折扣功能

  • 给项目增加国际化功能

  • 使用Rosetta来管理翻译

  • 使用Django-parler翻译模型

  • 建立商品推荐系统

1优惠码系统

很多电商网站,会向用户发送电子优惠码,以便用户在购买时使用,以折扣价进行结算。一个在线优惠码通常是一个字符串,然后还规定了有效期限,一次性有效或者可以反复使用。

我们将为站点添加优惠码功能。我们的优惠码带有有效期,但是不限制使用次数,输入之后,就会影响用户购物车中的总价。为了实现这个需求,需要建立一个数据模型来存储优惠码,有效期和对应的折扣比例。

为myshop项目创建新的应用coupons:

Copypython manage.py startapp coupons

然后在settings.py内激活该应用:

CopyINSTALLED_APPS = [
    # ...'coupons.apps.CouponsConfig',
]

1.1创建优惠码数据模型

编辑coupons应用的models.py文件,创建一个Coupon模型:

Copyfrom django.db import models
from django.core.validators import MinValueValidator, MaxValueValidator

classCoupon(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

这是用来存储优惠码的模型,Coupon模型包含以下字段:

  • code:用于存放码的字符串

  • valid_from:优惠码有效期的开始时间。

  • valid_to:优惠码有效期的结束时间。

  • discount:该券对应的折扣,是一个百分比,所以取值为0-100,我们使用了内置验证器控制该字段的取值范围。

  • active:表示该码是否有效

之后执行数据迁移程序。然后将Coupon模型加入到管理后台,编辑coupons应用的admin.py文件:

Copyfrom django.contrib import admin
from .models import Coupon

classCouponAdmin(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)

现在启动站点,到http://127.0.0.1:8000/admin/coupons/coupon/add/查看Coupon模型:

输入一个优惠码记录,有效期设置为当前日期,不要忘记勾上Active然后点击SAVE按钮。

1.2为购物车增加优惠码功能

创建数据模型之后,可以查询和获得优惠码对象。现在我们必须增添使用户可以输入优惠码从而获得折扣价的功能。这个功能将按照如下逻辑进行操作:

  1. 用户添加商品到购物车

  1. 用户能通过购物车详情页面的表单输入一个优惠码

  1. 输入优惠码并提交表单之后,需要来判断该码是否在数据库中存在、当前时间是否在valid_from和valid_to有效时间之间、active属性是否为True。

  1. 如果优惠码通过上述检查,将优惠码的信息保存在session中,用折扣重新计算价格并更新购物车中的商品价格

  1. 用户提交订单时,将优惠码保存在订单对象中。

在coupons应用里建立forms.py文件,添加下列代码:

Copyfrom django import forms

classCouponApplyForm(forms.Form):
    code = forms.CharField()

这个表单用于用户输入优惠码。然后来编辑coupons应用的views.py文件:

Copyfrom 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_POSTdefcoupon_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.idexcept Coupon.DoesNotExist:
            request.session['coupon_id'] = Nonereturn redirect('cart:cart_detail')

这个coupon_apply视图验证优惠码并将其存储在session中,使用了@require_POST装饰器令该视图仅接受POST请求。这个视图的业务逻辑如下:

  1. 使用请求中的数据初始化CouponApplyForm

  1. 如果表单通过验证,从表单的cleaned_data获取code,然后使用code查询数据库得到coupon对象,这里使用了过滤参数iexact,进行完全匹配;使用active=True过滤出有效的优惠码;使用timezone.now()获取当前时间,valid_from和valid_to分别采用lte(小于等于)和gte(大于等于)过滤查询以保证当前时间位于有效期内。

  1. 将优惠码ID存入当前用户的session。

  1. 重定向到cart_detail URL对应的购物车详情页,以显示应用了优惠码之后的金额。

需要为coupon_apply视图配置URL,在coupons应用中建立urls.py文件,添加下列代码:

Copyfrom django.urls import path
from . import views

app_name = 'coupons'
urlpatterns = [
    path('apply/', views.coupon_apply, name='apply'),
]

然后编辑项目的根路由,增加一行:

Copyurlpatterns = [
    # ...
    path('coupons/', include('coupons.urls', namespace='coupons')),
    path('', include('shop.urls', namespace='shop')),
]

依然记得要把这一行放在shop.urls上方。

编辑cart应用中的cart.py文件,添加下列导入:

Copyfrom coupons.models import Coupon

然后在cart类的__init__()方法的最后添加从session中获得优惠码ID的语句:

CopyclassCart(object):
    def__init__(self, request):
        # ...# store current applied coupon
        self.coupon_id = self.session.get('coupon_id')

在Cart类中,我们需要通过coupon_id获取优惠码信息并将其保存在Cart对象内,为Cart类添加如下方法:

CopyclassCart(object):
    # ...    @propertydefcoupon(self):
        if self.coupon_id:
            return Coupon.objects.get(id=self.coupon_id)
        returnNonedefget_discount(self):
        if self.coupon:
            return (self.coupon.discount / Decimal('100')) * self.get_total_price()
        return Decimal('0')

    defget_total_price_after_diccount(self):
        return self.get_total_price() - self.get_discount()

这些方法解释如下:

  • coupon():我们使用@property将该方法定义为属性,如果购物车包含一个coupon_id属性,会返回该id对应的Coupon对象

  • get_discount():如果包含优惠码id,计算折扣价格,否则返回0。

  • get_total_price_after_discount():返回总价减去折扣价之后的折扣后价格。

现在Cart类就具备了根据优惠码计算折扣价的功能。

现在还需要修改购物车详情视图函数,以便在页面中应用表单和展示折扣金额,修改cart应用的views.py文件,增加导入代码:

Copyfrom coupons.forms import CouponApplyForm

然后修改cart_detail视图,添加表单:

Copydefcart_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应用的购物车模板cart/detail.html,找到如下几行:

Copy<trclass="total"><td>total</td><tdcolspan="4"></td><tdclass="num">$ cart.get_total_price </td></tr>

替换成如下代码:

Copy% if cart.coupon %
    <trclass="subtotal"><td>Subtotal</td><tdcolspan="4"></td><tdclass="num">$ cart.get_total_price_after_diccount </td></tr><tr><td>" cart.coupon.code " coupon ( cart.coupon.discount % off)</td><tdcolspan="4"></td><tdclass="num neg">- $ cart.get_discount|floatformat:"2" </td></tr>
% endif %

    <trclass="total"><td>Total</td><tdcolspan="4"></td><tdclass="num">$ cart.get_total_price_after_diccount|floatformat:"2" </td></tr>

这是新的购物车模板。如果包含一个优惠券,就展示一行购物车总价,再展示一行优惠券信息,最后通过get_total_price_after_discount()展示折扣后价格。

在同一个文件内,在</table>后增加下列代码:

Copy# 在紧挨着</table>标签之后插入: #
<p>Apply a coupon:</p>
<form action="% url 'coupons:apply' %" method="post">
     coupon_apply_form 
    <inputtype="submit" value="Apply">
    % csrf_token %
</form>

上边这段代码展示输入优惠码的表单。

在浏览器中打开http://127.0.0.1:8000/,向购物车内加入一些商品,然后进入购物车页面输入优惠码并提交,可以看到如下所示:

之后来修改订单模板orders/order/create.html,在其中找到如下部分:

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

替换成:

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

如果有优惠码,现在的订单页面就展示优惠码信息了。继续找到下边这行:

Copy<p>Total: $ cart.get_total_price </p>

替换成:

Copy<p>Total: $ cart.get_total_price_after_diccount|floatformat:"2" </p>

这样总价也变成了折扣后价格。

在浏览器中打开http://127.0.0.1:8000/,添加商品到购物车然后生成订单,可以看到订单页面的价格现在是折扣后的价格了:

1.3在订单中记录优惠码信息

像之前说的,我们需要将优惠码信息保存至order对象中,为此需要修改Order模型。编辑

编辑orders应用的models.py文件,增加导入部分的代码:

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

然后为Order模型增加下列字段:

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

这两个字段用于存储优惠码信息。虽然折扣信息保存在Coupon对象中,但这里还是用discount字段保存了当前的折扣,以免未来优惠码折扣发生变化。为coupon字段设置了on_delete=models.SET_NULL,优惠码删除时,该外键字段会变成空值。

增加好字段后数据迁移程序。回到models.py文件,需要修改Order类中的get_total_cost()方法:

CopyclassOrder(models.Model):
    # ...defget_total_cost(self):
        total_cost = sum(item.get_cost() for item in self.items.all())
        return total_cost - total_cost * (self.discount / Decimal('100'))

修改后的get_total_cost()方法会把折扣也考虑进去。之后还需要修改orders应用里的views.py文件中的order_create视图,以便在生成订单的时候,存储这两个新增的字段。找到下边这行:

Copyorder = form.save()

将其替换成如下代码:

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

在修改后代码中,通过调用OrderCreateForm表单对象的save()方法,创建一个order对象,使用commit=False暂不存入数据库。如果购物车对象中有折扣信息,就保存折扣信息。然后将order对象存入数据库。

启动站点,在浏览器中访问http://127.0.0.1:8000/,使用一个自己创建的优惠码,在完成购买之后,可以到http://127.0.0.1:8000/admin/orders/order/>查看包含优惠码和折扣信息的订单:

还可以修改管理后台的订单详情页和和PDF发票,以使其包含优惠码和折扣信息。下边我们将为站点增加国际化功能。

译者注:这里有一个问题:用户提交了订单并清空购物车后,如果再向购物车内添加内容,再次进入购物车详情页面可以发现自动使用了上次使用的优惠券。此种情况的原因是作者把优惠券信息附加到了session上,在提交订单的时候没有清除。cart对象实例化的时候又取到了相同的优惠券信息。所以需要对程序进行一下改进。

修改orders应用的order_create视图,在生成OrderItem并清空购物车的代码下增加一行:

Copydeforder_create(request):
    cart = Cart(request)
    if request.method == "POST":
        form = OrderCreateForm(request.POST)
        # 表单验证通过就对购物车内每一条记录生成OrderItem中对应的一条记录if form.is_valid():
            order = form.save(commit=False)
            if cart.coupon:
                order.coupon = cart.coupon
                order.discount = cart.coupon.discount
            order.save()
            for item in cart:
                OrderItem.objects.create(order=order, product=item['product'], price=item['price'],
                                         quantity=item['quantity'])
            # 成功生成OrderItem之后清除购物车
            cart.clear()

            # 清除优惠券信息
            request.session['coupon_id'] = None# 成功完成订单后调用异步任务发送邮件
            order_created.delay(order.id)
            # 在session中加入订单id
            request.session['order_id'] = order.id# 重定向到支付页面return redirect(reverse('payment:process'))

    else:
        form = OrderCreateForm()
    return render(request, 'orders/order/create.html', 'cart': cart, 'form': form)

2国际化与本地化

Django对于国际化和本地化提供了完整的支持,允许开发者将站点内容翻译成多种语言,而且可以处理本地化的时间日期数字和时区格式等本地化的显示内容。在开始之前,先需要区分一下国际化和本地化两个概念。国际化和本地化都是一种软件开发过程。国际化(Internationalization,通常缩写为i18n),是指一个软件可以被不同的国家和地区使用,而不会局限于某种语言。本地化(Localization,缩写为l10n)是指对国际化的软件将其进行翻译或者其他本地化适配,使之变成适合某一个国家或地区使用的软件的过程。Django通过自身的国际化框架,可以支持超过50种语言。

2.1国际化与本地化设置

Django的国际化框架可以让开发者很方便的在Python代码和模板中标注需要翻译的字符串,这个框架依赖于GNU gettext开源软件来生成和管理消息文件(message file)。消息文件是一个纯文本文件,代表一种语言的翻译,存放着在站点应用中找到的部分或者所有需要翻译的字符串以及对应的某种语言的翻译,就像一个字典一样。消息文件的后缀名是.po。

一旦完成翻译,可以把消息文件编译,以快速访问翻译内容,编译后的消息文件的后缀名是.mo。

2.1.1国际化与本地化设置

Django提供了一些国际化和本地化的设置,下边一些设置是最重要的:

  • USE_I18N:布尔值,是否启用国际化功能,默认为True

  • USE_L10N:布尔值,设置本地化功能是否启用,设置为True时,数字和日期将采用本地化显示。默认为False

  • USE_TZ:布尔值,指定时间是否根据时区进行调整,当使用startproject创建项目时,默认为True

  • LANGUAGE_CODE:项目的默认语言代码,采用标准的语言代码格式,例如'en-us'表示美国英语,'en-gb'表示英国英语。这个设置需要USE_I18N设置为True才会生效。在http://www.i18nguy.com/unicode/language-identifiers.html可以找到语言代码清单。

  • LANGUAGES:一个包含项目所有可用语言的元组,其中每个元素是语言代码和语言名称构成的二元组。可以在django.conf.global_settings查看所有可用的语言。这个属性可设置的值必须是django.conf.global_settings中列出的值。

  • LOCALE_PATHS:一个目录列表,目录内存放项目的翻译文件。

  • TIME_ZONE:字符串,代表项目所采用的时区。如果使用startproject启动项目,该值被设置为'UTC'。可以按照实际情况将其设置为具体时区,如'Europe/Madrid'。中国的时区是'Asia/Shanghai',大小写敏感。

以上是常用的国际化和本地化设置,完整设置请参见https://docs.djangoproject.com/en/2.1/ref/settings/#globalization-i18n-l10n。

2.1.2国际化和本地化管理命令

Django包含了用于管理翻译的命令如下:

  • makemessages:运行该命令,会找到项目中所有标注要翻译的字符串,建立或者更新locale目录下的.po文件,每种语言会生成单独的.po文件。

  • compilemessages:编译所有的.po文件为.mo文件。

需要使用GNU gettext工具来执行上述过程,大部分linux发行版自带有该工具。如果在使用mac OSX,可以通过 http://brew.sh/ 使用命令brew install gettext来安装,之后使用brew link gettext --force强制链接。对于Windows下的安装,参考https://docs.djangoproject.com/en/2.0/topics/i18n/translation/#gettext-on-windows中的步骤。

2.1.3如何为项目增加翻译文件

先来看一下增加翻译需要进行的流程:

  1. 在Python代码和模板中标注出需要翻译的字符串

  1. 运行makemessages命令建立消息文件

  1. 在消息文件中将字符串翻译成另外一种语言,然后运行compilemessages命令编译消息文件

2.1.4Django如何确定当前语言

Django使用中间件django.middleware.locale.LocaleMiddleware来检查HTTP请求中所使用的本地语言。这个中间件做的工作如下:

  1. 如果使用i18_patterns(django特殊的一种URL方式,里边包含语言前缀),中间件会在请求的URL中寻找特定语言的前缀

  1. 如果在URL中没有发现语言前缀,会在session中寻找一个键LANGUAGE_SESSION_KEY

  1. 如果session中没有该键,会在cookie中寻找一个键。可以通过LANGUAGE_COOKIE_NAME自定义该cookie的名称,默认是django_language

  1. 如果cookie中未找到,找HTTP请求头的Accept-Language键

  1. 如果Accept-Language头部信息未指定具体语言,则使用LANGUAGE_CODE设置

注意这个过程只有在开启了该中间件的时候才会得到完整执行,如果未开启中间件,Django直接使用LANGUAGE_CODE中的设置。

2.2为项目使用国际化进行准备

我们准备为电商网站增添各种语言的支持,增添英语和西班牙语的支持。编辑settings.py文件,加入LANGUAGES设置,放在LANGUAGE_CODE的旁边:

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

LANGUAGES设置包含两个语言代码和名称组成的元组。语言代码可以指定具体语言如en-us或en-gb,也可以更模糊,如en。通过这个设置,我们定义了我们的网站仅支持英语和西班牙语。如果不定义LANGUAGES设置,默认支持所有django支持的语言。

设置LANGUAGE_CODE为如下:

CopyLANGUAGE_CODE = 'en'

添加django.middleware.locale.LocaleMiddleware到settings.py的中间件设置中,位置在SessionMiddleware中间件之后,CommonMiddleware中间件之前,因为LocaleMiddleware需要使用session,而CommonMiddleware需要一种可用语言来解析URL,MIDDLEWARE设置成如下:

CopyMIDDLEWARE = [
    'django.middleware.security.SecurityMiddleware',
    'django.contrib.sessions.middleware.SessionMiddleware',
    'django.middleware.locale.LocaleMiddleware',
    'django.middleware.common.CommonMiddleware',
    # ...
]

django中间件设置的顺序很重要,中间件会在请求上附加额外的数据,某个中间件会依赖于另外一个中间件附加的数据才能正常工作。

在manage.py文件所在的项目根目录下创建如下目录:

Copylocale/
    en/
    es/

locale目录是用来存放消息文件的目录,编辑settings.py文件加入如下设置:

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

LOCALE_PATH指定了Django寻找消息文件的路径,可以是一系列路径,最上边的路径优先级最高。

当使用makemessages命令的时候,消息文件会在我们创建的locale/目录中创建,如果某个应用也有locale/目录,那个应用中的翻译内容会优先在那个应用的目录中创建。

2.3翻译Python代码中的字符串

为了翻译Python代码中的字符串字面量,需要使用django.utils.translation模块中的gettext()方法来标注字符串。这个方法返回翻译后的字符串,通常做法是导入该方法然后命名为一个下划线"_"。可以在https://docs.djangoproject.com/en/2.0/topics/i18n/translation/查看文档。

2.3.1标记字符串

标记字符串的方法如下:

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

2.3.2惰性翻译

Django对于所有的翻译函数都有惰性版本,后缀为_lazy()。使用惰性翻译函数的时候,字符串只有被访问的时候才会进行翻译,而不是在翻译函数调用的时候。当字符串位于模块加载的时候才生成的路径中时候特别有效。

使用gettext_lazy()代替gettext()方法,只有在该字符串被访问的时候才会进行翻译,所有的翻译函数都有惰性版本。。

2.3.3包含变量的翻译

被标注的字符串中还可以带有占位符,以下是一个占位符的例子:

Copyfrom 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"。当需要翻译的文本中存在变量的时候,推荐使用占位符。

2.3.4复数的翻译

对于复数形式的翻译,可以采用ngettext()和ngettext_lazy()。这两个函数根据对象的数量来翻译单数或者复数。使用例子如下:

Copyoutput = ngettext('there is %(count)d product', 'there are %(count)d products', count) % 'count': count

现在我们了解了Python中翻译字面量的知识,可以来为我们的项目添加翻译功能了。

2.3.5为项目翻译Python字符串字面量

编辑setttings.py,导入gettext_lazy(),然后修改LANGUAGES设置:

Copyfrom django.utils.translation import gettext_lazy as _

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

这里导入了gettext_lazy()并使用了别名"_"来避免重复导入。将显示的名称也进行了翻译,这样对于不同的语言的人来说,可以看懂并选择他自己的语言。

然后打开系统命令行窗口,输入如下命令:

Copydjango-admin makemessages --all

可以看到如下输出:

Copyprocessing locale en
processing locale es

然后查看项目的locale目录,可以看到如下文件和目录结构:

Copyen/
    LC_MESSAGES/
        django.po
es/
    LC_MESSAGES/
        django.po

每个语言都生成了一个.po消息文件,使用文本编辑器打开es/LC_MESSAGES/django.po文件,在末尾可以看到如下内容:

Copy#: .\\myshop\\settings.py:107
msgid "English"
msgstr ""#: .\\myshop\\settings.py:108
msgid "Spanish"
msgstr ""

每一部分的第一行表示在那个文件的第几行发现了需翻译的内容,每个翻译包含两个字符串:

  • msgid:源代码中的字符串

  • msgstr:被翻译成的字符串,默认为空,需要手工添加。

添加好翻译之后的文件如下:

Copy#: myshop/settings.py:117
msgid "English"
msgstr "Inglés"#: myshop/settings.py:118
msgid "Spanish"
msgstr "Español"

保存这个文件,之后执行命令编译消息文件:

Copydjango-admin compilemessages

可以看到输出如下:

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

这表明已经编译了翻译文件,此时查看locale目录,其结构如下:

Copyen/
    LC_MESSAGES/
        django.mo
        django.po
es/
    LC_MESSAGES/
        django.mo
        django.po

可以看到每种语言都生成了.mo文件。

我们已经翻译好了语言名称本身。现在我们来试着翻译一下Order模型的所有字段,修改orders应用的models.py文件:

Copyfrom django.utils.translation import gettext_lazy as _

classOrder(models.Model):
    first_name = models.CharField(_('frist 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)
    ......

我们为每个显示出来的字段标记了翻译内容,也可以使用verbose_name属性来命名字段。在orders应用中建立如下目录:

Copylocale/
    en/
    es/

通过创建locale目录,当前应用下的翻译内容会优先保存到这个目录中,而不是保存在项目根目录下的locale目录中。这样就可以为每个应用配置独立的翻译文件。

在系统命令行中执行:

Copydjango-admin makemessages --all

输出为:

Copyprocessing locale es
processing locale en

使用文本编辑器打开locale/es/LC_MESSAGES/django.po,可以看到Order模型的字段翻译,在msgstr中为对应的msgid字符串加上西班牙语的翻译:

Copy#: orders/models.py:10
msgid "first name"
msgstr "nombre"#: orders/models.py:11
msgid "last name"
msgstr "apellidos"#: orders/models.py:12
msgid "e-mail"
msgstr "e-mail"#: orders/models.py:13
msgid "address"
msgstr "dirección"#: orders/models.py:14
msgid "postal code"
msgstr "código postal"#: orders/models.py:15
msgid "city"
msgstr "ciudad"

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

除了常用的文本编辑软件,还可以考虑使用Poedit编辑翻译内容,该软件同样依赖gettext,支持Linux,Windows和macOS X。可以在https://poedit.net/下载该软件。

下边来翻译项目使用的表单。OrderCreateForm这个表单类无需翻译,因为它会自动使用Order类中我们刚刚翻译的verbose_name。现在我们去翻译cart和coupons应用中的内容。

在cart应用的forms.py文件中,导入翻译函数,为CartAddProductForm类的quantity字段增加一个参数label,代码如下:

Copyfrom django import forms
from django.utils.translation import gettext_lazy as _
PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i inrange(1, 21)]

classCartAddProductForm(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类增加翻译:

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

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

我们为code字段增加了一个label标签用于展示翻译后的字段名称。

2.4翻译模板

Django为翻译模板内容提供了% trans %和% blocktrans %两个模板标签用于翻译内容,如果要启用这两个标签,需要在模板顶部加入% load i18n %。

2.4.1使用% trans %模板标签

% trans %标签用来标记一个字符串,常量或者变量用于翻译。Django内部也是该文本执行gettext()等翻译函数。标记字符串的例子是:

Copy% trans "Text to be translated" %

也可以像其他标签变量一样,使用as 将 翻译后的结果放入一个变量中,在其他地方使用。下面的例子使用了一个变量greeting:

Copy% trans "Hello!" as greeting %
<h1> greeting </h1>

这个标签用于比较简单的翻译,但不能用于带占位符的文字翻译。

2.4.2使用% blocktrans %模板标签

% blocktrans %标签可以标记包含常量和占位符的内容用于翻译,下边的例子展示了使用一个name变量的翻译:

Copy% blocktrans %Hello  name !% endblocktrans %

可以使用with,将具体的表达式设置为变量的值,此时在blocktrans块内部不能够再继续访问表达式和对象的属性,下面是一个使用了capfirst装饰器的例子:

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

如果翻译内容中包含变量,使用% blocktrans %代替% trans %。

2.4.3翻译商店模板

编辑shop应用的base.html,在其顶部加入i18n标签,然后标注如下要翻译的部分:

Copy% load i18n %
% load static %
<!DOCTYPE html><html><head><metacharset="utf-8"/><title>% block title %% trans "My shop" %% endblock %</title><linkhref="% static "css/base2.css" %" rel="stylesheet"></head><body><divid="header"><ahref="/"class="logo">% trans "My shop" %</a></div><divid="subheader"><divclass="cart">
        % with total_items=mycart|length %
            % if mycart|length > 0 %
                % trans "Your cart" %:
                <ahref="% url 'cart:cart_detail' %">
                    % blocktrans with total_items_plural=total_items|pluralize total_price=cart.get_total_price %
                     total_items  items total_items_plural , $ total_price 
                    % endblocktrans %
                </a>
            % else %
                % trans "Your cart is empty." %
            % endif %
        % endwith %
    </div></div><divid="content">
    % block content %
    % endblock %
</div></body></html>

注意% blocktrans %展示购物车总价部分的方法,在原来的模板中,我们使用了:

Copy total_items  item total_items|pluralize ,
$ cart.get_total_price 

现在改用% blocktrans with ... %来为total_items|pluralize(使用了过滤器)和cart.get_total_price(访问对象的方法)创建占位符:

编辑shop应用的shop/product/detail.html,紧接着% extends %标签导入i18n标签:

Copy% load i18n %

之后找到下边这一行:

Copy<inputtype="submit"value="Add to cart">

将其替换成:

Copy<inputtype="submit"value="% trans "Addtocart" %">

现在来翻译orders应用,编辑orders/order/create.html,标记如下翻译内容:

Copy% extends 'shop/base.html' %
% load i18n %
% block title %
    % trans "Checkout" %
% endblock %

% block content %
    <h1>% trans "Checkout" %</h1><divclass="order-info"><h3>% trans "Your order" %</h3><ul>
            % for item in cart %
                <li>
                     item.quantity x  item.product.name 
                    <span>$ item.total_price|floatformat:"2" </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_diccount|floatformat:"2" </p></div><formaction="."method="post"class="order-form"novalidate>
         form.as_p 
        <p><inputtype="submit"value="% trans "Placeorder" %"></p>
        % csrf_token %
    </form>
% endblock %

到现在我们完成了如下文件的翻译:

  • shop应用的shop/product/list.html模板

  • orders应用的orders/order/created.html模板

  • cart应用的cart/detail.html模板

之后来更新消息文件,打开命令行窗口执行:

Copydjango-admin makemessages --all

此时myshop项目下的locale目录内有了对应的.po文件,而orders应用的翻译文件优先存放在应用内部的locale目录中。

编辑所有.po文件,在msgstr属性内添加西班牙语翻译。你也可以直接复制随书代码内对应文件的内容。

执行命令编译消息文件:

Copydjango-admin compilemessages

可以看到如下输出:

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

针对每一个.po文件都会生成对应的.mo文件。

2.5使用Rosetta翻译界面

Rosetta是一个第三方应用,通过Django管理后台编辑所有翻译内容,让.po文件的管理变得更加方便,先通过pip安装该模块:

Copypip install django-rosetta==0.8.1

之后在settings.py中激活该应用:

CopyINSTALLED_APPS = [
    # ...'rosetta',
]

然后需要为Rosetta配置相应的URL,其二级路由已经配置好,修改项目根路由增加一行:

Copyurlpatterns = [
    # ...
    path('rosetta/', include('rosetta.urls')),
    path('', include('shop.urls', namespace='shop')),
]

这条路径也需要在shop.urls上边。

然后启动站点,使用管理员身份登录http://127.0.0.1:8000/rosetta/ ,再转到http://127.0.0.1:8000/rosetta/,点击右上的THIRD PARTY以列出所有的翻译文件,如下图所示:

点开Spanish下边的Myshop链接,可以看到列出了所有需要翻译的内容:

可以手工编辑需要翻译的地方,OCCURRENCES(S)栏显示了该翻译所在的文件名和行数,对于那些占位符翻译的内容,显示为这样:

Rosetta对占位符使用了不同的背景颜色,在手工输入翻译内容的时候注意不要破坏占位符的结构,例如要翻译下边这一行:

Copy%(total_items)s item%(total_items_plural)s, $%(total_price)s

应该输入:

Copy%(total_items)s producto%(total_items_plural)s, $%(total_price)s

可以参考本章随书代码中的西班牙语翻译来录入翻译内容。

结束输入的时候,点击一下Save即可将当前翻译的内容保存到.po文件中,当保存之后,Rosetta会自动进行编译,所以无需执行compilemessages命令。然而要注意Rosetta会直接读写locale目录,注意要给予其相应的权限。

如果需要其他用户来编辑翻译内容,可以到http://127.0.0.1:8000/admin/auth/group/add/新增一个用户组叫translators,然后到http://127.0.0.1:8000/admin/auth/user/编辑用户的权限以给予其修改翻译的权限,将该用户加入到translators用户组内。仅限超级用户和translators用户组内的用户才能使用Rosetta。

Rosetta的官方文档在https://django-rosetta.readthedocs.io/en/latest/。

特别注意的是,当Django已经在生产环境运行时,如果修改和新增了翻译,在运行了compilemessages命令之后,只有重新启动Django才会让新的翻译生效。

2.6待校对翻译Fuzzy translations

你可能注意到了,Rosetta页面上有一列叫做Fuzzy。这不是Rosetta的功能,而是gettext提供的功能。如果将fuzzy设置为true,则该条翻译不会包含在编译后的消息文件中。这个字段用来标记需要由用户进行检查的翻译内容。当.po文件更新了新的翻译字符串时,很可能一些翻译被自动标成了fuzzy。这是因为:在gettext发现一些msgid被修改过的时候,gettext会将其与它认为的旧有翻译进行匹配,然后标注上fuzzy。看到fuzzy出现的时候,人工翻译者必须检查该条翻译,然后取消fuzzy,之后再行编译。

2.7国际化URL

Django提供两种国际化URL的特性:

  • Language prefix in URL patterns 语言前缀URL模式:在URL的前边加上不同的语言前缀构成不同的基础URL

  • Translated URL patterns 翻译URL模式:基础URL相同,把基础URL按照不同语言翻译给用户得到不同语言的URL

使用翻译URL模式的优点是对搜索引擎友好。如果采用语言前缀URL,则必须要为每一种语言进行索引,使用翻译URL模式,则一条URL就可以匹配全部语言。下边来看一下两种模式的使用:

2.7.1语言前缀URL模式

Django可以为不同语言在URL前添加前缀,例如我们的网站,英语版以/en/开头,而西班牙语版以/es/开头。

要使用语言前缀URL模式,需要启用LocaleMiddleware中间件,用于从不同的URL中识别语言,在之前我们已经添加过该中间件。

我们来为URL模式增加前缀,现在需要修改项目的根urls.py文件:

Copyfrom django.conf.urls.i18n import i18n_patterns

urlpatterns = i18n_patterns(
    path('admin/', admin.site.urls),
    path('cart/', include('cart.urls', namespace='cart')),
    path('orders/', include('orders.urls', namespace='orders')),
    path('pyament/', include('payment.urls', namespace='payment')),
    path('coupons/', include('coupons.urls', namespace='coupons')),
    path('rosetta/', include('rosetta.urls')),
    path('', include('shop.urls', namespace='shop')),
)

可以混用未经翻译的标准URL与i18n_patterns类型的URL,使部分URL带有语言前缀,部分不带前缀。但最好只使用翻译URL,以避免把翻译过的URL匹配到未经翻译过的URL模式上。

现在启动站点,到http://127.0.0.1:8000/ ,Django的语言中间件会按照之前介绍的顺序来确定本地语言,然后重定向到带有语言前缀的URL。现在看一下浏览器的地址栏,应该是http://127.0.0.1:8000/en/。当前语言是由请求头Accept-Language所设置,或者就是LANGUAGE_CODE的设置。

2.7.2翻译URL模式

Django支持在URL模式中翻译字符串。针对不同的语言,可以翻译出不同的URL。在urls.py中,使用ugettext_lazy()来标注字符串。

编辑myshop应用的根urls.py,为cart,orders,payment和coupons应用配置URL:

Copyfrom django.utils.translation import gettext_lazy as _

urlpatterns = i18n_patterns(
    path(_('admin/'), admin.site.urls),
    path(_('cart/'), include('cart.urls', namespace='cart')),
    path(_('orders/'), include('orders.urls', namespace='orders')),
    path(_('payment/'), include('payment.urls', namespace='payment')),
    path(_('coupons/'), include('coupons.urls', namespace='coupons')),
    path('rosetta/', include('rosetta.urls')),
    path('', include('shop.urls', namespace='shop')),
)

编辑orders应用的urls.py文件,修改成如下:

Copyfrom django.utils.translation import gettext_lazy as _

urlpatterns = [
    path(_('create/'), views.order_create, name='order_create'),
    # ...
]

修改payment应用的urls.py文件,修改成如下:

Copyfrom django.utils.translation import gettext_lazy as _

urlpatterns = [
    path(_('process/'), views.payment_process, name='process'),
    path(_('done/'), views.payment_done, name='done'),
    path(_('canceled/'), views.payment_canceled, name='canceled'),
]

对于shop应用的URL不需要修改,因为其URL是动态建立的。

执行命令进行编译,更新消息文件:

Copydjango-admin makemessages --all

启动站点,访问http://127.0.0.1:8000/en/rosetta/,点击Spanish下的Myshop,可以看到出现了URL对应的翻译。可以点击Untranslated查看所有尚未翻译的字符串,然后输入翻译内容。

2.8允许用户切换语言

在之前的工作中,我们配置好了英语和西班牙语的翻译,应该给用户提供切换语言的选项,为此准备给网站增加一个语言选择器,列出所有支持的语言,显示为一系列链接。

编辑shop应用下的base.html,找到下边这三行:

Copy<divid="header"><ahref="/"class="logo">% trans "My shop" %</a></div>

将其替换成:

Copy<divid="header"><ahref="/"class="logo">% trans "My shop" %</a>
    % get_current_language as LANGUAGE_CODE %
    % get_available_languages as LANGUAGES %
    % get_language_info_list for LANGUAGES as languages %
    <divclass="languages"><p>% trans "Language" %:</p><ulclass="languages">
            % for language in languages %
                <li><ahref="/ language.code /"
                       % iflanguage.code == LANGUAGE_CODE % class="selected"% endif %>
                         language.name_local 
                    </a></li>
            % endfor

以上是关于扩展商店功能的主要内容,如果未能解决你的问题,请参考以下文章

扩展商店功能

python Django接口自动化测试

Django manage.py 测试:“数据库后端不接受 0 作为 AutoField 的值”(mysql)

用于创建数据库、运行测试的 Django / postgres 设置

django是前端还是后端

django是前端还是后端