如何在 TanStack Start v1 中从文件加载翻译
将可翻译内容与代码分离
问题
将面向用户的字符串直接硬编码到组件中,会导致内容与代码之间的紧密耦合。每次字符串发生更改或添加新语言时,开发人员都必须定位并修改源文件,然后重新部署应用程序。这种方法使得翻译工作流依赖于工程周期,并阻止非技术团队成员更新文案。随着支持语言数量的增加,用于选择正确字符串的条件逻辑变得难以管理且容易出错。其结果是迭代速度变慢、维护成本增加,以及代码库中充斥着本应存放在其他地方的可翻译内容。
解决方案
通过将所有可翻译字符串存储在外部 JSON 文件中(每个语言环境一个文件),将它们与应用程序代码分离。为每条消息定义一个稳定的键,并在组件中引用这些键,而不是直接使用文本。在运行时,根据用户的语言环境加载相应的翻译文件,并将这些消息提供给 react-intl 的 IntlProvider。这将内容与代码解耦:翻译人员可以直接处理 JSON 文件,文案更改无需修改代码,添加新语言只需添加一个新文件,而无需修改组件。
步骤
1. 为每个语言环境创建翻译文件
将翻译内容组织为 JSON 文件,存放在专用目录中,每个语言环境一个文件,包含所有消息的键值对。
{
"welcome": "欢迎回来",
"greeting": "你好,{name}",
"itemCount": "{count, plural, =0 {没有项目} one {一个项目} other {# 个项目}}"
}
将此文件保存为 app/translations/zh.json。为其他语言环境创建对应的文件,例如 app/translations/es.json 和 app/translations/fr.json,使用相同的键但翻译后的值。
2. 使用服务器函数加载翻译
使用服务器函数根据请求的语言环境从磁盘读取翻译文件,确保在 SSR(服务器端渲染)期间加载翻译,并在导航时在客户端获取。
import { createServerFn } from "@tanstack/react-start";
import * as fs from "node:fs";
export const getMessages = createServerFn({ method: "GET" }).handler(
async ({ request }) => {
const url = new URL(request.url);
const locale = url.searchParams.get("locale") || "en";
const filePath = `app/translations/${locale}.json`;
const content = await fs.promises.readFile(filePath, "utf-8");
return JSON.parse(content);
},
);
此函数读取指定语言环境的 JSON 文件并返回解析后的消息对象。它仅在服务器上运行,确保文件系统访问的安全性。
3. 创建一个帮助函数以确定用户的语言环境
定义一个小型实用工具,从请求中提取语言环境或回退到默认值,使其可以在各个路由中重复使用。
export function getLocaleFromRequest(request: Request): string {
const url = new URL(request.url);
const localeParam = url.searchParams.get("locale");
if (localeParam) return localeParam;
const acceptLanguage = request.headers.get("accept-language");
if (acceptLanguage) {
const match = acceptLanguage.split(",")[0].split("-")[0];
return match || "en";
}
return "en";
}
此函数首先检查查询参数,然后检查 Accept-Language 头部,最后默认为英语。它为语言环境检测提供了单一的可信来源。
4. 在路由加载器中加载消息
使用路由加载器在渲染之前为当前语言环境获取消息,使其可用于组件树。
import { createFileRoute } from "@tanstack/react-router";
import { getMessages, getLocaleFromRequest } from "../lib/i18n";
export const Route = createFileRoute("/")({
loader: async ({ context }) => {
const locale = getLocaleFromRequest(context.request);
const messages = await getMessages({ data: { locale } });
return { locale, messages };
},
component: HomePage,
});
function HomePage() {
const { locale, messages } = Route.useLoaderData();
return (
<div>
<p>{messages.welcome}</p>
</div>
);
}
加载器调用服务器函数以检索消息,组件通过 useLoaderData 访问它们。此模式适用于 SSR 和客户端导航。
5. 提供消息给 react-intl
使用 IntlProvider 包裹您的组件树,传递加载的语言环境和消息,这样所有子组件都可以访问翻译内容。
import { IntlProvider } from "react-intl";
function HomePage() {
const { locale, messages } = Route.useLoaderData();
return (
<IntlProvider locale={locale} messages={messages}>
<AppContent />
</IntlProvider>
);
}
function AppContent() {
return (
<div>
<FormattedMessage id="welcome" />
</div>
);
}
IntlProvider 将语言环境和消息提供给所有 react-intl 组件和钩子。组件现在可以通过 FormattedMessage 或 useIntl 引用消息的键,并根据加载的语言环境渲染正确的翻译。
6. 在组件中通过键引用消息
使用 react-intl 的 FormattedMessage 组件或 useIntl 钩子来显示翻译字符串,引用 JSON 文件中定义的键。
import { FormattedMessage, useIntl } from "react-intl";
function UserGreeting({ name }: { name: string }) {
const intl = useIntl();
const title = intl.formatMessage({ id: "greeting" }, { name });
return (
<div>
<h1 title={title}>
<FormattedMessage id="greeting" values={{ name }} />
</h1>
<p>
<FormattedMessage id="itemCount" values={{ count: 5 }} />
</p>
</div>
);
}
FormattedMessage 会内联渲染翻译字符串,而 useIntl().formatMessage 返回一个字符串,可用于属性或 JavaScript 逻辑。两者都接受 values 用于插值,并支持 ICU 消息语法以处理复数和格式化。