如何在 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.jsonapp/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 发生变化且加载器重新运行加载不同的消息时,所有组件会自动显示新的翻译,无需更改代码。