# General Translation Knowledge Base: 如何翻译 Flask 或 FastAPI 应用？
URL: https://generaltranslation.com/zh/kb/translate-python-app.mdx
Docs index: https://generaltranslation.com/llms.txt
Description: 标记面向用户的字符串，为每个请求确定区域设置（通常依据 Accept-Language），然后加载该区域设置的译文。Flask 应用通常使用 Flask-Babel 搭配 gettext 消息目录，FastAPI 应用则通过中间件接入 Babel。General Translation 的 CLI 可以填充这些消息目录，而其实验性的 gt-flask 和 gt-fastapi SDK 无需消息目录，即可翻译内联的 t() 字符串。
Category: 国际化
Last updated: 2026-10-28
Authors: General Translation Team
Published: 2026-10-28

Flask 和 FastAPI 都没有内置翻译支持，因此你需要自行补上三样东西：标记面向用户字符串的方式、为每个请求确定语言的方式，以及所支持的每种语言的译文。成熟的做法是使用 Babel 的 gettext 消息目录；较新的做法则将字符串以内联形式保留在代码中，并以 JSON 格式加载译文。无论选择哪种做法，语言都应跟随请求而非进程，因为同一台服务器会同时用多种语言响应不同的用户。

## 多语言 Python Web 应用需要什么

* **标记字符串。** 代码调用 `gettext()` 或 [`t()`](/docs/python/reference/functions/t) 之类的函数，而不是直接返回裸字符串；模板则使用 `_()` 或 `{% trans %}`。有了标记，工具才能找到每个字符串并查找对应的译文。
* **为每个请求确定区域设置。** 通常取自 `Accept-Language` 请求头，并可由 URL 前缀、cookie 或账户设置覆盖。Flask 将其保存在请求上下文中；FastAPI 在单个事件循环上并发处理请求，因此区域设置存放在 `contextvars.ContextVar` 中，由每个请求各自设置。
* **每个区域设置的译文。** gettext 加载编译后的 `.mo` 消息目录；General Translation SDK 则为每个区域设置加载一个 JSON 文件。无论采用哪种方式，缺失的译文都会回退到源文本，且不会报错，因此未翻译的字符串只会显示为英文，而不会导致崩溃。

## 选项 1：使用 Babel 管理 gettext 消息目录

[Babel](https://babel.pocoo.org/en/latest/cmdline.html) 会将已标记的字符串提取到 POT 模板中，为每种语言创建和更新 PO 目录，并将其编译为 `.mo` 文件。它还能按任意区域设置格式化日期、数字和货币。

### 在 Flask 中使用 Flask-Babel

[Flask-Babel](https://python-babel.github.io/flask-babel/) (4.0.0) 将 Babel 接入 Flask 的请求和 Jinja 模板。你需要提供一个 `locale_selector`，用于返回每个请求的区域设置：

```python title="app.py"
from flask import Flask, render_template, request
from flask_babel import Babel, gettext, ngettext


def select_locale():
    # 从 Accept-Language 请求头中选出与你所支持语言最匹配的一项
    return request.accept_languages.best_match(["en", "es", "pl"])


app = Flask(__name__)
babel = Babel(app, locale_selector=select_locale)


@app.route("/inbox/<int:count>")
def inbox(count):
    return {
        "title": gettext("Inbox"),
        # ngettext 会根据当前区域设置选择复数形式
        "unread": ngettext("You have %(num)d unread message.", "You have %(num)d unread messages.", count),
    }


@app.route("/")
def home():
    return render_template("home.html", name="Alice")
```

模板使用 `_()` 标记字符串，例如 `{{ _("Welcome back, %(name)s!", name=name) }}`。`babel.cfg` 文件用于告诉 `pybabel` 去哪里查找字符串，消息目录则通过以下四个命令进行管理：

```bash
# babel.cfg 包含 [python: **.py] 和 [jinja2: **/templates/**.html]
pybabel extract -F babel.cfg -o messages.pot .
pybabel init -i messages.pot -d translations -l es    # 每种语言执行一次
pybabel update -i messages.pot -d translations        # 每次提取后执行，作用于所有语言
pybabel compile -d translations
```

使用 General Translation 的 CLI 生成的西班牙语目录时，带有 `Accept-Language: es-MX,es;q=0.9` 的请求返回了“Bandeja de entrada”和“Tienes 5 mensajes sin leer.”。而波兰语目录虽已创建但内容为空，此时波兰语请求返回的所有字符串均为英语，且没有任何警告。

### 在 FastAPI 中使用 Babel

FastAPI 没有类似 Flask-Babel 的官方方案。你可以使用 [fastapi-babel](https://pypi.org/project/fastapi-babel/)、[starlette-babel](https://pypi.org/project/starlette-babel/) 等第三方软件包，也可以自行在中间件中加载 Babel 的消息目录。下面的实现读取同一个 `translations` 目录，并将每个请求对应的目录保存在 `ContextVar` 中：

```python title="main.py"
from contextvars import ContextVar
from functools import lru_cache

from babel.core import negotiate_locale
from babel.support import Translations
from fastapi import FastAPI, Request

SUPPORTED = ["en", "es", "pl"]
current = ContextVar("translations")


@lru_cache
def load(locale: str) -> Translations:
    # 读取由 pybabel 编译的 translations/<locale>/LC_MESSAGES/messages.mo
    return Translations.load("translations", [locale])


def _(message: str) -> str:
    return current.get().gettext(message)


def preferred_locales(header: str) -> list[str]:
    """Accept-Language tags, most preferred first, without q=0 entries."""
    weighted = []
    for part in header.split(","):
        tag, sep, param = part.strip().partition(";")
        try:
            q = float(param.strip().removeprefix("q=")) if sep else 1.0
        except ValueError:
            q = 0.0
        # q=0 表示“不接受”，因此跳过
        if tag and q > 0:
            weighted.append((q, tag.replace("-", "_")))
    # sorted() 是稳定排序，因此权重相同的标签会保持其在请求头中的原有顺序
    return [tag for q, tag in sorted(weighted, key=lambda pair: -pair[0])]


app = FastAPI()


@app.middleware("http")
async def select_locale(request: Request, call_next):
    preferred = preferred_locales(request.headers.get("accept-language", ""))
    current.set(load(negotiate_locale(preferred, SUPPORTED) or "en"))
    return await call_next(request)


@app.get("/inbox/{count}")
def inbox(count: int):
    t = current.get()
    return {
        "title": _("Inbox"),
        "unread": t.ngettext("You have %(num)d unread message.", "You have %(num)d unread messages.", count) % {"num": count},
    }
```

在 Uvicorn 下运行时，`Accept-Language: es-MX,es;q=0.9,en;q=0.8` 返回西班牙语字符串，`de,en;q=0.5` 回退到英语，`en;q=0.5,es` 返回西班牙语，`es;q=0,en` 则返回英语。`negotiate_locale` 会将 `es_MX` 匹配到 `es`。`preferred_locales` 按 `q` 权重对各标签排序，并丢弃 `q=0` 的标签 (`q=0` 表示用户不接受该语言) 。

### Babel 方案留给你的工作

Babel 负责管理消息目录，但不会替你翻译。每个新增或修改的字符串，都要等人逐一为每种语言填写 `msgstr` (波兰语还要填满全部三个复数形式槽位) ，再运行 `pybabel compile` 并重新部署。 (有关该格式以及如何检查出损坏的条目，请参阅 [How do I translate PO and POT files?](/kb/translate-po-files)；各工具分别涵盖哪些功能，请参阅 [General Translation vs. Babel and Flask-Babel](/kb/general-translation-vs-flask-babel)。) Django 应用也是同样的模式，只是改用其自带的命令。 (请参阅 [General Translation vs. Django&#39;s translation framework](/kb/general-translation-vs-django-i18n)。)

## 为每个请求选择区域设置

`Accept-Language` 会列出用户偏好的语言，并可附带 `q` 权重。你要做的是将其与你支持的语言进行匹配，其余情况则使用默认值。Werkzeug 中 `request.accept_languages` 的 `best_match()` 和 Babel 的 `negotiate_locale` 都会从 `es-MX` 这类区域标签回退到 `es`。用户的明确选择应优先于该请求头：例如，为需要被搜索引擎索引的页面使用 `/es/` URL 前缀，或使用语言切换器设置的 cookie。 (完整的信号优先顺序请参阅[如何检测用户的浏览器语言？](/kb/detect-user-language)。) Node.js 服务器则通过 `AsyncLocalStorage` 解决同样的问题。 (请参阅[如何翻译 Node.js 应用？](/kb/translate-node-app)。)

## 方案对比

| 方案                                              | 字符串的编写方式                                                 | 按请求确定区域设置               | 由谁编写译文                     | 状态                          |
| ----------------------------------------------- | -------------------------------------------------------- | ----------------------- | -------------------------- | --------------------------- |
| Flask-Babel 搭配 PO 消息目录                          | `gettext`、`ngettext`、Jinja `_()`                         | `locale_selector`       | 你或译者，按消息目录逐一处理               | 稳定                          |
| 在 FastAPI 中间件中使用 Babel                          | 通过 `ContextVar` 调用 `gettext`                             | 你自己的中间件                 | 你或译者，按消息目录逐一处理               | 稳定，需自行组装                    |
| 每次请求调用机器翻译 API                                  | 在运行时发送原始字符串                                              | 由你传入目标语言                | 由 API 在每次请求时翻译             | 若不做缓存，每次调用都会增加延迟和费用         |
| Babel 消息目录搭配 General Translation 的 CLI          | 与 Babel 相同                                               | 与 Babel 相同              | General Translation，在部署前完成 | CLI 已稳定；Babel 保持不变          |
| General Translation 的 `gt-flask` 或 `gt-fastapi` | 内联 [`t()`](/docs/python/reference/functions/t)，使用 ICU 消息 | 内置 `Accept-Language` 匹配 | General Translation，在部署前完成 | 实验性 (0.1.0) ；已加载的译文 60 秒后过期 |

如果你已有消息目录或译者，请使用 Flask-Babel，或在 FastAPI 中间件中使用 Babel。如果希望每次变更后都能自动为所有语言补全全部消息目录，而无需再走一轮人工翻译，可在此基础上引入 General Translation 的 CLI。如果你更倾向于将字符串内联编写、不维护消息目录文件，可以先在原型项目中试用 `gt-flask` 或 `gt-fastapi`，并锁定版本。只有对仅在运行时才产生的内容 (例如用户发布的帖子) ，才应在请求时调用机器翻译 API。

## General Translation 如何翻译 Flask 和 FastAPI 应用

### 保留 Babel 并翻译消息目录

General Translation 的 CLI `gt` 会读取英文 PO 消息目录，并将其他语言的消息目录写入 Babel 预期的位置。先用 `pybabel init -l en` 创建英文消息目录 (只需执行一次) ，之后每次提取完成后用 `pybabel update -l en` 更新该消息目录，然后在 `gt.config.json` 中将其指向该消息目录：

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": ["es", "pl"],
  "files": {
    "pot": {
      "include": ["translations/[locale]/LC_MESSAGES/messages.po"],
      "transformationFormat": "PO"
    }
  }
}
```

经 `gt` 2.26.11 验证，CLI 会将 `translations/en/LC_MESSAGES` 中的英文消息目录映射到同级的 `es` 和 `pl` 文件夹。先运行 [`npx gt translate`](/docs/cli/reference/commands/translate)，再运行 `pybabel compile -d translations`。General Translation 会为每种目标语言填充所有复数槽位，并检查每个复数形式中的占位符 (例如 `%(num)d`) 。借助[上下文分组](/docs/platform/dashboard/guides/defining-context-for-translations)中的词汇表和自定义提示词，可让术语与你的其他项目保持一致。CLI 基于 Node.js 运行，因此运行 CLI 的机器需要安装 Node；你的应用照常加载已编译的消息目录，运行时无需依赖 General Translation。

### 或者使用 gt-flask 和 gt-fastapi

General Translation 的 Python SDK `gt-flask` 和 `gt-fastapi` (PyPI 上的 0.1.0 版本，来自 [gt-python](https://github.com/generaltranslation/gt-python) 代码仓库) 目前仍处于实验阶段，后续发布中可能会有变动。它们需要 Python 3.10 或更高版本。字符串以 ICU 消息的形式保留在代码中，并传给 [`t`](/docs/python/reference/functions/t)，变量值则通过关键字参数传入；[`initialize_gt`](/docs/python/reference/functions/initialize-gt) 负责配置 `Accept-Language` 匹配。下面这个 Flask 应用从本地 JSON 文件加载译文：

```python title="app.py"
import json
from pathlib import Path

from flask import Flask
from gt_flask import initialize_gt, t, get_locale


def load_translations(locale: str) -> dict[str, str]:
    # 即 gt CLI 写入 files.gt.output 所指路径的文件
    path = Path("translations") / f"{locale}.json"
    return json.loads(path.read_text()) if path.exists() else {}


app = Flask(__name__)
# 在 Flask 中需在此处传入 locales，以便启动时加载译文
initialize_gt(app, default_locale="en", locales=["es", "fr"], load_translations=load_translations)


@app.route("/")
def index():
    return {
        "message": t("Hello, {name}!", name="Alice"),
        "unread": t("{count, plural, one {You have # unread message.} other {You have # unread messages.}}", count=3),
        "locale": get_locale(),
    }
```

如果使用 FastAPI，则从 `gt_fastapi` 导入，创建 `FastAPI()`，并使用 `@app.get`；在这种情况下，[`initialize_gt`](/docs/python/reference/functions/initialize-gt) 会添加一个 HTTP 中间件，并在应用的 lifespan 中加载译文，因此只需配置 `gt.config.json` 中的 locales 即可。使用 General Translation 的 CLI 生成的 `translations/es.json` 后，两个应用的响应如下：

```text title="Output"
Accept-Language: es-MX,es;q=0.9   {"locale": "es", "message": "¡Hola, Alice!", "unread": "Tienes 3 mensajes sin leer."}
Accept-Language: de                {"locale": "en", "message": "Hello, Alice!", "unread": "You have 3 unread messages."}
```

这些文件由 CLI 生成。在 `requirements.txt` 或 `pyproject.toml` 中声明 `gt-flask` 或 `gt-fastapi` 依赖，以便 CLI 能够识别，然后在 `gt.config.json` 中设置输出路径：

```json title="gt.config.json"
{
  "defaultLocale": "en",
  "locales": ["es", "fr"],
  "files": { "gt": { "output": "translations/[locale].json" } }
}
```

[`npx gt translate`](/docs/cli/reference/commands/translate) 会扫描 `.py` 文件中的 [`t()`](/docs/python/reference/functions/t) 调用 (跳过虚拟环境、测试和迁移文件) ，完成翻译后为每个区域设置写入一个文件。在本应用中，[`npx gt validate`](/docs/cli/reference/commands/validate) 无需调用 API 即报告 `gt-flask` 有 2 个可翻译条目。如需改为从 General Translation 的 CDN 加载，请在配置中设置 `projectId` 和 `cacheUrl` (参见[选择译文的交付方式](/docs/python/guides/configuring#delivery)) 。这些 SDK 无需 API 密钥；使用本地文件，运行时便不会产生任何网络请求。

在 0.1.0 版本中，[`t()`](/docs/python/reference/functions/t) 从内存缓存读取译文，该缓存 60 秒后过期，且在这条读取路径上不会重新加载。因此，长时间运行的服务器在缓存过期后会一直返回源字符串，直到加载器再次运行。 (参见[预加载备注](/docs/python/guides/configuring#preload)。) 在后续版本发布修复此问题之前，生产环境流量请使用由 CLI 填充的 Babel 消息目录，`gt-flask` 和 `gt-fastapi` 仅限用于原型开发。

## 常见问题

### gt-flask 会取代 Flask-Babel 的日期和货币格式化功能吗？

不会。在 0.1.0 版本中，`gt-flask` 和 `gt-fastapi` 导出的是翻译和区域设置相关的辅助函数 ([`t`](/docs/python/reference/functions/t)、[`get_locale`](/docs/python/reference/functions/get-locale)、[`declare_var`](/docs/python/reference/functions/declare-var) 等) ，因此按区域设置进行格式化时，请继续使用 Babel 的 `format_date` 和 `format_currency`。

### 如何让用户自行选择语言，而不是依赖 Accept-Language？

使用 Flask-Babel 时，在 `locale_selector` 中读取 cookie 或 URL 路径段即可。使用 General Translation SDK 时，向 [`initialize_gt`](/docs/python/reference/functions/initialize-gt) 传入一个 [`get_locale`](/docs/python/reference/functions/get-locale) 回调。该回调会取代基于请求头的匹配，因此需要由你自行返回一个支持的语言区域。 (参见[检测请求区域设置](/docs/python/guides/detecting-locale)。)

### 如何翻译数据库内容或用户帖子？

目录和 [`t()`](/docs/python/reference/functions/t) 用于处理代码中的字符串。对于仅在运行时才存在的内容，需要在其创建或被请求时调用翻译服务，例如 General Translation 的[翻译内容](/docs/platform/openapi/reference/translation/translate-content)端点 (`POST /v2/projects/{projectId}/translate`) ，并将翻译结果与原文一同存储。

## 要点总结

* Flask 和 FastAPI 需要补充三项内容：已标记的字符串、按请求确定的区域设置，以及各区域设置的译文；FastAPI 会将区域设置保存在 `ContextVar` 中。
* 在 Flask 中，Flask-Babel 借助 `locale_selector` 以及 `pybabel extract`、[`init`](/docs/cli/reference/commands/init)、`update` 和 `compile` 完成处理；FastAPI 则在中间件中加载 Babel 的 `Translations`。
* 消息目录为空或缺失时会静默回退到英语，因此剩下的工作就是填好每一个 `msgstr`。
* General Translation 的 CLI 可通过 `"pot"` 和 `"transformationFormat": "PO"` 填充 Babel 的 PO 消息目录 (包括复数形式槽位) ，你的应用照常加载编译后的消息目录即可。
* General Translation 实验性的 `gt-flask` 和 `gt-fastapi` 0.1.0 会根据 CLI 写入的 JSON 文件翻译内联的 [`t()`](/docs/python/reference/functions/t) 字符串；在 Flask 中，需要将 `locales` 传给 [`initialize_gt`](/docs/python/reference/functions/initialize-gt)，并在承接生产环境流量前考虑好 60 秒缓存过期的影响。

## Related questions

- [General Translation 与 Babel、Flask-Babel 对比：该选哪一个？](https://generaltranslation.com/zh/kb/general-translation-vs-flask-babel.mdx): Babel 可以为任意区域设置格式化日期、数字和货币，并管理 gettext 消息目录；Flask-Babel 则负责将其集成到 Flask 中，但两者都不会翻译你的字符串。General Translation 的 CLI 会为每种语言填充 Babel 的 PO 文件（包括复数形式），因此你可以继续使用 Flask-Babel，不必再手动翻译消息目录。
- [如何翻译 PO 和 POT 文件？](https://generaltranslation.com/zh/kb/translate-po-files.mdx): 填写每一个 msgstr（包括 Plural-Forms 头部字段要求的每种复数形式），并保持 msgid、msgctxt 和占位符不变。运行 msgfmt --check 可检查头部信息、复数形式数量，以及带有 format 标志的条目中的占位符；此外还需单独检查是否有空的 msgstr。General Translation 的 CLI 会根据你的 POT 模板，为每种语言生成一个翻译好的 PO 文件。
- [如何检测用户的浏览器语言？](https://generaltranslation.com/zh/kb/detect-user-language.mdx): 读取浏览器提供的有序语言列表（在 JavaScript 中为 navigator.languages，在服务器端为 Accept-Language 标头），按 q 值排序，再与你支持的语言区域进行匹配，并设置后备语言区域。Safari 和 Chrome 无痕模式只会报告一种语言，而 IP 地理定位只能反映用户所在位置，无法反映其使用的语言。
- [如何翻译 Node.js 应用？](https://generaltranslation.com/zh/kb/translate-node-app.mdx): 检测每个请求的语言（通常依据其 Accept-Language 请求头），并以该语言返回所有字符串。General Translation 的 gt-node 通过 withGT 为每个请求绑定区域设置，并通过 getGT 翻译字符串，所用译文由 npx gt translate --publish 在部署前生成。
- [General Translation 与 Django 翻译框架：两者都需要吗？](https://generaltranslation.com/zh/kb/general-translation-vs-django-i18n.mdx): 将 Django 的翻译框架与 General Translation 搭配使用。Django 负责消息提取、语言选择和渲染；General Translation 则基于你的产品术语和仪表板审阅，填充 Django 的 PO 文件（包括复数形式）。你的模板和 URL 模式无需改动，译文会随代码同步更新。

## Previous and next questions

- Previous question: [如何在 Payload CMS 中翻译内容？](https://generaltranslation.com/zh/kb/translate-payload-content.mdx)
- Next question: [如何翻译 Lottie 动画？](https://generaltranslation.com/zh/kb/translate-lottie-animations.mdx)

## Sitemap

See the [Knowledge Base sitemap](https://generaltranslation.com/zh/kb/sitemap.md) for every article.
