forked from minkolee/django2-by-example-ZH
-
Notifications
You must be signed in to change notification settings - Fork 0
/
chapter09.html
1422 lines (1327 loc) · 83.7 KB
/
chapter09.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="css/normalize.min.css">
<link rel="stylesheet" href="css/base.css">
<title>第九章 扩展商店功能</title>
</head>
<body>
<h1 id="top"><b>第九章 扩展商店功能</b></h1>
<p>在上一章里,为电商站点集成了支付功能,然后可以生成PDF发票发送给用户。在本章,我们将为商店添加优惠码功能。此外,还会学习国际化和本地化的设置和建立一个推荐商品的系统。</p>
<p>本章涵盖如下要点:</p>
<ul>
<li>建立一个优惠券系统,可以实现折扣功能</li>
<li>给项目增加国际化功能</li>
<li>使用Rosetta来管理翻译</li>
<li>使用Django-parler翻译模型</li>
<li>建立商品推荐系统</li>
</ul>
<h2 id="c9-1"><span class="title">1</span>优惠码系统</h2>
<p>很多电商网站,会向用户发送电子优惠码,以便用户在购买时使用,以折扣价进行结算。一个在线优惠码通常是一个字符串,然后还规定了有效期限,一次性有效或者可以反复使用。</p>
<p>我们将为站点添加优惠码功能。我们的优惠码带有有效期,但是不限制使用次数,输入之后,就会影响用户购物车中的总价。为了实现这个需求,需要建立一个数据模型来存储优惠码,有效期和对应的折扣比例。</p>
<p>为<code>myshop</code>项目创建新的应用<code>coupons</code>:</p>
<pre>python manage.py startapp coupons</pre>
<p>然后在<code>settings.py</code>内激活该应用:</p>
<pre>
INSTALLED_APPS = [
# ...
<b>'coupons.apps.CouponsConfig',</b>
]
</pre>
<h3 id="c9-1-1"><span class="title">1.1</span>创建优惠码数据模型</h3>
<p>编辑<code>coupons</code>应用的<code>models.py</code>文件,创建一个<code>Coupon</code>模型:</p>
<pre>
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
</pre>
<p>这是用来存储优惠码的模型,<code>Coupon</code>模型包含以下字段:</p>
<ul>
<li><code>code</code>:用于存放码的字符串</li>
<li><code>valid_from</code>:优惠码有效期的开始时间。</li>
<li><code>valid_to</code>:优惠码有效期的结束时间。</li>
<li><code>discount</code>:该券对应的折扣,是一个百分比,所以取值为<code>0-100</code>,我们使用了内置验证器控制该字段的取值范围。</li>
<li><code>active</code>:表示该码是否有效</li>
</ul>
<p>之后执行数据迁移程序。然后将<code>Coupon</code>模型加入到管理后台,编辑<code>coupons</code>应用的<code>admin.py</code>文件:</p>
<pre>
from django.contrib import admin
from .models import 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)
</pre>
<p>现在启动站点,到<a href="http://127.0.0.1:8000/admin/coupons/coupon/add/" target="_blank">http://127.0.0.1:8000/admin/coupons/coupon/add/</a>查看<code>Coupon</code>模型:</p>
<p><img src="http://img.conyli.cc/django2/C09-01.jpg" alt=""></p>
<p>输入一个优惠码记录,有效期设置为当前日期,不要忘记勾上Active然后点击SAVE按钮。</p>
<h3 id="c9-1-2"><span class="title">1.2</span>为购物车增加优惠码功能</h3>
<p>创建数据模型之后,可以查询和获得优惠码对象。现在我们必须增添使用户可以输入优惠码从而获得折扣价的功能。这个功能将按照如下逻辑进行操作:</p>
<ol>
<li>用户添加商品到购物车</li>
<li>用户能通过购物车详情页面的表单输入一个优惠码</li>
<li>输入优惠码并提交表单之后,需要来判断该码是否在数据库中存在、当前时间是否在<code>valid_from</code>和<code>valid_to</code>有效时间之间、<code>active</code>属性是否为<code>True</code>。</li>
<li>如果优惠码通过上述检查,将优惠码的信息保存在<code>session</code>中,用折扣重新计算价格并更新购物车中的商品价格</li>
<li>用户提交订单时,将优惠码保存在订单对象中。</li>
</ol>
<p>在<code>coupons</code>应用里建立<code>forms.py</code>文件,添加下列代码:</p>
<pre>
from django import forms
class CouponApplyForm(forms.Form):
code = forms.CharField()
</pre>
<p>这个表单用于用户输入优惠码。然后来编辑<code>coupons</code>应用的<code>views.py</code>文件:</p>
<pre>
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')
</pre>
<p>这个<code>coupon_apply</code>视图验证优惠码并将其存储在session中,使用了<code>@require_POST</code>装饰器令该视图仅接受<code>POST</code>请求。这个视图的业务逻辑如下:</p>
<ol>
<li>使用请求中的数据初始化<code>CouponApplyForm</code></li>
<li>如果表单通过验证,从表单的<code>cleaned_data</code>获取<code>code</code>,然后使用<code>code</code>查询数据库得到<code>coupon</code>对象,这里使用了过滤参数<code>iexact</code>,进行完全匹配;使用<code>active=True</code>过滤出有效的优惠码;使用<code>timezone.now()</code>获取当前时间,<code>valid_from</code>和<code>valid_to</code>分别采用<code>lte</code>(小于等于)和<code>gte</code>(大于等于)过滤查询以保证当前时间位于有效期内。</li>
<li>将优惠码ID存入当前用户的session。</li>
<li>重定向到<code>cart_detail</code> URL对应的购物车详情页,以显示应用了优惠码之后的金额。</li>
</ol>
<p>需要为<code>coupon_apply</code>视图配置URL,在<code>coupons</code>应用中建立<code>urls.py</code>文件,添加下列代码:</p>
<pre>
from django.urls import path
from . import views
app_name = 'coupons'
urlpatterns = [
path('apply/', views.coupon_apply, name='apply'),
]
</pre>
<p>然后编辑项目的根路由,增加一行:</p>
<pre>
urlpatterns = [
# ...
<b>path('coupons/', include('coupons.urls', namespace='coupons')),</b>
path('', include('shop.urls', namespace='shop')),
]
</pre>
<p>依然记得要把这一行放在<code>shop.urls</code>上方。</p>
<p>编辑<code>cart</code>应用中的<code>cart.py</code>文件,添加下列导入:</p>
<pre>
from coupons.models import Coupon
</pre>
<p>然后在<code>cart</code>类的<code>__init__()</code>方法的最后添加从session中获得优惠码ID的语句:</p>
<pre>
class Cart(object):
def __init__(self, request):
# ...
<b># store current applied coupon</b>
<b>self.coupon_id = self.session.get('coupon_id')</b>
</pre>
<p>在<code>Cart</code>类中,我们需要通过<code>coupon_id</code>获取优惠码信息并将其保存在<code>Cart</code>对象内,为<code>Cart</code>类添加如下方法:</p>
<pre>
class Cart(object):
# ...
<b>@property</b>
<b>def coupon(self):</b>
<b>if self.coupon_id:</b>
<b>return Coupon.objects.get(id=self.coupon_id)</b>
<b>return None</b>
<b>def get_discount(self):</b>
<b>if self.coupon:</b>
<b>return (self.coupon.discount / Decimal('100')) * self.get_total_price()</b>
<b>return Decimal('0')</b>
<b>def get_total_price_after_diccount(self):</b>
<b>return self.get_total_price() - self.get_discount()</b>
</pre>
<p>这些方法解释如下:</p>
<ul>
<li><code>coupon()</code>:我们使用<code>@property</code>将该方法定义为属性,如果购物车包含一个<code>coupon_id</code>属性,会返回该id对应的<code>Coupon</code>对象</li>
<li><code>get_discount()</code>:如果包含优惠码id,计算折扣价格,否则返回0。</li>
<li><code>get_total_price_after_discount()</code>:返回总价减去折扣价之后的折扣后价格。</li>
</ul>
<p>现在<code>Cart</code>类就具备了根据优惠码计算折扣价的功能。</p>
<p>现在还需要修改购物车详情视图函数,以便在页面中应用表单和展示折扣金额,修改<code>cart</code>应用的<code>views.py</code>文件,增加导入代码:</p>
<pre>from coupons.forms import CouponApplyForm
</pre>
<p>然后修改<code>cart_detail</code>视图,添加表单:</p>
<pre>
def cart_detail(request):
cart = Cart(request)
for item in cart:
item['update_quantity_form'] = CartAddProductForm(initial={'quantity': item['quantity'], 'update': True})
<b>coupon_apply_form = CouponApplyForm()</b>
return render(request, 'cart/detail.html', {'cart': cart, <b>'coupon_apply_form': coupon_apply_form</b>})
</pre>
<p>修改<code>cart</code>应用的购物车模板<code>cart/detail.html</code>,找到如下几行:</p>
<pre>
<tr class="total">
<td>total</td>
<td colspan="4"></td>
<td class="num">${{ cart.get_total_price }}</td>
</tr>
</pre>
<p>替换成如下代码:</p>
<pre>
{% if cart.coupon %}
<tr class="subtotal">
<td>Subtotal</td>
<td colspan="4"></td>
<td class="num">${{ cart.get_total_price_after_diccount }}</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_diccount|floatformat:"2" }}</td>
</tr>
</pre>
<p>这是新的购物车模板。如果包含一个优惠券,就展示一行购物车总价,再展示一行优惠券信息,最后通过<code>get_total_price_after_discount()</code>展示折扣后价格。</p>
<p>在同一个文件内,在<code></table></code>后增加下列代码:</p>
<pre>
{# 在紧挨着</table>标签之后插入: #}
<p>Apply a coupon:</p>
<form action="{% url 'coupons:apply' %}" method="post">
{{ coupon_apply_form }}
<input type="submit" value="Apply">
{% csrf_token %}
</form>
</pre>
<p>上边这段代码展示输入优惠码的表单。</p>
<p>在浏览器中打开<code>http://127.0.0.1:8000/</code>,向购物车内加入一些商品,然后进入购物车页面输入优惠码并提交,可以看到如下所示:</p>
<p><img src="http://img.conyli.cc/django2/C09-02.jpg" alt=""></p>
<p>之后来修改订单模板<code>orders/order/create.html</code>,在其中找到如下部分:</p>
<pre>
<ul>
{% for item in cart %}
<li>
{{ item.quantity }} x {{ item.product.name }}
<span>${{ item.total_price }}</span>
</li>
{% endfor %}
</ul>
</pre>
<p>替换成:</p>
<pre>
<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>
</pre>
<p>如果有优惠码,现在的订单页面就展示优惠码信息了。继续找到下边这行:</p>
<pre>
<p>Total: ${{ cart.get_total_price }}</p>
</pre>
<p>替换成:</p>
<pre>
<p>Total: ${{ cart.get_total_price_after_diccount|floatformat:"2" }}</p>
</pre>
<p>这样总价也变成了折扣后价格。</p>
<p>在浏览器中打开<code>http://127.0.0.1:8000/</code>,添加商品到购物车然后生成订单,可以看到订单页面的价格现在是折扣后的价格了:</p>
<p><img src="http://img.conyli.cc/django2/C09-03.jpg" alt=""></p>
<h3 id="c9-1-3"><span class="title">1.3</span>在订单中记录优惠码信息</h3>
<p>像之前说的,我们需要将优惠码信息保存至<code>order</code>对象中,为此需要修改<code>Order</code>模型。编辑</p>
<p>编辑<code>orders</code>应用的<code>models.py</code>文件,增加导入部分的代码:</p>
<pre>
from decimal import Decimal
from django.core.validators import MinValueValidator, MaxValueValidator
from coupons.models import Coupon
</pre>
<p>然后为<code>Order</code>模型增加下列字段:</p>
<pre>
class Order(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)])
</pre>
<p>这两个字段用于存储优惠码信息。虽然折扣信息保存在Coupon对象中,但这里还是用<code>discount</code>字段保存了当前的折扣,以免未来优惠码折扣发生变化。为<code>coupon</code>字段设置了<code>on_delete=models.SET_NULL</code>,优惠码删除时,该外键字段会变成空值。</p>
<p>增加好字段后数据迁移程序。回到<code>models.py</code>文件,需要修改<code>Order</code>类中的<code>get_total_cost()</code>方法:</p>
<pre>
class Order(models.Model):
# ...
def get_total_cost(self):
<b>total_cost</b> = sum(item.get_cost() for item in self.items.all())
<b>return total_cost - total_cost * (self.discount / Decimal('100'))</b>
</pre>
<p>修改后的<code>get_total_cost()</code>方法会把折扣也考虑进去。之后还需要修改<code>orders</code>应用里的<code>views.py</code>文件中的<code>order_create</code>视图,以便在生成订单的时候,存储这两个新增的字段。找到下边这行:</p>
<pre>order = form.save()</pre>
<p>将其替换成如下代码:</p>
<pre>
order = form.save(<b>commit=False</b>)
<b>if cart.coupon:</b>
<b>order.coupon = cart.coupon</b>
<b>order.discount = cart.coupon.discount</b>
<b>order.save()</b>
</pre>
<p>在修改后代码中,通过调用<code>OrderCreateForm</code>表单对象的<code>save()</code>方法,创建一个<code>order</code>对象,使用<code>commit=False</code>暂不存入数据库。如果购物车对象中有折扣信息,就保存折扣信息。然后将<code>order</code>对象存入数据库。</p>
<p>启动站点,在浏览器中访问<a href="http://127.0.0.1:8000/" target="_blank">http://127.0.0.1:8000/</a>,使用一个自己创建的优惠码,在完成购买之后,可以到<a href="http://127.0.0.1:8000/admin/orders/order/" target="_blank">http://127.0.0.1:8000/admin/orders/order/></a>查看包含优惠码和折扣信息的订单:</p>
<p><img src="http://img.conyli.cc/django2/C09-04.jpg" alt=""></p>
<p>还可以修改管理后台的订单详情页和和PDF发票,以使其包含优惠码和折扣信息。下边我们将为站点增加国际化功能。</p>
<p class="emp">译者注:这里有一个问题:用户提交了订单并清空购物车后,如果再向购物车内添加内容,再次进入购物车详情页面可以发现自动使用了上次使用的优惠券。此种情况的原因是作者把优惠券信息附加到了session上,在提交订单的时候没有清除。cart对象实例化的时候又取到了相同的优惠券信息。所以需要对程序进行一下改进。</p>
<p class="emp">修改<code>orders</code>应用的<code>order_create</code>视图,在生成<code>OrderItem</code>并清空购物车的代码下增加一行:</p>
<pre>
def order_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()
<b># 清除优惠券信息</b>
<b>request.session['coupon_id'] = None</b>
# 成功完成订单后调用异步任务发送邮件
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})
</pre>
<h2 id="c9-2"><span class="title">2</span>国际化与本地化</h2>
<p>Django对于国际化和本地化提供了完整的支持,允许开发者将站点内容翻译成多种语言,而且可以处理本地化的时间日期数字和时区格式等本地化的显示内容。在开始之前,先需要区分一下<a href="https://en.wikipedia.org/wiki/Internationalization_and_localization" target="_blank">国际化和本地化</a>两个概念。国际化和本地化都是一种软件开发过程。国际化(Internationalization,通常缩写为<b>i18n</b>),是指一个软件可以被不同的国家和地区使用,而不会局限于某种语言。本地化(Localization,缩写为<b>l10n</b>)是指对国际化的软件将其进行翻译或者其他本地化适配,使之变成适合某一个国家或地区使用的软件的过程。Django通过自身的国际化框架,可以支持超过50种语言。</p>
<h3 id="c9-2-1"><span class="title">2.1</span>国际化与本地化设置</h3>
<p>Django的国际化框架可以让开发者很方便的在Python代码和模板中标注需要翻译的字符串,这个框架依赖于GNU gettext开源软件来生成和管理<b>消息文件(message file</b>)。消息文件是一个纯文本文件,代表一种语言的翻译,存放着在站点应用中找到的部分或者所有需要翻译的字符串以及对应的某种语言的翻译,就像一个字典一样。消息文件的后缀名是<code>.po</code>。</p>
<p>一旦完成翻译,可以把消息文件编译,以快速访问翻译内容,编译后的消息文件的后缀名是<code>.mo</code>。</p>
<h4 id="c9-2-1-1"><span class="title">2.1.1</span>国际化与本地化设置</h4>
<p>Django提供了一些国际化和本地化的设置,下边一些设置是最重要的:</p>
<ul>
<li><code>USE_I18N</code>:布尔值,是否启用国际化功能,默认为<code>True</code></li>
<li><code>USE_L10N</code>:布尔值,设置本地化功能是否启用,设置为<code>True</code>时,数字和日期将采用本地化显示。默认为<code>False</code></li>
<li><code>USE_TZ</code>:布尔值,指定时间是否根据时区进行调整,当使用<code>startproject</code>创建项目时,默认为<code>True</code></li>
<li><code>LANGUAGE_CODE</code>:项目的默认语言代码,采用标准的语言代码格式,例如'en-us'表示美国英语,'en-gb'表示英国英语。这个设置需要<code>USE_I18N</code>设置为<code>True</code>才会生效。在<a href="http://www.i18nguy.com/unicode/language-identifiers.html" target="_blank">http://www.i18nguy.com/unicode/language-identifiers.html</a>可以找到语言代码清单。</li>
<li><code>LANGUAGES</code>:一个包含项目所有可用语言的元组,其中每个元素是语言代码和语言名称构成的二元组。可以在<code>django.conf.global_settings</code>查看所有可用的语言。这个属性可设置的值必须是<code>django.conf.global_settings</code>中列出的值。</li>
<li><code>LOCALE_PATHS</code>:一个目录列表,目录内存放项目的翻译文件。</li>
<li><code>TIME_ZONE</code>:字符串,代表项目所采用的时区。如果使用<code>startproject</code>启动项目,该值被设置为<code>'UTC'</code>。可以按照实际情况将其设置为具体时区,如<code>'Europe/Madrid'</code>。中国的时区是<code>'Asia/Shanghai'</code>,大小写敏感。</li>
</ul>
<p>以上是常用的国际化和本地化设置,完整设置请参见<a href="https://docs.djangoproject.com/en/2.1/ref/settings/#globalization-i18n-l10n" target="_blank">https://docs.djangoproject.com/en/2.1/ref/settings/#globalization-i18n-l10n</a>。</p>
<h4 id="c9-2-1-2"><span class="title">2.1.2</span>国际化和本地化管理命令</h4>
<p>Django包含了用于管理翻译的命令如下:</p>
<ul>
<li><code>makemessages</code>:运行该命令,会找到项目中所有标注要翻译的字符串,建立或者更新<code>locale</code>目录下的<code>.po</code>文件,每种语言会生成单独的<code>.po</code>文件。</li>
<li><code>compilemessages</code>:编译所有的<code>.po</code>文件为<code>.mo</code>文件。</li>
</ul>
<p>需要使用GNU gettext工具来执行上述过程,大部分linux发行版自带有该工具。如果在使用mac OSX,可以通过 <a href="http://brew.sh/" target="_blank">http://brew.sh/</a> 使用命令<code>brew install gettext</code>来安装,之后使用<code>brew link gettext --force</code>强制链接。对于Windows下的安装,参考<a
href="https://docs.djangoproject.com/en/2.0/topics/i18n/translation/#gettext-on-windows" target="_blank">https://docs.djangoproject.com/en/2.0/topics/i18n/translation/#gettext-on-windows</a>中的步骤。</p>
<h4 id="c9-2-1-3"><span class="title">2.1.3</span>如何为项目增加翻译文件</h4>
<p>先来看一下增加翻译需要进行的流程:</p>
<ol>
<li>在Python代码和模板中标注出需要翻译的字符串</li>
<li>运行<code>makemessages</code>命令建立消息文件</li>
<li>在消息文件中将字符串翻译成另外一种语言,然后运行<code>compilemessages</code>命令编译消息文件</li>
</ol>
<h4 id="c9-2-1-4"><span class="title">2.1.4</span>Django如何确定当前语言</h4>
<p>Django使用中间件<code>django.middleware.locale.LocaleMiddleware</code>来检查HTTP请求中所使用的本地语言。这个中间件做的工作如下:</p>
<ol>
<li>如果使用<a href="https://docs.djangoproject.com/en/2.0/topics/i18n/translation/#language-prefix-in-url-patterns" target="_blank">i18_patterns</a>(django特殊的一种URL方式,里边包含语言前缀),中间件会在请求的URL中寻找特定语言的前缀</li>
<li>如果在URL中没有发现语言前缀,会在session中寻找一个键<code>LANGUAGE_SESSION_KEY</code></li>
<li>如果session中没有该键,会在cookie中寻找一个键。可以通过<code>LANGUAGE_COOKIE_NAME</code>自定义该cookie的名称,默认是<code>django_language</code></li>
<li>如果cookie中未找到,找HTTP请求头的<code>Accept-Language</code>键</li>
<li>如果<code>Accept-Language</code>头部信息未指定具体语言,则使用<code>LANGUAGE_CODE</code>设置</li>
</ol>
<p>注意这个过程只有在开启了该中间件的时候才会得到完整执行,如果未开启中间件,Django直接使用<code>LANGUAGE_CODE</code>中的设置。</p>
<h3 id="c9-2-2"><span class="title">2.2</span>为项目使用国际化进行准备</h3>
<p>我们准备为电商网站增添各种语言的支持,增添英语和西班牙语的支持。编辑<code>settings.py</code>文件,加入<code>LANGUAGES</code>设置,放在<code>LANGUAGE_CODE</code>的旁边:</p>
<pre>
LANGUAGES = (
('en', 'English'),
('es', 'Spanish'),
)
</pre>
<p><code>LANGUAGES</code>设置包含两个语言代码和名称组成的元组。语言代码可以指定具体语言如<code>en-us</code>或<code>en-gb</code>,也可以更模糊,如<code>en</code>。通过这个设置,我们定义了我们的网站仅支持英语和西班牙语。如果不定义<code>LANGUAGES</code>设置,默认支持所有django支持的语言。</p>
<p>设置<code>LANGUAGE_CODE</code>为如下:</p>
<pre>LANGUAGE_CODE = 'en'</pre>
<p>添加<code>django.middleware.locale.LocaleMiddleware</code>到<code>settings.py</code>的中间件设置中,位置在<code>SessionMiddleware</code>中间件之后,<code>CommonMiddleware</code>中间件之前,因为<code>LocaleMiddleware</code>需要使用session,而<code>CommonMiddleware</code>需要一种可用语言来解析URL,<code>MIDDLEWARE</code>设置成如下:</p>
<pre>
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
<b>'django.middleware.locale.LocaleMiddleware',</b>
'django.middleware.common.CommonMiddleware',
# ...
]
</pre>
<p class="hint">django中间件设置的顺序很重要,中间件会在请求上附加额外的数据,某个中间件会依赖于另外一个中间件附加的数据才能正常工作。</p>
<p>在manage.py文件所在的项目根目录下创建如下目录:</p>
<pre>
locale/
en/
es/
</pre>
<p><code>locale</code>目录是用来存放消息文件的目录,编辑<code>settings.py</code>文件加入如下设置:</p>
<pre>
LOCALE_PATH = (
os.path.join(BASE_DIR, 'locale/'),
)
</pre>
<p><code>LOCALE_PATH</code>指定了Django寻找消息文件的路径,可以是一系列路径,最上边的路径优先级最高。</p>
<p>当使用<code>makemessages</code>命令的时候,消息文件会在我们创建的<code>locale/</code>目录中创建,如果某个应用也有<code>locale/</code>目录,那个应用中的翻译内容会优先在那个应用的目录中创建。</p>
<h3 id="c9-2-3"><span class="title">2.3</span>翻译Python代码中的字符串</h3>
<p>为了翻译Python代码中的字符串字面量,需要使用<code>django.utils.translation</code>模块中的<code>gettext()</code>方法来标注字符串。这个方法返回翻译后的字符串,通常做法是导入该方法然后命名为一个下划线"_"。可以在<a href="https://docs.djangoproject.com/en/2.0/topics/i18n/translation/" target="_blank">https://docs.djangoproject.com/en/2.0/topics/i18n/translation/</a>查看文档。</p>
<h4 id="c9-2-3-1"><span class="title">2.3.1</span>标记字符串</h4>
<p>标记字符串的方法如下:</p>
<pre>
from django.utils.translation import gettext as _
output = _('Text to be translated.')
</pre>
<h4 id="c9-2-3-2"><span class="title">2.3.2</span>惰性翻译</h4>
<p>Django对于所有的翻译函数都有惰性版本,后缀为<code>_lazy()</code>。使用惰性翻译函数的时候,字符串只有被访问的时候才会进行翻译,而不是在翻译函数调用的时候。当字符串位于模块加载的时候才生成的路径中时候特别有效。</p>
<p class="hint">使用<code>gettext_lazy()</code>代替<code>gettext()</code>方法,只有在该字符串被访问的时候才会进行翻译,所有的翻译函数都有惰性版本。。</p>
<h4 id="c9-2-3-3"><span class="title">2.3.3</span>包含变量的翻译</h4>
<p>被标注的字符串中还可以带有占位符,以下是一个占位符的例子:</p>
<pre>
from django.utils.translation import gettext as _
month = _('April')
day = '14'
output = _('Today is %(month)s %(day)s') % {'month': month, day': day}
</pre>
<p>通过使用占位符,可以使用字符串变量。例如,上边这个例子的英语如果是<em>"Today is April 14"</em>,翻译成的西班牙语就是<em>"Hoy es 14 de Abril"</em>。当需要翻译的文本中存在变量的时候,推荐使用占位符。</p>
<h4 id="c9-2-3-4"><span class="title">2.3.4</span>复数的翻译</h4>
<p>对于复数形式的翻译,可以采用<code>ngettext()</code>和<code>ngettext_lazy()</code>。这两个函数根据对象的数量来翻译单数或者复数。使用例子如下:</p>
<pre>
output = ngettext('there is %(count)d product', 'there are %(count)d products', count) % {'count': count}
</pre>
<p>现在我们了解了Python中翻译字面量的知识,可以来为我们的项目添加翻译功能了。</p>
<h4 id="c9-2-3-5"><span class="title">2.3.5</span>为项目翻译Python字符串字面量</h4>
<p>编辑<code>setttings.py</code>,导入<code>gettext_lazy()</code>,然后修改<code>LANGUAGES</code>设置:</p>
<pre>
from django.utils.translation import gettext_lazy as _
LANGUAGES = (
('en', _('English')),
('es', _('Spanish')),
)
</pre>
<p>这里导入了<code>gettext_lazy()</code>并使用了别名"_"来避免重复导入。将显示的名称也进行了翻译,这样对于不同的语言的人来说,可以看懂并选择他自己的语言。</p>
<p>然后打开系统命令行窗口,输入如下命令:</p>
<pre>
django-admin makemessages --all
</pre>
<p>可以看到如下输出:</p>
<pre>
processing locale en
processing locale es
</pre>
<p>然后查看项目的<code>locale</code>目录,可以看到如下文件和目录结构:</p>
<pre>
en/
LC_MESSAGES/
django.po
es/
LC_MESSAGES/
django.po
</pre>
<p>每个语言都生成了一个<code>.po</code>消息文件,使用文本编辑器打开<code>es/LC_MESSAGES/django.po</code>文件,在末尾可以看到如下内容:</p>
<pre>
#: .\myshop\settings.py:107
msgid "English"
msgstr ""
#: .\myshop\settings.py:108
msgid "Spanish"
msgstr ""
</pre>
<p>每一部分的第一行表示在那个文件的第几行发现了需翻译的内容,每个翻译包含两个字符串:</p>
<ul>
<li><code>msgid</code>:源代码中的字符串</li>
<li><code>msgstr</code>:被翻译成的字符串,默认为空,需要手工添加。</li>
</ul>
<p>添加好翻译之后的文件如下:</p>
<pre>
#: myshop/settings.py:117
msgid "English"
msgstr "Inglés"
#: myshop/settings.py:118
msgid "Spanish"
msgstr "Español"
</pre>
<p>保存这个文件,之后执行命令编译消息文件:</p>
<pre>
django-admin compilemessages
</pre>
<p>可以看到输出如下:</p>
<pre>
processing file django.po in myshop/locale/en/LC_MESSAGES
processing file django.po in myshop/locale/es/LC_MESSAGES
</pre>
<p>这表明已经编译了翻译文件,此时查看<code>locale</code>目录,其结构如下:</p>
<pre>
en/
LC_MESSAGES/
django.mo
django.po
es/
LC_MESSAGES/
django.mo
django.po
</pre>
<p>可以看到每种语言都生成了<code>.mo</code>文件。</p>
<p>我们已经翻译好了语言名称本身。现在我们来试着翻译一下<code>Order</code>模型的所有字段,修改<code>orders</code>应用的<code>models.py</code>文件:</p>
<pre>
<b>from django.utils.translation import gettext_lazy as _</b>
class Order(models.Model):
first_name = models.CharField(<b>_('frist name')</b>, max_length=50)
last_name = models.CharField(<b>_('last name')</b>, max_length=50)
email = models.EmailField(<b>_('e-mail')</b>, )
address = models.CharField(<b>_('address')</b>, max_length=250)
postal_code = models.CharField(<b>_('postal code')</b>, max_length=20)
city = models.CharField(<b>_('city'</b>), max_length=100)
......
</pre>
<p>我们为每个显示出来的字段标记了翻译内容,也可以使用<code>verbose_name</code>属性来命名字段。在<code>orders</code>应用中建立如下目录:</p>
<pre>
locale/
en/
es/
</pre>
<p>通过创建<code>locale</code>目录,当前应用下的翻译内容会优先保存到这个目录中,而不是保存在项目根目录下的<code>locale</code>目录中。这样就可以为每个应用配置独立的翻译文件。</p>
<p>在系统命令行中执行:</p>
<pre>
django-admin makemessages --all
</pre>
<p>输出为:</p>
<pre>
processing locale es
processing locale en
</pre>
<p>使用文本编辑器打开<code>locale/es/LC_MESSAGES/django.po</code>,可以看到Order模型的字段翻译,在<code>msgstr</code>中为对应的<code>msgid</code>字符串加上西班牙语的翻译:</p>
<pre>
#: 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"
</pre>
<p>添加完翻译之后保存文件。</p>
<p>除了常用的文本编辑软件,还可以考虑使用Poedit编辑翻译内容,该软件同样依赖gettext,支持Linux,Windows和macOS X。可以在<a href="https://poedit.net/" target="_blank">https://poedit.net/</a>下载该软件。</p>
<p>下边来翻译项目使用的表单。<code>OrderCreateForm</code>这个表单类无需翻译,因为它会自动使用<code>Order</code>类中我们刚刚翻译的<code>verbose_name</code>。现在我们去翻译<code>cart</code>和<code>coupons</code>应用中的内容。</p>
<p>在<code>cart</code>应用的<code>forms.py</code>文件中,导入翻译函数,为<code>CartAddProductForm</code>类的<code>quantity</code>字段增加一个参数<code>label</code>,代码如下:</p>
<pre>
from django import forms
<b>from django.utils.translation import gettext_lazy as _</b>
<b style="color: red;">PRODUCT_QUANTITY_CHOICES = [(i, str(i)) for i in range(1, 21)]</b>
class CartAddProductForm(forms.Form):
quantity = forms.TypedChoiceField(choices=PRODUCT_QUANTITY_CHOICES, coerce=int, <b>label=_('Quantity')</b>)
update = forms.BooleanField(required=False, initial=False, widget=forms.HiddenInput)
</pre>
<p class="emp">译者注:红字部分是本书上一版的遗留,无任何作用,读者可以忽略。</p>
<p>之后修改<code>coupons</code>应用的<code>forms.py</code>文件,为<code>CouponApplyForm</code>类增加翻译:</p>
<pre>
from django import forms
<b>from django.utils.translation import gettext_lazy as _</b>
class CouponApplyForm(forms.Form):
code = forms.CharField(<b>label=_('Coupon')</b>)
</pre>
<p>我们为<code>code</code>字段增加了一个label标签用于展示翻译后的字段名称。</p>
<h3 id="c9-2-4"><span class="title">2.4</span>翻译模板</h3>
<p>Django为翻译模板内容提供了<code>{% trans %}</code>和<code>{% blocktrans %}</code>两个模板标签用于翻译内容,如果要启用这两个标签,需要在模板顶部加入<code>{% load i18n %}</code>。</p>
<h4 id="c9-2-4-1"><span class="title">2.4.1</span>使用<code>{% trans %}</code>模板标签</h4>
<p><code>{% trans %}</code>标签用来标记一个字符串,常量或者变量用于翻译。Django内部也是该文本执行<code>gettext()</code>等翻译函数。标记字符串的例子是:</p>
<pre>{% trans "Text to be translated" %}</pre>
<p>也可以像其他标签变量一样,使用as 将 翻译后的结果放入一个变量中,在其他地方使用。下面的例子使用了一个变量<code>greeting</code>:</p>
<pre>
{% trans "Hello!" as greeting %}
<h1>{{ greeting }}</h1>
</pre>
<p>这个标签用于比较简单的翻译,但不能用于带占位符的文字翻译。</p>
<h4 id="c9-2-4-2"><span class="title">2.4.2</span>使用<code>{% blocktrans %}</code>模板标签</h4>
<p><code>{% blocktrans %}</code>标签可以标记包含常量和占位符的内容用于翻译,下边的例子展示了使用一个<code>name</code>变量的翻译:</p>
<pre>{% blocktrans %}Hello {{ name }}!{% endblocktrans %}</pre>
<p>可以使用with,将具体的表达式设置为变量的值,此时在<code>blocktrans</code>块内部不能够再继续访问表达式和对象的属性,下面是一个使用了<code>capfirst</code>装饰器的例子:</p>
<pre>
{% blocktrans with name=user.name|capfirst %}
Hello {{ name }}!
{% endblocktrans %}
</pre>
<p class="hint">如果翻译内容中包含变量,使用<code>{% blocktrans %}</code>代替<code>{% trans %}</code>。</p>
<h4 id="c9-2-4-3"><span class="title">2.4.3</span><b>翻译商店模板</b></h4>
<p>编辑<code>shop</code>应用的<code>base.html</code>,在其顶部加入<code>i18n</code>标签,然后标注如下要翻译的部分:</p>
<pre>
<b>{% load i18n %}</b>
{% load static %}
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>{% block title %}<b>{% trans "My shop" %}</b>{% endblock %}</title>
<link href="{% static "css/base2.css" %}" rel="stylesheet">
</head>
<body>
<div id="header">
<a href="/" class="logo"><b>{% trans "My shop" %}</b></a>
</div>
<div id="subheader">
<div class="cart">
{% with total_items=mycart|length %}
{% if mycart|length > 0 %}
<b>{% trans "Your cart" %}</b>:
<a href="{% url 'cart:cart_detail' %}">
<b>{% blocktrans with total_items_plural=total_items|pluralize total_price=cart.get_total_price %}</b>
<b>{{ total_items }} items{{ total_items_plural }}, ${{ total_price }}</b>
<b>{% endblocktrans %}</b>
</a>
{% else %}
<b>{% trans "Your cart is empty." %}</b>
{% endif %}
{% endwith %}
</div>
</div>
<div id="content">
{% block content %}
{% endblock %}
</div>
</body>
</html>
</pre>
<p>注意<code>{% blocktrans %}</code>展示购物车总价部分的方法,在原来的模板中,我们使用了:</p>
<pre>
{{ total_items }} item{{ total_items|pluralize }},
${{ cart.get_total_price }}
</pre>
<p>现在改用<code>{% blocktrans with ... %}</code>来为<code>total_items|pluralize</code>(使用了过滤器)和<code>cart.get_total_price</code>(访问对象的方法)创建占位符:</p>
<p>编辑<code>shop</code>应用的<code>shop/product/detail.html</code>,紧接着<code>{% extends %}</code>标签导入<code>i18n</code>标签:</p>
<pre>{% load i18n %}</pre>
<p>之后找到下边这一行:</p>
<pre><input type="submit" value="Add to cart"></pre>
<p>将其替换成:</p>
<pre><input type="submit" value="{% trans "Add to cart" %}"></pre>
<p>现在来翻译<code>orders</code>应用,编辑<code>orders/order/create.html</code>,标记如下翻译内容:</p>
<pre>
{% extends 'shop/base.html' %}
<b>{% load i18n %}</b>
{% block title %}
<b>{% trans "Checkout" %}</b>
{% endblock %}
{% block content %}
<h1><b>{% trans "Checkout" %}</b></h1>
<div class="order-info">
<h3><b>{% trans "Your order" %}</b></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>
<b>{% blocktrans with code=cart.coupon.code discount=cart.coupon.discount %}</b>
<b>"{{ code }}" ({{ discount }}% off)</b>
<b>{% endblocktrans %}</b>
<span>- ${{ cart.get_discount|floatformat:"2" }}</span>
</li>
{% endif %}
</ul>
<p><b>{% trans "Total" %}</b>: ${{ cart.get_total_price_after_diccount|floatformat:"2" }}</p>
</div>
<form action="." method="post" class="order-form" novalidate>
{{ form.as_p }}
<p><input type="submit" value="<b>{% trans "Place order" %}</b>"></p>
{% csrf_token %}
</form>
{% endblock %}
</pre>
<p>到现在我们完成了如下文件的翻译:</p>
<ul>
<li><code>shop</code>应用的<code>shop/product/list.html</code>模板</li>
<li><code>orders</code>应用的<code>orders/order/created.html</code>模板</li>
<li><code>cart</code>应用的<code>cart/detail.html</code>模板</li>
</ul>
<p>之后来更新消息文件,打开命令行窗口执行:</p>
<pre>django-admin makemessages --all</pre>
<p>此时<code>myshop</code>项目下的<code>locale</code>目录内有了对应的<code>.po</code>文件,而<code>orders</code>应用的翻译文件优先存放在应用内部的<code>locale</code>目录中。</p>
<p>编辑所有<code>.po</code>文件,在<code>msgstr</code>属性内添加西班牙语翻译。你也可以直接复制随书代码内对应文件的内容。</p>
<p>执行命令编译消息文件:</p>
<pre>django-admin compilemessages</pre>
<p>可以看到如下输出:</p>
<pre>
processing 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
</pre>
<p>针对每一个<code>.po</code>文件都会生成对应的<code>.mo</code>文件。</p>
<h3 id="c9-2-5"><span class="title">2.5</span>使用Rosetta翻译界面</h3>
<p>Rosetta是一个第三方应用,通过Django管理后台编辑所有翻译内容,让<code>.po</code>文件的管理变得更加方便,先通过<code>pip</code>安装该模块:</p>
<pre>
pip install django-rosetta==0.8.1
</pre>
<p>之后在<code>settings.py</code>中激活该应用:</p>
<pre>
INSTALLED_APPS = [
# ...
<b>'rosetta',</b>
]
</pre>
<p>然后需要为Rosetta配置相应的URL,其二级路由已经配置好,修改项目根路由增加一行:</p>
<pre>
urlpatterns = [
# ...
<b>path('rosetta/', include('rosetta.urls')),</b>
path('', include('shop.urls', namespace='shop')),
]
</pre>
<p>这条路径也需要在<code>shop.urls</code>上边。</p>
<p>然后启动站点,使用管理员身份登录<a href="http://127.0.0.1:8000/rosetta/" target="_blank">http://127.0.0.1:8000/rosetta/</a> ,再转到<a
href="http://127.0.0.1:8000/rosetta/" target="_blank">http://127.0.0.1:8000/rosetta/</a>,点击右上的THIRD PARTY以列出所有的翻译文件,如下图所示:</p>
<p><img src="http://img.conyli.cc/django2/C09-05.jpg" alt=""></p>
<p>点开Spanish下边的Myshop链接,可以看到列出了所有需要翻译的内容:</p>
<p><img src="http://img.conyli.cc/django2/C09-06.jpg" alt=""></p>
<p>可以手工编辑需要翻译的地方,OCCURRENCES(S)栏显示了该翻译所在的文件名和行数,对于那些占位符翻译的内容,显示为这样:</p>
<p><img src="http://img.conyli.cc/django2/C09-07.jpg" alt=""></p>
<p>Rosetta对占位符使用了不同的背景颜色,在手工输入翻译内容的时候注意不要破坏占位符的结构,例如要翻译下边这一行:</p>
<pre>%(total_items)s item%(total_items_plural)s, $%(total_price)s</pre>
<p>应该输入:</p>
<pre>%(total_items)s producto%(total_items_plural)s, $%(total_price)s</pre>
<p>可以参考本章随书代码中的西班牙语翻译来录入翻译内容。</p>
<p>结束输入的时候,点击一下Save即可将当前翻译的内容保存到<code>.po</code>文件中,当保存之后,Rosetta会自动进行编译,所以无需执行<code>compilemessages</code>命令。然而要注意Rosetta会直接读写<code>locale</code>目录,注意要给予其相应的权限。</p>
<p>如果需要其他用户来编辑翻译内容,可以到<a
href="http://127.0.0.1:8000/admin/auth/group/add/" target="_blank">http://127.0.0.1:8000/admin/auth/group/add/</a>新增一个用户组叫<code>translators</code>,然后到<a
href="http://127.0.0.1:8000/admin/auth/user/" target="_blank">http://127.0.0.1:8000/admin/auth/user/</a>编辑用户的权限以给予其修改翻译的权限,将该用户加入到<code>translators</code>用户组内。仅限超级用户和<code>translators</code>用户组内的用户才能使用Rosetta。</p>
<p>Rosetta的官方文档在<a href="https://django-rosetta.readthedocs.io/en/latest/" target="_blank">https://django-rosetta.readthedocs.io/en/latest/</a>。</p>
<p class="hint">特别注意的是,当Django已经在生产环境运行时,如果修改和新增了翻译,在运行了<code>compilemessages</code>命令之后,只有重新启动Django才会让新的翻译生效。</p>
<h3 id="c9-2-6"><span class="title">2.6</span>待校对翻译Fuzzy translations</h3>
<p>你可能注意到了,Rosetta页面上有一列叫做Fuzzy。这不是Rosetta的功能,而是<code>gettext</code>提供的功能。如果将fuzzy设置为true,则该条翻译不会包含在编译后的消息文件中。这个字段用来标记需要由用户进行检查的翻译内容。当<code>.po</code>文件更新了新的翻译字符串时,很可能一些翻译被自动标成了fuzzy。这是因为:在<code>gettext</code>发现一些<code>msgid</code>被修改过的时候,<code>gettext</code>会将其与它认为的旧有翻译进行匹配,然后标注上fuzzy。看到fuzzy出现的时候,人工翻译者必须检查该条翻译,然后取消fuzzy,之后再行编译。</p>
<h3 id="c9-2-7"><span class="title">2.7</span>国际化URL</h3>
<p>Django提供两种国际化URL的特性:</p>
<ul>
<li><b>Language prefix in URL patterns</b> 语言前缀URL模式:在URL的前边加上不同的语言前缀构成不同的基础URL</li>
<li><b>Translated URL patterns</b> 翻译URL模式:基础URL相同,把基础URL按照不同语言翻译给用户得到不同语言的URL</li>
</ul>
<p>使用翻译URL模式的优点是对搜索引擎友好。如果采用语言前缀URL,则必须要为每一种语言进行索引,使用翻译URL模式,则一条URL就可以匹配全部语言。下边来看一下两种模式的使用:</p>
<h4 id="c9-2-7-1"><span class="title">2.7.1</span><b>语言前缀URL模式</b></h4>
<h3><b></b></h3>
<p>Django可以为不同语言在URL前添加前缀,例如我们的网站,英语版以<code>/en/</code>开头,而西班牙语版以<code>/es/</code>开头。</p>
<p>要使用语言前缀URL模式,需要启用<code>LocaleMiddleware</code>中间件,用于从不同的URL中识别语言,在之前我们已经添加过该中间件。</p>
<p>我们来为URL模式增加前缀,现在需要修改项目的根<code>urls.py</code>文件:</p>
<pre>
from django.conf.urls.i18n import i18n_patterns
urlpatterns = <b>i18n_patterns</b>(
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')),
)
</pre>
<p>可以混用未经翻译的标准URL与<code>i18n_patterns</code>类型的URL,使部分URL带有语言前缀,部分不带前缀。但最好只使用翻译URL,以避免把翻译过的URL匹配到未经翻译过的URL模式上。</p>
<p>现在启动站点,到<a href="http://127.0.0.1:8000/" target="_blank">http://127.0.0.1:8000/</a> ,Django的语言中间件会按照之前介绍的顺序来确定本地语言,然后重定向到带有语言前缀的URL。现在看一下浏览器的地址栏,应该是<a href="http://127.0.0.1:8000/en/" target="_blank">http://127.0.0.1:8000/en/</a>。当前语言是由请求头<code>Accept-Language</code>所设置,或者就是<code>LANGUAGE_CODE</code>的设置。</p>
<h4 id="c9-2-7-2"><span class="title">2.7.2</span><b>翻译URL模式</b></h4>
<p>Django支持在URL模式中翻译字符串。针对不同的语言,可以翻译出不同的URL。在<code>urls.py</code>中,使用<code>ugettext_lazy()</code>来标注字符串。</p>
<p>编辑<code>myshop</code>应用的根<code>urls.py</code>,为<code>cart</code>,<code>orders</code>,<code>payment</code>和<code>coupons</code>应用配置URL:</p>
<pre>
<b>from django.utils.translation import gettext_lazy as _</b>
urlpatterns = i18n_patterns(
path(<b>_('admin/')</b>, admin.site.urls),
path(<b>_('cart/')</b>, include('cart.urls', namespace='cart')),
path(<b>_('orders/')</b>, include('orders.urls', namespace='orders')),
path(<b>_('payment/')</b>, include('payment.urls', namespace='payment')),
path(<b>_('coupons/')</b>, include('coupons.urls', namespace='coupons')),
path('rosetta/', include('rosetta.urls')),
path('', include('shop.urls', namespace='shop')),
)
</pre>
<p>编辑<code>orders</code>应用的<code>urls.py</code>文件,修改成如下:</p>
<pre>
<b>from django.utils.translation import gettext_lazy as _</b>
urlpatterns = [
path(<b>_('create/')</b>, views.order_create, name='order_create'),
# ...
]
</pre>
<p>修改<code>payment</code>应用的<code>urls.py</code>文件,修改成如下:</p>
<pre>
<b>from django.utils.translation import gettext_lazy as _</b>
urlpatterns = [
path(<b>_('process/')</b>, views.payment_process, name='process'),
path(<b>_('done/')</b>, views.payment_done, name='done'),
path(<b>_('canceled/')</b>, views.payment_canceled, name='canceled'),
]
</pre>
<p>对于<code>shop</code>应用的URL不需要修改,因为其URL是动态建立的。</p>
<p>执行命令进行编译,更新消息文件:</p>
<pre>django-admin makemessages --all</pre>
<p>启动站点,访问<a href="http://127.0.0.1:8000/en/rosetta/" target="_blank">http://127.0.0.1:8000/en/rosetta/</a>,点击Spanish下的Myshop,可以看到出现了URL对应的翻译。可以点击Untranslated查看所有尚未翻译的字符串,然后输入翻译内容。</p>
<h3 id="c9-2-8"><span class="title">2.8</span>允许用户切换语言</h3>
<p>在之前的工作中,我们配置好了英语和西班牙语的翻译,应该给用户提供切换语言的选项,为此准备给网站增加一个语言选择器,列出所有支持的语言,显示为一系列链接。</p>
<p>编辑<code>shop</code>应用下的<code>base.html</code>,找到下边这三行:</p>
<pre>
<div id="header">
<a href="/" class="logo">{% trans "My shop" %}</a>
</div>
</pre>
<p>将其替换成:</p>
<pre>
<div id="header">
<a href="/" class="logo">{% trans "My shop" %}</a>
<b>{% get_current_language as LANGUAGE_CODE %}</b>
<b>{% get_available_languages as LANGUAGES %}</b>
<b>{% get_language_info_list for LANGUAGES as languages %}</b>
<b><div class="languages"></b>
<b><p>{% trans "Language" %}:</p></b>
<b><ul class="languages"></b>
<b>{% for language in languages %}</b>
<b><li></b>
<b><a href="/{{ language.code }}/"</b>
<b>{% if language.code == LANGUAGE_CODE %} class="selected"{% endif %}></b>
<b>{{ language.name_local }}</b>
<b></a></b>
<b></li></b>
<b>{% endfor %}</b>
<b></ul></b>
<b></div></b>
</div>
</pre>
<p>这个就是我们的语言选择器,逻辑如下:</p>
<ol>
<li>页面的最上方加载<code>{% load i18n %}</code></li>
<li>使用<code>{% get_current_language %}</code>标签用于获取当前语言</li>
<li>使用<code>{% get_available_languages %}</code>标签用于从<code>LANGUAGES</code>里获取所有可用的支持语言</li>
<li>使用<code>{% get_language_info_list %}</code>是为了快速获取语言的属性而设置的变量</li>
<li>用循环列出了所有可支持的语言,对于当前语言设置CSS类为<code>select</code></li>
</ol>
<p>启动站点到<a href="http://127.0.0.1:8000/">http://127.0.0.1:8000/</a> ,可以看到页面右上方出现了语言选择器,如下图:</p>
<p><img src="http://img.conyli.cc/django2/C09-08.jpg" alt=""></p>
<h3 id="c9-2-9"><span class="title">2.9</span>使用django-parler翻译模型</h3>
<p>Django没有提供直接可用的模型翻译功能,必须采用自己的方式实现模型翻译。有一些第三方工具可以翻译模型字段,每个工具存储翻译的方式都不相同。其中一个工具叫做<code>django-parler</code>,提供了高效的翻译管理,还能够与管理后台进行集成。</p>
<p><code>django-parler</code>的工作原理是为每个模型建立一个对应的翻译数据表,表内每条翻译记录通过外键连到翻译文字所在的模型,表内还有一个<code>language</code>字段,用于标记是何种语言。</p>
<h4 id="c9-2-9-1"><span class="title">2.9.1</span><b>安装django-parler</b></h4>
<p>使用<code>pip</code>安装<code>django-parler</code>:</p>
<pre>
pip install django-parler==1.9.2
</pre>
<p>在<code>settings.py</code>内激活该应用:</p>
<pre>
INSTALLED_APPS = [
# ...
<b>'parler',</b>
]
</pre>
<p>继续添加下列设置:</p>
<pre>
PARLER_LANGUAGES = {
None: (
{'code': 'en'},
{'code': 'es'},
),
'default': {
'fallback': 'en',
'hide_untranslated': False,
}
}
</pre>
<p>该配置的含义是指定了<code>django-parler</code>的可用语言为<code>en</code>和<code>es</code>,然后指定了默认语言为<code>en</code>,然后指定<code>django-parler</code>不要隐藏未翻译的内容。</p>
<h4 id="c9-2-9-2"><span class="title">2.9.2</span><b>翻译模型字段</b></h4>
<p>我们为商品品类添加翻译。<code>django-parler</code>提供一个<code>TranslatableModel</code>类<span style="color: red;">(此处作者原文有误,写成了<code>TranslatedModel</code>)</span>和<code>TranslatedFields</code>方法来翻译模型的字段。编辑<code>shop</code>应用的<code>models.py</code>文件,添加导入语句:</p>
<pre>from parler.models import TranslatableModel, TranslatedFields
</pre>
<p>然后修改<code>Category</code>模型的<code>name</code>和<code>slug</code>字段:</p>
<pre>
class Category(<b>TranslatableModel</b>):
<b>translations = TranslatedFields(</b>
<b>name=models.CharField(max_length=200, db_index=True),</b>
<b>slug=models.SlugField(max_length=200, db_index=True, unique=True)</b>
<b>)</b>
</pre>
<p><code>Category</code>类现在继承了<code>TranslatableModel</code>类,而不是原来的<code>models.Model</code>,<code>name</code>和<code>slug</code>字段被包含在了<code>TranslatedFields</code>包装器里。</p>
<p>编辑<code>Product</code>,<code>name</code>,<code>slug</code>,<code>description</code>,和上边一样的方式:</p>
<pre>
class Product(<b>TranslatableModel</b>):
<b>translations = TranslatedFields(</b>
<b>name=models.CharField(max_length=200, db_index=True),</b>
<b>slug=models.SlugField(max_length=200, db_index=True),</b>
<b>description=models.TextField(blank=True)</b>
<b>)</b>
category = models.ForeignKey(Category, related_name='products')
image = models.ImageField(upload_to='products/%Y/%m/%d', blank=True)
price = models.DecimalField(max_digits=10, decimal_places=2)
available = models.BooleanField(default=True)
created = models.DateTimeField(auto_now_add=True)
updated = models.DateTimeField(auto_now=True)
</pre>
<p><code>django-parler</code>通过新创建模型为其他模型提供翻译,在下图可以看到<code>Product</code>与其对应的翻译模型<code>ProductTranslation</code>之间的关系:</p>
<p><img src="http://img.conyli.cc/django2/C09-10.jpg" alt=""></p>
<p class="emp">译者注:此时如果运行站点,一些IDE会提示模型的字段找不到,这个对于实际运行程序没有影响,该字段依然可用。</p>
<p><code>django-parler</code>生成的<code>ProductTranslation</code>类包含<code>name</code>,<code>slug</code>,<code>description</code>,和一个<code>language_code</code>字段,还有一个外键连接到<code>Product</code>类,针对一个<code>Product</code>模型,会按照每种语言生成一个对应的<code>ProductTranslation</code>对象。</p>
<p>由于翻译的部分和原始的类是独立的两个模型,因此一些ORM的功能无法使用,比如不能在<code>Product</code>类中根据一个翻译后的字段进行排序,也不能在<code>Meta</code>类的<code>ordering</code>属性中使用翻译的字段。</p>
<p>所以编辑<code>shop</code>应用的<code>models.py</code>文件,注释掉<code>ordering</code>设置:</p>
<pre>
class Category(TranslatableModel):
# ...
class Meta:
<b># ordering = ('name',)</b>
verbose_name = 'category'
verbose_name_plural = 'categories'
</pre>
<p>对于<code>Product</code>类,也要注释掉<code>ordering</code>,还需要注释掉<code>index_together</code>,这是因为目前的<code>django-parler</code>不支持联合索引的验证关系。如下图:</p>
<pre>
class Product(TranslatableModel):
# ...
class Meta:
<b>pass</b>
<b># ordering = ('name',)</b>
<b># index_together = (('id', 'slug'),)</b>
</pre>
<p class="emp">译者注:原书在这里遗漏了pass,不要忘记加上。</p>
<p>关于<code>django-parler</code>的兼容性,可以在<a href="https://django-parler.readthedocs.io/en/latest/compatibility.html" target="_blank">https://django-parler.readthedocs.io/en/latest/compatibility.html</a>查看。</p>
<h4 id="c9-2-9-3"><span class="title">2.9.3</span><b>将<code>django-parler</code>集成到管理后台</b></h4>
<p><code>django-parler</code>易于集成到django管理后台中,包含一个<code>TranslatableAdmin</code>类代替了原来的<code>ModelAdmin</code>类。</p>
<p>编辑<code>shop</code>应用的<code>admin.py</code>文件,导入该类:</p>
<pre>
from parler.admin import TranslatableAdmin
</pre>
<p>修改<code>CategoryAdmin</code>和<code>ProductAdmin</code>类,使其继承<code>TranslatableAdmin</code>而不是<code>ModelAdmin</code>类,<code>django-parler</code>不支持<code>prepopulated_fields</code>属性,但支持相同功能的<a
href="https://docs.djangoproject.com/en/2.1/_modules/django/contrib/admin/options/" target="_blank">get_prepopulated_fields()方法</a>,因此将两个类修改如下:</p>
<pre>
from django.contrib import admin
from .models import Category, Product
<b>from parler.admin import TranslatableAdmin</b>