目录
国际化与本地化的区别
国际化(internationalization)
为本地化准备软件。通常由开发者完成。
本地化(localization)
编写翻译和本地格式化。通常由翻译者完成。
请求参数中包含中文
接着上一篇笔记,不修改之前的程序,直接在调用时传入 UTF-8
编码的中文,看一看效果。
注意: MySQL 数据库的字符集应该是
utf8mb4
。
- 方法一: 建立一个 add_cars.bat 文件,文件使用
UTF-8
编码,文件内容为:
@ECHO OFF
curl -X POST http://127.0.0.1:8000/cars/ -d "brand=红旗&model=E-HS9&classification=2&color=黑色/量子银灰"
执行上面的批处理(返回内容是汉字的 Unicode 转义字符):
C:\>add_cars.bat
{"car": {"id": 8, "brand": "\u7ea2\u65d7", "model": "E-HS9", "class": 2, "color": "\u9ed1\u8272/\u91cf\u5b50\u94f6\u7070"}}
- 方法二: 先使用 django 提供的
iri_to_uri()
将中文转换为UTF-8
编码形式的 URI 字符串
iri_to_uri()
比urllib.parse.quote()
更安全,因为它不会对%
进行转换,iri_to_uri()
是幂等的,多次调用没有重复转换的风险。同样的,uri_to_iri()
也是幂等的,比urllib.parse.unquote()
更安全。
>>> from django.utils.encoding import iri_to_uri
>>> iri_to_uri('brand=红旗&model=H5&classification=1&color=极光蓝')
'brand=%E7%BA%A2%E6%97%97&model=H5&classification=1&color=%E6%9E%81%E5%85%89%E8%93%9D'
在命令窗口调用 curl:
C:\>curl -X POST http://127.0.0.1:8000/cars/ -d "brand=%E7%BA%A2%E6%97%97&model=H5&classification=1&color=%E6%9E%81%E5%85%89%E8%93%9D"
{"car": {"id": 9, "brand": "\u7ea2\u65d7", "model": "H5", "class": 1, "color": "\u6781\u5149\u84dd"}}
URL 中包含中文
- 修改 cars/urls.py ,增加按品牌查询车辆的 URL pattern:
from django.urls import path
from .views import MyCars, MyCar, get_brand_cars
urlpatterns = [
path('', MyCars.as_view()),
path('<int:id>/', MyCar.as_view()),
path('<str:brand>/', get_brand_cars),
]
- 修改 cars/forms.py ,增加一个校验参数合法性的表单
BrandForm
:
from django import forms
from .models import Car
class CarForm(forms.ModelForm):
class Meta:
model = Car
fields = ['brand', 'model', 'classification', 'color']
class BrandForm(forms.Form):
brand = forms.CharField(label='Brand', max_length=20)
- 修改 cars/views.py ,增加按品牌查询车辆的函数
get_brand_cars
:
...
from .forms import BrandForm
def get_brand_cars(request, brand):
'''
获取品牌车辆列表
'''
form = BrandForm({'brand': brand})
if form.is_valid():
cars = Car.objects.filter(brand=form.cleaned_data['brand'])
data = []
for car in cars:
data.append({
'id': car.pk,
'brand': car.brand,
'model': car.model,
'class': car.classification,
'color': car.color
})
return JsonResponse({
'message': 'Success',
'count': len(data),
'cars': data
})
else:
return JsonResponse({'error': form.errors})
...
- 发起调用:
C:\>curl http://127.0.0.1:8000/cars/%E7%BA%A2%E6%97%97/
{"message": "Success", "count": 2, "cars": [{"id": 8, "brand": "\u7ea2\u65d7", "model": "E-HS9", "class": 2, "color": "\u9ed1\u8272/\u91cf\u5b50\u94f6\u7070"}, {"id": 9, "brand": "\u7ea2\u65d7", "model": "H5", "class": 1, "color": "\u6781\u5149\u84dd"}]}
C:\>curl http://127.0.0.1:8000/cars/aaaaaaaaaaaaaaaaaaaaaa/
{"error": {"brand": ["Ensure this value has at most 20 characters (it has 22)."]}}
翻译文本
可见,django 默认就是支持 UTF-8
编码的,但响应的内容可能还是英语(准确地说是原始字符串)。要实现对返回内容的翻译,需要启用国际化机制和本地化中间件。
- 默认情况下,django 的国际化机制是打开的,即在 settings.py 中:
USE_I18N = True
USE_L10N = True # 该参数在4.x版本中被移除,默认为True;在5.x版本中被废弃,总是支持本地化。
- 激活本地化中间件,修改 settings.py 的
MIDDLEWARE
配置,添加LocaleMiddleware
:
注意:
LocaleMiddleware
必须在SessionMiddleware
之后 和CommonMiddleware
之前。
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.locale.LocaleMiddleware', #必须在这里
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
- 使用
gettext_lazy
或gettext
对代码中需要翻译的文本进行标记:
# cars/forms.py
...
from django.utils.translation import gettext_lazy as _
...
class BrandForm(forms.Form):
brand = forms.CharField(label=_('Brand'), max_length=20) # Brand 待翻译
# cars/views.py
...
from django.utils.translation import gettext_lazy as _
def get_brand_cars(request, brand):
...
if form.is_valid():
...
return JsonResponse({
'message': _('Success'), # Success 待翻译
'count': len(data),
'cars': data
})
else:
return JsonResponse({'error': form.errors})
...
什么时候用
gettext_lazy
,什么时候用gettext
?
- 在表单、模型的定义中一定要使用
gettext_lazy
,在视图函数里可以使用gettext
。- 或者,只要
gettext_lazy
不会报错就用gettext_lazy
,否则用gettext
。
关于 惰性翻译 的详细说明参考这里。
下面是一个如果使用 gettext_lazy
会报错的例子,这里只能使用 gettext
:
...
import json
from django.http.response import HttpResponse
from django.utils.translation import gettext as _
...
return HttpResponse(json.dumps({
'message': _('Success'),
'count': len(data),
'cars': data
}), content_type='application/json')
- Windows 环境需自己安装 GNU gettext 程序以便
makemessages
工作。解压后,需将 bin 加入 %PATH% 。 - 创建 cars/locale 目录:
(.djenv) PS C:\django-projects\mygarage> mkdir cars\locale
- 执行
makemessages
命令生成待翻译的消息文件(注意zh_Hans
的连接符和大小写):
(.djenv) PS C:\django-projects\mygarage> python manage.py makemessages -l zh_Hans
zh_Hans
与zh_CN
的区别和关系看这里。
- 翻译消息文件 cars/locale/zh_Hans/LC_MESSAGES/django.po(可借助工具 如 Poedit):
如果消息文件中有
fuzzy
标记,则意味着该条消息可能有歧义,不会被翻译。如果你确定没有问题,删除fuzzy
标记,重新编译消息即可。[参考]
# SOME DESCRIPTIVE TITLE.
# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
# This file is distributed under the same license as the PACKAGE package.
# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-03-23 18:30+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
#: .\cars\forms.py:13
msgid "Brand"
msgstr "品牌"
#: .\cars\views.py:32
msgid "Success"
msgstr "成功"
- 执行
compilemessages
命令编译消息文件:
(.djenv) PS C:\django-projects\mygarage> python manage.py compilemessages
- 启动服务,并进行调用:
C:\>curl http://127.0.0.1:8000/cars/Benz/ -H "Accept-Language: zh-CN"
{"message": "\u6210\u529f", "count": 1, "cars": [{"id": 3, "brand": "Benz", "model": "CLS", "class": 1, "color": ""}]}
C:\>curl http://127.0.0.1:8000/cars/Benz/ -H "Accept-Language: en-US"
{"message": "Success", "count": 1, "cars": [{"id": 3, "brand": "Benz", "model": "CLS", "class": 1, "color": ""}]}
C:\>curl http://127.0.0.1:8000/cars/Benzzzzzzzzzzzzzzzzzzz/ -H "Accept-Language: zh-CN"
{"error": {"brand": ["\u786e\u4fdd\u8be5\u53d8\u91cf\u5305\u542b\u4e0d\u8d85\u8fc7 20 \u5b57\u7b26 (\u76ee\u524d\u5b57\u7b26\u6570 22)\u3002"]}}
>>> print("\u786e\u4fdd\u8be5\u53d8\u91cf\u5305\u542b\u4e0d\u8d85\u8fc7 20 \u5b57\u7b26 (\u76ee\u524d\u5b57\u7b26\u6570 22)\u3002")
确保该变量包含不超过 20 字符 (目前字符数 22)。
LocaleMiddleware 如何确定响应什么语言?
首先,它在请求的 URL 中寻找语言前缀。 只有当你在你的根 URLconf 中使用
i18n_patterns()
函数时,才会这样做。参见 国际化:在 URL 模式中 了解更多关于语言前缀和如何国际化 URL 模式的信息。如果失败,它将查找 cookie。所使用的 cookie 的名称由
LANGUAGE_COOKIE_NAME
设定。(默认名称是 django_language)。如果失败了,它将查看
Accept-Language
HTTP 头。这个头由你的浏览器发送,并告诉服务器你喜欢哪种语言,按优先级排序。Django 会尝试每一种语言,直到找到可用的翻译。如果不行,则使用全局
LANGUAGE_CODE
设置。
通常,我愿意使用 Accept-Language
的方式,当客户端没有发送这个 HTTP 头时, settings.py 中的 LANGUAGE_CODE
将起作用。例如,该参数默认值是 en-us
,调用结果如下:
C:\>curl http://127.0.0.1:8000/cars/Benz/
{"message": "Success", "count": 1, "cars": [{"id": 3, "brand": "Benz", "model": "CLS", "class": 1, "color": ""}]}
locale name 和 language code 的区别
连接符 | 区分大小写 | 举例 | |
---|---|---|---|
local name | _ | Yes. 国家部分如果超过两个字符,则首字母大写,否则全部大写。 | en_US, zh_Hans |
language code | - | No. | en-us, en-US, zh-hans |
时区基本用法
当启用对时区的支持时,Django 在数据库中以 UTC 为单位存储日期时间信息,在内部使用具有时区的日期时间对象,并在模板和表单中将其转换为最终用户的时区。
MySQL 的时区参数
time_zone
必须设置为 “+00:00”
使用 startproject
命令创建的 settings.py 中,关于时区的参数默认值为:
TIME_ZONE = 'UTC'
USE_TZ = True
将上述 TIME_ZONE 修改为 Asia/Shanghai
:
TIME_ZONE = 'Asia/Shanghai'
修改 cars/views.py,在 MyCars.get()
的返回信息中增加记录的创建时间 utc_time
、beijing_time
和 paris_time
:
...
from django.utils import timezone
import zoneinfo
...
class MyCars(View):
def get(self, request):
'''
获取所有车辆列表
'''
cars = Car.objects.all()
data = []
for car in cars:
beijing_time = timezone.localtime(car.created_time)
paris_time = car.created_time.astimezone(zoneinfo.ZoneInfo("Europe/Paris"))
data.append({
'id': car.pk,
'brand': car.brand,
'model': car.model,
'class': car.classification,
'color': car.color,
'utc_time': car.created_time,
'beijing_time': beijing_time,
'paris_time': paris_time
})
return JsonResponse({'cars': data})
...
调用服务,得到如下结果:
C:\>curl http://127.0.0.1:8000/cars/
{"cars": [{"id": 6, "brand": "Audi", "model": "R8", "class": 5, "color": "white", "utc_time": "2023-03-22T09:32:16.495Z", "beijing_time": "2023-03-22T17:32:16.495+08:00", "paris_time": "2023-03-22T10:32:16.495+01:00"}, ...]}
关于时区的其他内容
- Python
datetime.datetime.now()
返回的是 无时区信息 的系统本地时间
(.djenv) PS C:\django-projects\mygarage> python manage.py shell
Python 3.9.6 (tags/v3.9.6:db3ff76, Jun 28 2021, 15:26:21) [MSC v.1929 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import datetime
>>> datetime.datetime.now()
datetime.datetime(2023, 3, 24, 17, 13, 48, 830459)
django.utils.timezone.now()
返回的是 带时区信息 的 UTC 时间
>>> from django.utils import timezone
>>> timezone.now()
datetime.datetime(2023, 3, 24, 9, 14, 5, 628021, tzinfo=datetime.timezone.utc)
django.utils.timezone.localtime()
返回当前时区的本地时间,或者把某个时间转换为当前时区的本地时间。
>>> timezone.get_default_timezone()
zoneinfo.ZoneInfo(key='Asia/Shanghai')
>>> timezone.get_current_timezone()
zoneinfo.ZoneInfo(key='Asia/Shanghai')
>>> timezone.localtime()
datetime.datetime(2023, 3, 24, 17, 16, 4, 236880, tzinfo=zoneinfo.ZoneInfo(key='Asia/Shanghai'))
- 查看可用时区
>>> import zoneinfo
>>> zoneinfo.available_timezones()
{'Pacific/Noumea', 'America/Argentina/Buenos_Aires', 'Asia/Aqtobe', 'Poland', 'GMT-0', 'Asia/Ashkhabad', ...}
- 将字符串转换为带时区的时间
>>> import zoneinfo
>>> from django.utils.dateparse import parse_datetime
>>> naive = parse_datetime("2012-02-21 10:28:45")
>>> aware = naive.replace(tzinfo=zoneinfo.ZoneInfo("Asia/Shanghai"))
>>> aware
datetime.datetime(2012, 2, 21, 10, 28, 45, tzinfo=zoneinfo.ZoneInfo(key='Asia/Shanghai'))
date()
方法根据时区信息返回当地日期
>>> timezone.now().date()
datetime.date(2023, 3, 27)
>>> timezone.now().astimezone(zoneinfo.ZoneInfo("America/New_York")).date()
datetime.date(2023, 3, 26)
- 配置 MySQL 的时区支持
这段内容也许用不着。如果收到 “Are time zone definitions for your database installed?” 的错误,需要使用 mysql_tzinfo_to_sql 将时区表加载到 MySQL 数据库中。这只需要为 MySQL 服务器做一次。
对于 Linux,执行以下命令:
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql
对于 Windows,下载 sql 文件,然后执行以下命令:
mysql -u root -p mysql < file_name