如何在 React Router v7 中从文件加载翻译内容
将可翻译内容与代码分离
问题
将面向用户的字符串直接硬编码到组件中,会导致内容与代码紧密耦合。每增加一种新语言,开发者都需要修改实现文件,扩展条件逻辑,增加系统复杂度。当文案发生变更时,即使只是微小的措辞调整,也需要重新部署代码。这种方式让翻译流程依赖于工程周期,阻碍了非技术团队成员独立管理内容。
随着应用规模扩大,分散在各处的字符串字面量变得难以追踪和维护。要在整个代码库中查找某个短语的所有出现位置非常容易出错,而要保证类似消息的一致性,几乎不可能做到,除非有一个集中的内容源。
解决方案
将所有可翻译字符串提取到按语言组织的外部 JSON 文件中,每个语言一个文件。在组件中用消息标识符替换硬编码字符串,这些标识符对应 JSON 文件中的条目。运行时,应用会根据用户的语言环境加载相应的翻译文件,并将这些消息提供给国际化库,由其将标识符解析为对应的翻译内容。
这种分离方式让译者可以直接操作 JSON 文件,无需接触代码;内容更新只需简单修改文件;每种语言的字符串都有唯一的内容源,便于统一管理。
步骤
1. 为每个语言环境创建翻译文件
将翻译文件集中存放在专用目录下,每种语言一个 JSON 文件。每个文件结构为扁平对象,将消息标识符映射到对应的翻译字符串。
{
"welcome.title": "Welcome back",
"welcome.subtitle": "Continue where you left off",
"nav.home": "Home",
"nav.about": "About",
"nav.contact": "Contact"
}
将此文件保存为 app/translations/en.json(英文),然后为其他语言创建类似的文件,如 app/translations/es.json 和 app/translations/fr.json,并填入相应的翻译内容。所有文件应使用一致的键名,以便同一标识符在不同语言环境下能正确解析为对应的翻译。
2. 在路由加载器中加载翻译内容
使用路由加载器在渲染前获取当前 locale 的翻译文件,确保组件挂载时消息已可用。
import type { Route } from "./+types/root";
import enMessages from "./translations/en.json";
import esMessages from "./translations/es.json";
import frMessages from "./translations/fr.json";
const messages: Record<string, Record<string, string>> = {
en: enMessages,
es: esMessages,
fr: frMessages,
};
export async function loader({ request }: Route.LoaderArgs) {
const url = new URL(request.url);
const locale = url.searchParams.get("locale") || "en";
return {
locale,
messages: messages[locale] || messages.en,
};
}
加载器会从 URL 查询参数中读取 locale,并返回 locale 及其对应的消息。组件可以通过 loaderData 访问这些数据,用于配置国际化提供程序。
3. 使用加载的消息配置 IntlProvider
用来自 react-intl 的 IntlProvider 包裹应用,并传入从加载器数据获取的 locale 和消息。
import { IntlProvider } from "react-intl";
import { Outlet } from "react-router";
import type { Route } from "./+types/root";
export default function Root({ loaderData }: Route.ComponentProps) {
return (
<IntlProvider locale={loaderData.locale} messages={loaderData.messages}>
<html lang={loaderData.locale}>
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<Outlet />
</body>
</html>
</IntlProvider>
);
}
IntlProvider 会通过 React context 向所有子组件提供 locale 和消息。子路由通过 Outlet 渲染,并继承对翻译数据的访问权限。
4. 在组件中通过标识符引用消息
用 FormattedMessage 组件替换硬编码字符串,通过消息标识符引用翻译文件中的内容。
import { FormattedMessage } from "react-intl";
export default function Welcome() {
return (
<div>
<h1>
<FormattedMessage id="welcome.title" />
</h1>
<p>
<FormattedMessage id="welcome.subtitle" />
</p>
<nav>
<a href="/">
<FormattedMessage id="nav.home" />
</a>
<a href="/about">
<FormattedMessage id="nav.about" />
</a>
<a href="/contact">
<FormattedMessage id="nav.contact" />
</a>
</nav>
</div>
);
}
每个 FormattedMessage 组件会在 IntlProvider 提供的 messages 对象中查找其 id,并渲染对应的翻译字符串。当 locale 发生变化且加载器重新运行加载不同的消息时,所有组件会自动显示新的翻译,无需更改代码。