Litefs 国际化(i18n)

更新日期: 2026-10-21 适用版本: v0.9.0(开发中)

概述

Litefs 内置国际化支持,基于 Python 标准库 gettext 实现,零新增第三方依赖,提供:

  • locale 协商:解析 Accept-Language 请求头,结合 URL 参数与 Cookie 选定本次请求的语言

  • 请求级 locale:request.locale、g.locale 与线程局部存储三处同步写入,视图内可直接取用

  • 翻译函数:模块级 gettext / ngettext 与模板上下文注入的 _ / gettext / ngettext

  • 中间件:同步 / 异步双路径实现,响应阶段回写 Content-Language 响应头

设计原则:

原则

实现方式

零行为变化

未启用 i18n 时注入恒等函数,模板渲染结果与未注入前完全一致

标准兼容

翻译文件遵循 gettext 标准目录结构,可直接用 msgfmt / pybabel 生成

请求隔离

locale 存于 threading.local,并发请求互不污染

显式优先

URL 参数 > Cookie > Accept-Language > 默认语言,符合用户预期

依赖隔离

litefs.middleware 通过 PEP 562 延迟导出 LocaleMiddleware,避免循环导入

快速开始

启用国际化

from litefs.core import Litefs

app = Litefs(
    i18n_enabled=True,
    i18n_supported_locales=['zh_CN', 'en'],
    i18n_default_locale='zh_CN',
    i18n_locale_dir='locales',
)

启用后框架会自动挂载 LocaleMiddleware,无需手工 add_middleware。

config.yaml 配置方式:

i18n_enabled: true
i18n_supported_locales: ['zh_CN', 'en']
i18n_default_locale: zh_CN
i18n_locale_dir: locales
i18n_cookie_name: locale
i18n_param_name: locale

手工挂载中间件

from litefs.core import Litefs
from litefs.middleware import LocaleMiddleware

app = Litefs()
app.add_middleware(
    LocaleMiddleware,
    supported_locales=['zh_CN', 'en'],
    default_locale='zh_CN',
    locale_dir='locales',
)

模板中使用

<h1>${_('Welcome')}</h1>
<p>${_('You have %s messages') % message_count}</p>

render_template() 会自动向模板上下文注入 _ / gettext / ngettext 三个函数。

视图中使用

from litefs.routing import get
from litefs.i18n import gettext, ngettext

@get('/index', name='index')
def index_handler(request):
    # request.locale 由 LocaleMiddleware 写入
    title = gettext('Welcome')          # 使用当前请求的 locale
    items = ngettext('item', 'items', len(items))
    return {'title': title, 'items': items}

翻译文件组织

翻译文件遵循 gettext 标准目录结构:

locales/
├── zh_CN/
│   └── LC_MESSAGES/
│       └── messages.mo
└── en/
    └── LC_MESSAGES/
        └── messages.mo
  • locales/ 为 i18n_locale_dir 指定的根目录

  • zh_CN/ 为 locale 目录名(下划线格式,不是 zh-CN)

  • messages.mo 为翻译域(domain)对应的编译产物,默认域为 messages

生成翻译文件

# 1. 提取源码中的可翻译字符串
xgettext -L Python -o messages.pot src/**/*.py

# 2. 创建并编辑翻译
msginit -i messages.pot -l zh_CN -o zh_CN.po
vim zh_CN.po

# 3. 编译为 .mo
msgfmt -o locales/zh_CN/LC_MESSAGES/messages.mo zh_CN.po

模板字符串需要在源码中显式标记,可使用 pybabel 的提取指令:

<% trans name="title">Welcome</% trans %>

或直接使用 Mako 的 Python 表达式形式 $_('Welcome')(_ 由框架注入)。

复数形式

.mo 文件中的复数条目 msgid 形如 单数\x00复数,由 msgfmt 根据 Plural-Forms 头自动生成:

ngettext('apple', 'apples', 3)   # 中文返回「苹果们」

locale 协商

优先级

URL 查询参数 (?locale=)  >  Cookie  >  Accept-Language 请求头  >  i18n_default_locale

来源

读取方式

说明

URL 参数

request.get[param_name]

显式指定,优先级最高,如 ?locale=en

Cookie

HTTP_COOKIE 中的 cookie_name

由上一次响应自动回写,记住用户选择

请求头

HTTP_ACCEPT_LANGUAGE

浏览器自动发送

默认值

i18n_default_locale

兜底

匹配规则

对每个候选 locale,按以下顺序匹配受支持列表:

  1. 精确匹配:zh_CN 命中 zh_CN

  2. 前缀回退:zh_CN 回退到 zh

  3. 未命中:继续尝试下一个候选,最终使用默认 locale

Accept-Language 解析规则:

  • 按 q 值降序排列,q 值相同时保持请求头中的原始顺序(稳定排序)

  • 非法 q 值记为 0.0

  • * 通配符视为「接受任意语言」,直接采用默认 locale

示例:

Accept-Language: zh-CN,zh;q=0.9,en;q=0.8

若受支持列表为 ['zh_CN', 'en'],则 zh-CN 精确命中 zh_CN,最终 locale 为 zh_CN。

locale 规范化

normalize_locale() 统一不同书写形式:

输入

输出

说明

zh-CN

zh_CN

连字符转下划线,地区码转大写

zh_cn

zh_CN

地区码转大写

zh

zh

纯语言码转小写

es-419

es_419

3 位数字地区码补零

zh-Hans-CN

zh_Hans_CN

变体标识保持原样

请求级 locale

协商完成后,locale 同时写入三处:

位置

类型

用途

request.locale

请求处理器实例属性

视图内直接读取

g.locale

请求上下文全局对象

跨模块共享(无请求上下文时自动忽略)

线程局部存储

threading.local

翻译函数隐式取用,无需层层传参

@get('/index', name='index')
def index_handler(request):
    print(request.locale)   # 'zh_CN'

翻译目录切换

除 locale 外,翻译目录也支持请求级切换(set_current_locale_dir()), 适用于多租户场景——不同租户使用不同的翻译文件目录。

LocaleMiddleware 会在每次请求时把自身配置的 locale_dir 同步到线程局部存储, 因此 i18n_locale_dir 配置对模板翻译函数自动生效:

app = Litefs(
    i18n_enabled=True,
    i18n_locale_dir='/srv/tenants/acme/locales',   # 模板翻译函数自动使用该目录
)

也可以在代码中手动切换(会覆盖中间件写入的值):

from litefs.i18n import set_current_locale_dir

set_current_locale_dir('/srv/tenants/acme/locales')

模板翻译函数注入

注入规则

inject_translations() 向模板上下文注入三个键:

键

对应函数

_

gettext 别名(最常用)

gettext

翻译单数形式文本

ngettext

按数量翻译复数形式文本

两个关键约束:

  1. 不覆盖已有键(使用 setdefault)——调用方显式传入的同名变量优先级最高

  2. 无 locale 时注入恒等函数——无请求上下文、未启用 i18n 或翻译表缺失时, 渲染结果与未注入前完全一致

# 调用方显式传入的 _ 不会被覆盖
handler.render_template('a.html', value='x', _=my_func)

在模板中切换语言

## 简体中文
${_('Welcome')}
## English
${translator.gettext('Welcome')}

需要固定语言的场景(如多语言邮件),可显式构造翻译器:

from litefs.i18n import Translator

translator = Translator('locales', 'en')
translator.gettext('Welcome')

配置项

配置项

默认值

说明

i18n_enabled

False

是否启用国际化(启用后自动挂载中间件)

i18n_default_locale

'en'

默认 locale

i18n_supported_locales

['en']

受支持的 locale 列表

i18n_locale_dir

'locales'

翻译文件根目录

i18n_cookie_name

'locale'

存放 locale 的 Cookie 名称

i18n_param_name

'locale'

存放 locale 的查询参数名称

API 参考

litefs.i18n.locale

名称

说明

normalize_locale(locale)

规范化 locale 字符串

parse_accept_language(header)

解析请求头,返回 [(locale, q), ...](q 降序)

match_locale(requested, supported)

在受支持列表中匹配,返回命中的 locale 或 None

negotiate_locale(supported, default_locale, accept_language, cookie_locale, param_locale)

完整协商流程

WILDCARD

通配符常量 *

litefs.i18n.translator

名称

说明

get_translations(locale, locale_dir, domain)

获取(并缓存)翻译表,缺失时回退 NullTranslations

clear_translation_cache()

清空翻译表缓存(测试与热重载场景)

Translator(locale_dir, locale, domain)

绑定固定 locale 的翻译器,提供 gettext / ngettext

set_current_locale(locale) / get_current_locale()

请求级 locale 读写

set_current_locale_dir(path) / get_current_locale_dir()

请求级翻译目录读写

install_translations(locale, locale_dir, domain)

安装到 gettext 内置函数

gettext(message, locale)

请求级翻译(模块级函数)

ngettext(singular, plural, n, locale)

请求级复数翻译(模块级函数)

litefs.i18n.template

名称

说明

inject_translations(context, locale)

向模板上下文注入翻译函数

make_translation_callables(locale)

构造独立翻译函数字典

最佳实践

  1. locale 目录名统一用下划线。zh_CN 而不是 zh-CN,否则 gettext 找不到文件。

  2. .mo 文件要进版本库或构建产物。部署时确保 i18n_locale_dir 指向实际存在的路径。

  3. 翻译缺失不报错。gettext 对未收录的字符串原样返回,缺失 .mo 时同样回退原文, 因此可以安全地「先上线英文,再逐步补翻译」。

  4. CDN 缓存要带 Content-Language。多语言页面必须按该头区分缓存,否则会出现语言串台。

  5. 后台任务显式指定 locale。Celery 等无请求上下文的场景,用 Translator(dir, locale) 显式绑定, 不要依赖线程局部状态。

  6. 翻译字符串用占位符。使用 %(name)s 形式而非字符串拼接,避免语序调整时破坏语义。

相关文档

  • :doc:mail-service —— 邮件发送(多语言邮件模板)

  • :doc:middleware-guide —— 中间件系统总览

  • :doc:configuration —— 配置项完整列表

  • :doc:celery-integration —— Celery 任务队列集成