如何在 React Router v7 中从文件加载翻译

将可翻译内容与代码分离

问题

将面向用户的字符串直接硬编码到组件中,会导致内容与代码之间的紧密耦合。每增加一种新语言,开发人员都需要修改实现文件,扩展条件逻辑,从而增加复杂性。当文案发生变化时,即使是微小的措辞调整也需要重新部署代码。这种方法使得翻译工作流依赖于工程周期,并且阻止了非技术团队成员独立管理内容。

随着应用程序的增长,分散的字符串文本变得难以追踪和维护。在代码库中找到某个短语的每个出现位置容易出错,并且在类似消息中保持一致性几乎是不可能的,除非有一个集中的真源。

解决方案

将所有可翻译的字符串提取到按语言组织的外部 JSON 文件中,每个语言环境一个文件。用消息标识符替换组件中的硬编码字符串,这些标识符引用这些文件中的条目。在运行时,应用程序根据用户的语言环境加载相应的翻译文件,并将这些消息提供给国际化库,国际化库将标识符解析为其翻译值。

这种分离允许翻译人员直接处理 JSON 文件而无需接触代码,使内容更新可以通过简单的文件更改完成,并为每种语言的字符串提供单一的真源。

步骤

1. 为每个语言环境创建翻译文件

在专用目录中组织翻译文件,每种语言一个 JSON 文件。将每个文件结构化为一个平面对象,将消息标识符映射到翻译字符串。

{
"welcome.title": "欢迎回来",
"welcome.subtitle": "继续您上次的进度",
"nav.home": "首页",
"nav.about": "关于",
"nav.contact": "联系"
}

将此文件保存为 app/translations/en.json(英文),然后为其他语言创建类似的文件,例如 app/translations/es.jsonapp/translations/fr.json,并添加相应的翻译。在所有文件中使用一致的键,这样相同的标识符可以在每种语言环境中解析为适当的翻译。

2. 在路由加载器中加载翻译

使用路由加载器在渲染之前获取当前语言环境的翻译文件。这确保了组件挂载时消息是可用的。

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 查询参数中读取语言环境,并返回语言环境及其对应的消息。组件可以通过 loaderData 访问这些数据,以配置国际化提供程序。

3. 使用加载的消息配置 IntlProvider

使用 react-intl 的 IntlProvider 包裹您的应用程序,并传递来自加载器数据的语言环境和消息。

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 上下文将语言环境和消息提供给所有后代组件。子路由通过 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 提供的消息对象中查找其 id,并渲染对应的翻译字符串。当语言环境更改并且加载器使用不同的消息重新运行时,所有组件会自动显示新的翻译,而无需更改代码。