Django 4 笔记 - 5.翻译与时区

国际化与本地化的区别

国际化(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 中包含中文

  1. 修改 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),
]
  1. 修改 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)
  1. 修改 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})

...
  1. 发起调用:
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 编码的,但响应的内容可能还是英语(准确地说是原始字符串)。要实现对返回内容的翻译,需要启用国际化机制和本地化中间件。

  1. 默认情况下,django 的国际化机制是打开的,即在 settings.py 中:
USE_I18N = True
USE_L10N = True    # 该参数在4.x版本中被移除,默认为True;在5.x版本中被废弃,总是支持本地化。
  1. 激活本地化中间件,修改 settings.pyMIDDLEWARE 配置,添加 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',
]
  1. 使用 gettext_lazygettext 对代码中需要翻译的文本进行标记:
# 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')
  1. Windows 环境需自己安装 GNU gettext 程序以便 makemessages 工作。解压后,需将 bin 加入 %PATH%
  2. 创建 cars/locale 目录:
(.djenv) PS C:\django-projects\mygarage> mkdir cars\locale
  1. 执行 makemessages 命令生成待翻译的消息文件(注意 zh_Hans 的连接符和大小写):
(.djenv) PS C:\django-projects\mygarage> python manage.py makemessages -l zh_Hans

zh_Hanszh_CN 的区别和关系看这里

  1. 翻译消息文件 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 "成功"
  1. 执行 compilemessages 命令编译消息文件:
(.djenv) PS C:\django-projects\mygarage> python manage.py compilemessages
  1. 启动服务,并进行调用:
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_timebeijing_timeparis_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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值