作者:
    Creation:2025-09-09Last update:2026-08-30

    使用Intlayer翻译您的Tanstack Start | 国际化(i18n)

    目录

    本指南演示如何在 Tanstack Start 项目中集成 Intlayer,实现无缝国际化,支持基于区域设置的路由、TypeScript 支持以及现代开发实践。

    为什么选择 Inlayer 而不是替代品?

    与“react-i18next”或“use-intl”或“paraglide”等主要解决方案相比,Intlayer是一个具有集成优化的解决方案,例如:

    Intlayer 针对 TanStack Start 进行了全面优化,提供多语言路由cookie 管理站点地图生成动态内容加载以及扩展国际化 (i18n) 工作所需的所有功能。

    不要将大量 JSON 文件加载到页面中,而只需加载必要的内容。 Intlayer 有助于将捆绑包和页面大小减少多达 50%

    确定应用程序内容的范围有利于大型应用程序的维护。您可以复制或删除单个功能文件夹,而无需承担检查整个内容代码库的精神负担。此外,Intlayer 具有完全类型化 (fully typed),以确保您的内容的准确性。

    共置内容减少大型语言模型 (LLM) 所需的上下文。 Intlayer 还附带了一套工具,例如用于测试缺失翻译的 CLILSPMCPagent skills,使 AI 代理的开发者体验 (DX) 更加流畅。

    使用您选择的法学硕士,通过自动化在 CI/CD 管道中进行翻译,而费用由您的 AI 提供商承担。 Intlayer 还提供了一个编译器来自动提取内容,以及一个网络平台来帮助在后台翻译

    将大量 JSON 文件连接到组件可能会导致性能和反应性问题。 Intlayer 可在构建时 (build time)优化您的内容加载。

    Intlayer 不仅仅是一个 i18n 解决方案,还提供了一个自托管的可视化编辑器和一个完整的 CMS 来帮助您管理多语言内容实时,与译员、文案人员和其他团队成员无缝协作。内容可以本地和/或远程存储。

    在 Tanstack Start 应用程序中设置 Intlayer 的分步指南

    youtube.com
    ide.intlayer.org
    intlayer-tanstack-start-template.vercel.app

    在 GitHub 上查看 应用程序模板

    1. 创建项目

      首先,按照 TanStack Start 网站上的 开始新项目 指南创建一个新的 TanStack Start 项目。

    2. 安装 Intlayer 包

      使用您首选的包管理器安装必要的包:

      bash
      npx intlayer init --interactive
      
      --interactive 标志是可选的。如果您是 AI 代理,请使用 intlayer-cli init
      此命令将检测您的环境并安装所需的包。例如:
      bash
      npm install intlayer react-intlayer
      npm install vite-intlayer --save-dev
      
      • intlayer

        核心包,为配置管理、翻译、内容声明、转译和 CLI 命令 提供国际化工具。

      • react-intlayer 将 Intlayer 与 React 应用程序集成的包。为 React 国际化提供上下文提供程序和 hooks。

      • vite-intlayer 包含用于将 Intlayer 与 Vite bundler 集成的 Vite 插件,以及用于检测用户首选语言、管理 cookies 和处理 URL 重定向的 middleware。

    3. 配置您的项目

      架构

      在此架构中,所有本地化路由都嵌套在 {-$locale} 路由段下。这种方法确保每种语言都拥有专用的 URL,同时支持自动区域前缀、验证和 SEO 优化。

      bash
      .
      ├── src
         ├── components
         ├── Header.tsx
         ├── locale-switcher.content.ts
         ├── locale-switcher.tsx
         └── localized-link.tsx
         ├── hooks
         ├── useI18nHTMLAttributes.tsx
         └── useLocalizedNavigate.ts
         ├── routes
         ├── {-$locale}
         ├── 404.content.ts
         ├── 404.tsx
         ├── about.content.ts
         ├── about.tsx
         ├── index.content.tsx
         ├── index.tsx
         └── route.tsx             # Locale layout & prefix validation
         ├── __root.tsx                # Root route with IntlayerProvider
         └── sitemap[.]xml.ts
         ├── router.tsx
         └── styles.css
      ├── intlayer.config.ts
      ├── package.json
      ├── tsconfig.json
      └── vite.config.ts
      

      配置

      创建一个配置文件来配置您应用程序的语言:

      intlayer.config.ts
      import type { IntlayerConfig } from "intlayer";
      
      import { Locales } from "intlayer";
      
      const config: IntlayerConfig = {
        internationalization: {
          defaultLocale: Locales.ENGLISH,
          locales: [Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH],
        },
      };
      
      export default config;
      
      通过此配置文件,您可以设置本地化 URL、middleware 重定向、cookie 名称、内容声明的位置和扩展名、禁用控制台中的 Intlayer 日志等。有关可用参数的完整列表,请参考 配置文档
    4. 在 Vite 配置中集成 Intlayer

      将 intlayer 插件添加到您的配置中:

      vite.config.ts
      import { tanstackStart } from "@tanstack/react-start/plugin/vite";
      import viteReact from "@vitejs/plugin-react";
      import { nitro } from "nitro/vite";
      import { defineConfig } from "vite";
      import { intlayer } from "vite-intlayer";
      
      const config = defineConfig({
        plugins: [
          nitro(),
          intlayer({
            proxy: {
              ignore: (req) => req.url?.startsWith("/api"),
            },
          }),
          tanstackStart({
            router: {
              routeFileIgnorePattern:
                ".content.(ts|tsx|js|mjs|cjs|jsx|json|jsonc|json5|md|mdx|yaml|yml)$",
            },
          }),
          viteReact(),
        ],
      });
      
      export default config;
      
      intlayer() Vite 插件用于将 Intlayer 与 Vite 集成。它确保构建内容声明文件并在开发模式下监视它们。它在 Vite 应用程序中定义 Intlayer 环境变量。此外,它提供别名来优化性能。
    5. 创建根布局

      配置您的根布局以支持国际化,使用 useParams 检测当前语言,并在 html 标签上设置 langdir 属性。

      src/routes/__root.tsx
      import {
        createRootRouteWithContext,
        getRouteApi,
        HeadContent,
        Scripts,
      } from "@tanstack/react-router";
      import { defaultLocale, getHTMLTextDir } from "intlayer";
      import { type ReactNode } from "react";
      import { IntlayerProvider } from "react-intlayer";
      
      const localeRoute = getRouteApi("/{-$locale}");
      
      export const Route = createRootRouteWithContext<{}>()({
        head: () => ({
          meta: [
            {
              charSet: "utf-8",
            },
            {
              content: "width=device-width, initial-scale=1",
              name: "viewport",
            },
            {
              title: "TanStack Start Starter",
            },
          ],
        }),
      
        shellComponent: RootDocument,
      });
      
      function RootDocument({ children }: { children: ReactNode }) {
        const params = localeRoute.useParams();
        const locale = params?.locale ?? defaultLocale;
      
        return (
          <html dir={getHTMLTextDir(locale)} lang={locale}>
            <head>
              <HeadContent />
            </head>
            <body>
              <IntlayerProvider locale={locale}>{children}</IntlayerProvider>
              <Scripts />
            </body>
          </html>
        );
      }
      
    6. 创建语言布局

      创建一个处理语言前缀并执行验证的布局。

      src/routes/{-$locale}/route.tsx
      import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
      import { validatePrefix } from "intlayer";
      
      export const Route = createFileRoute("/{-$locale}")({
        beforeLoad: ({ params }) => {
          const localeParam = params.locale;
      
          // 验证语言前缀
          const { isValid, localePrefix } = validatePrefix(localeParam);
      
          if (!isValid) {
            throw redirect({
              to: "/{-$locale}/404",
              params: { locale: localePrefix },
            });
          }
        },
        component: Outlet,
      });
      
      在这里,{-$locale} 是一个动态路由参数,被替换为当前语言。这种符号使该插槽可选,允许它与路由模式(如 'prefix-no-default' 等)一起工作。

      请注意,如果您在同一路由中使用多个动态段(例如 /{-$locale}/other-path/$anotherDynamicPath/...),此插槽可能会导致问题。 对于 'prefix-all' 模式,您可能更喜欢将插槽切换为 $locale。 对于 'no-prefix''search-params' 模式,您可以完全移除该插槽。

    7. 声明您的内容

      创建和管理您的内容声明以存储翻译:

      src/contents/page.content.ts
      import type { Dictionary } from "intlayer";
      
      import { t } from "intlayer";
      
      const appContent = {
        content: {
          links: {
            about: t({
              zh: "关于",
              en: "About",
              es: "Acerca de",
              fr: "À propos",
            }),
            home: t({
              zh: "首页",
              en: "Home",
              es: "Inicio",
              fr: "Accueil",
            }),
          },
          meta: {
            title: t({
              zh: "欢迎使用 Intlayer + TanStack Router",
              en: "Welcome to Intlayer + TanStack Router",
              es: "Bienvenido a Intlayer + TanStack Router",
              fr: "Bienvenue à Intlayer + TanStack Router",
            }),
            description: t({
              zh: "这是使用 Intlayer 与 TanStack Router 的示例",
              en: "This is an example of using Intlayer with TanStack Router",
              es: "Este es un ejemplo de uso de Intlayer con TanStack Router",
              fr: "Ceci est un exemple d'utilisation d'Intlayer avec TanStack Router",
            }),
          },
        },
        key: "app",
      } satisfies Dictionary;
      
      export default appContent;
      
      您的内容声明可以在应用程序中的任何位置定义,只要它们包含在 contentDir 目录中(默认为 ./app)。并匹配内容声明文件扩展名(默认为 .content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml})。
      有关更多详情,请参考 内容声明文档
    8. 创建语言感知组件和 Hooks

      为语言感知导航创建一个 LocalizedLink 组件:

      src/components/localized-link.tsx
      import type { FC } from "react";
      
      import { Link, type LinkComponentProps } from "@tanstack/react-router";
      import { useLocale } from "react-intlayer";
      import { getPrefix } from "intlayer";
      
      export const LOCALE_ROUTE = "{-$locale}" as const;
      
      export type To = StripLocalePrefix<LinkComponentProps["to"]>;
      
      export type StripLocalePrefix<T extends string | undefined> = T extends
        `/${typeof LOCALE_ROUTE}/` | `/${typeof LOCALE_ROUTE}`
        ? "/"
        : T extends `/${typeof LOCALE_ROUTE}/${infer Rest}`
          ? `/${Rest}`
          : T;
      
      type LocalizedLinkProps = {
        to?: To;
      } & Omit<LinkComponentProps, "to">;
      
      export const LocalizedLink: FC<LocalizedLinkProps> = (props) => {
        const { locale } = useLocale();
        const { localePrefix } = getPrefix(locale);
      
        return (
          <Link
            {...props}
            params={{
              locale: localePrefix,
              ...(typeof props?.params === "object" ? props?.params : {}),
            }}
            to={`/${LOCALE_ROUTE}${props.to}` as LinkComponentProps["to"]}
          />
        );
      };
      

      此组件有两个目标:

      • 从 URL 中删除不必要的 {-$locale} 前缀。
      • 将语言参数注入到 URL 中,以确保用户直接重定向到本地化路由。

      然后我们可以创建一个 useLocalizedNavigate hook 来进行编程导航:

      src/hooks/useLocalizedNavigate.tsx
      import { useNavigate } from "@tanstack/react-router";
      import { getPrefix } from "intlayer";
      import { useLocale } from "react-intlayer";
      import type { StripLocalePrefix } from "@/components/localized-link";
      import type { FileRouteTypes } from "@/routeTree.gen";
      
      type NavigateFn = ReturnType<typeof useNavigate>;
      type BaseNavigateOptions = Parameters<NavigateFn>[0];
      
      type LocalizedTo = StripLocalePrefix<FileRouteTypes["to"]>;
      
      export type LocalizedNavigateOptions = Omit<
        BaseNavigateOptions,
        "to" | "params"
      > & {
        to: LocalizedTo;
        params?: Omit<NonNullable<BaseNavigateOptions["params"]>, "locale">;
      };
      
      type LocalizedNavigate = (
        options: LocalizedNavigateOptions
      ) => ReturnType<NavigateFn>;
      
      export const useLocalizedNavigate = () => {
        const navigate = useNavigate();
      
        const { locale } = useLocale();
      
        const localizedNavigate: LocalizedNavigate = (args: any) => {
          const { localePrefix } = getPrefix(locale);
      
          if (typeof args === "string") {
            return navigate({
              to: `/${LOCALE_ROUTE}${args}`,
              params: { locale: localePrefix },
            });
          }
      
          const { to, ...rest } = args;
      
          const localizedTo = `/${LOCALE_ROUTE}${to}` as any;
      
          return navigate({
            to: localizedTo,
            params: { locale: localePrefix, ...rest } as any,
          });
        };
      
        return localizedNavigate;
      };
      
    9. 在您的页面中使用 Intlayer

      在组件中请默认使用 useIntlayer:这是读取内容的推荐方式,编译器会把它解析为当前渲染的语言环境。仅在 React 树之外(路由 head、loader 和服务端函数)才使用 getIntlayer / getIntlayerAsync

      在整个应用程序中访问您的内容字典:

      本地化主页

      src/routes/{-$locale}/index.tsx
      import { createFileRoute } from "@tanstack/react-router";
      import { useIntlayer } from "react-intlayer";
      
      import LocaleSwitcher from "@/components/locale-switcher";
      import { LocalizedLink } from "@/components/localized-link";
      import { useLocalizedNavigate } from "@/hooks/useLocalizedNavigate";
      
      export const Route = createFileRoute("/{-$locale}/")({
        component: RouteComponent,
      });
      
      function RouteComponent() {
        const content = useIntlayer("app");
        const navigate = useLocalizedNavigate();
      
        return (
          <div>
            <div>
              {content.title}
              <LocaleSwitcher />
              <div>
                <LocalizedLink to="/">{content.links.home}</LocalizedLink>
                <LocalizedLink to="/about">{content.links.about}</LocalizedLink>
              </div>
              <div>
                <button onClick={() => navigate({ to: "/" })}>
                  {content.links.home}
                </button>
                <button onClick={() => navigate({ to: "/about" })}>
                  {content.links.about}
                </button>
              </div>
            </div>
          </div>
        );
      }
      

      如果您想在 string 属性中使用您的内容,例如 alttitlehrefaria-label 等,您可以使用函数的值,如:

      tsx
      <img src={content.image.src.value} alt={content.image.value} />
      <img src={content.image.src.toString()} alt={content.image.toString()} />
      <img src={String(content.image.src)} alt={String(content.image)} />
      
      要了解更多关于 useIntlayer hook 的信息,请参考文档
    10. 创建语言切换器组件

      创建一个组件来允许用户更改语言:

      src/components/locale-switcher.tsx
      import { useLocation } from "@tanstack/react-router";
      import {
        getHTMLTextDir,
        getLocaleName,
        getPathWithoutLocale,
        getPrefix,
        Locales,
      } from "intlayer";
      import type { FC } from "react";
      import { useLocale } from "react-intlayer";
      
      import { LocalizedLink, type To } from "./localized-link";
      
      export const LocaleSwitcher: FC = () => {
        const { pathname } = useLocation();
      
        const { availableLocales, locale, setLocale } = useLocale();
      
        const pathWithoutLocale = getPathWithoutLocale(pathname);
      
        return (
          <ol>
            {availableLocales.map((localeEl) => (
              <li key={localeEl}>
                <LocalizedLink
                  aria-current={localeEl === locale ? "page" : undefined}
                  onClick={() => setLocale(localeEl)}
                  params={{ locale: getPrefix(localeEl).localePrefix }}
                  to={pathWithoutLocale as To}
                >
                  <span>
                    {/* 区域设置 - 例如 FR */}
                    {localeEl}
                  </span>
                  <span>
                    {/* 用其自己的区域设置显示的语言 - 例如 Français */}
                    {getLocaleName(localeEl, locale)}
                  </span>
                  <span dir={getHTMLTextDir(localeEl)} lang={localeEl}>
                    {/* 用当前区域设置显示的语言 - 例如当前区域设置为 Locales.SPANISH 时的 Francés */}
                    {getLocaleName(localeEl)}
                  </span>
                  <span dir="ltr" lang={Locales.ENGLISH}>
                    {/* 用英文显示的语言 - 例如 French */}
                    {getLocaleName(localeEl, Locales.ENGLISH)}
                  </span>
                </LocalizedLink>
              </li>
            ))}
          </ol>
        );
      };
      
      要了解更多关于 useLocale hook 的信息,请参考文档
    11. HTML 属性管理

      如第 5 步所述,您可以在根组件中使用 useParams 来管理 html 标签的 langdir 属性。这确保在服务器和客户端上设置正确的属性。

      src/routes/__root.tsx
      const localeRoute = getRouteApi("/{-$locale}");
      
      function RootDocument({ children }: { children: ReactNode }) {
        const params = localeRoute.useParams();
        const locale = params?.locale ?? defaultLocale;
      
        return (
          <html dir={getHTMLTextDir(locale)} lang={locale}>
            {/* ... */}
          </html>
        );
      }
      
    12. 添加中间件

      您也可以使用 intlayerProxy 为您的应用程序添加服务器端路由。该插件将根据 URL 自动检测当前区域设置并设置适当的区域设置 cookie。如果未指定任何区域设置,该插件将根据用户的浏览器语言偏好确定最合适的区域设置。如果未检测到任何区域设置,它将重定向到默认区域设置。

      注意,要在生产环境中使用 intlayerProxy,您需要将 vite-intlayer 软件包从 devDependencies 切换到 dependencies
      从 Intlayer v9 开始,intlayerProxy() 直接捆绑在 intlayer() 插件中,并通过 routing.enableProxy 选项(默认为 true)默认启用。如下所示分别注册它现在是可选的:为了向后兼容性和需要控制插件顺序的设置而保留。设置 routing.enableProxy: false 以选择退出。请参考 v9 发行说明
      vite.config.ts
      import { tanstackStart } from "@tanstack/react-start/plugin/vite";
      import viteReact from "@vitejs/plugin-react";
      import { nitro } from "nitro/vite";
      import { defineConfig } from "vite";
      import { intlayer } from "vite-intlayer";
      
      export default defineConfig({
        plugins: [
          nitro(),
          intlayer({
            proxy: {
              ignore: (req) => req.url?.startsWith("/api"),
            },
          }),
          tanstackStart({
            router: {
              routeFileIgnorePattern:
                ".content.(ts|tsx|js|mjs|cjs|jsx|json|jsonc|json5|md|mdx|yaml|yml)$",
            },
          }),
          viteReact(),
        ],
      });
      
    13. 国际化您的元数据

      getIntlayer 针对合并的字典(包含每个声明的区域设置的字典)进行同步解析。head 保持同步,不会等待任何内容,但整个多语言字典被拉入发送到浏览器的路由块中。

      src/routes/{-$locale}/index.tsx
      import { createFileRoute } from "@tanstack/react-router";
      import {
        defaultLocale,
        getIntlayer,
        getLocalizedUrl,
        localeMap,
      } from "intlayer";
      
      export const Route = createFileRoute("/{-$locale}/")({
        component: RouteComponent,
        head: ({ params }) => {
          const { locale = defaultLocale } = params;
          const path = "/"; // 此路由的路径
      
          const metaContent = getIntlayer("app", locale);
      
          return {
            links: [
              // 规范链接:指向当前本地化页面
              { rel: "canonical", href: getLocalizedUrl(path, locale) },
      
              // Hreflang:告诉 Google 所有本地化版本
              ...localeMap(({ locale: mapLocale }) => ({
                rel: "alternate",
                hrefLang: mapLocale,
                href: getLocalizedUrl(path, mapLocale),
              })),
      
              // x-default:适用于语言不匹配的用户
              // 定义默认的回退区域设置(通常是您的主要语言)
              {
                rel: "alternate",
                hrefLang: "x-default",
                href: getLocalizedUrl(path, defaultLocale),
              },
            ],
            meta: [
              { title: metaContent.title },
              { name: "description", content: metaContent.meta.description },
            ],
          };
        },
      });
      

      最适合小型元数据字典、少数几个区域设置或原型设计时使用。

      getIntlayerAsync(从 v9.4 开始可用)的行为类似于 getIntlayer,但构建插件将其指向 .intlayer/dynamic_dictionaries/ 中的按区域设置块,而不是合并字典。因此,页面仅发送它呈现的区域设置。由于该块是按需加载的,head 变为 async

      src/routes/{-$locale}/index.tsx
      import { createFileRoute } from "@tanstack/react-router";
      import {
        defaultLocale,
        getIntlayerAsync,
        getLocalizedUrl,
        localeMap,
      } from "intlayer";
      
      export const Route = createFileRoute("/{-$locale}/")({
        component: RouteComponent,
        head: async ({ params }) => {
          const { locale = defaultLocale } = params;
          const path = "/"; // 此路由的路径
      
          const metaContent = await getIntlayerAsync("app", locale);
      
          return {
            links: [
              // 规范链接:指向当前本地化页面
              { rel: "canonical", href: getLocalizedUrl(path, locale) },
      
              // Hreflang:告诉 Google 所有本地化版本
              ...localeMap(({ locale: mapLocale }) => ({
                rel: "alternate",
                hrefLang: mapLocale,
                href: getLocalizedUrl(path, mapLocale),
              })),
      
              // x-default:适用于语言不匹配的用户
              // 定义默认的回退区域设置(通常是您的主要语言)
              {
                rel: "alternate",
                hrefLang: "x-default",
                href: getLocalizedUrl(path, defaultLocale),
              },
            ],
            meta: [
              { title: metaContent.title },
              { name: "description", content: metaContent.meta.description },
            ],
          };
        },
      });
      
      如果 head 读取多个字典,请使用 Promise.all 解析它们:在自己的行上等待每个 getIntlayerAsync 会链接请求,而不是并行运行它们。

      权衡:动态导入在 head 运行时解析,在文档呈现的关键路径上。在冷路由上,这会将 head 延迟几毫秒,可能会略微降低 LCP

      在路由 loader 中解析字典,然后在 head 中从 loaderData 读取。匹配路由的加载程序并行运行,staleTime: Infinity 告诉 TanStack Router 结果永不过期,因此按区域设置块被解析一次,随后从路由器缓存提供,使 head 保持同步。

      src/routes/{-$locale}/index.tsx
      import { createFileRoute } from "@tanstack/react-router";
      import {
        defaultLocale,
        getIntlayerAsync,
        getLocalizedUrl,
        localeMap,
      } from "intlayer";
      
      export const Route = createFileRoute("/{-$locale}/")({
        component: RouteComponent,
        // 与其他匹配的路由并行解析,不在 head 关键路径上
        loader: async ({ params }) => {
          const { locale = defaultLocale } = params;
      
          return { metaContent: await getIntlayerAsync("app", locale) };
        },
        // 给定区域设置的字典永不改变:解析块一次
        staleTime: Infinity,
        head: ({ params, loaderData }) => {
          const { locale = defaultLocale } = params;
          const path = "/"; // 此路由的路径
      
          return {
            links: [
              // 规范链接:指向当前本地化页面
              { rel: "canonical", href: getLocalizedUrl(path, locale) },
      
              // Hreflang:告诉 Google 所有本地化版本
              ...localeMap(({ locale: mapLocale }) => ({
                rel: "alternate",
                hrefLang: mapLocale,
                href: getLocalizedUrl(path, mapLocale),
              })),
      
              // x-default:适用于语言不匹配的用户
              // 定义默认的回退区域设置(通常是您的主要语言)
              {
                rel: "alternate",
                hrefLang: "x-default",
                href: getLocalizedUrl(path, defaultLocale),
              },
            ],
            meta: [
              { title: loaderData?.metaContent.title },
              {
                name: "description",
                content: loaderData?.metaContent.meta.description,
              },
            ],
          };
        },
      });
      
      head 可能在加载程序解决之前被调用,因此 loaderData 的类型可能为 undefined。保持可选链接,或返回回退标题。

      您保持按区域设置块而不在 head 关键路径上支付其成本。代价是开发者体验:内容必须通过 loaderData 从加载程序显式线程化到 head

      我应该选择哪种解析方式?

      静态解析动态解析缓存动态解析
      APIgetIntlayergetIntlayerAsync (v9.4+)getIntlayerAsync in loader (v9.4+)
      head 签名synchronousasyncsynchronous, reads loaderData
      传送的语言版本every declared localerequested locale onlyrequested locale only
      客户端导航nothing to resolvere-entered on every matchserved from the router cache
      开发者体验simplestone awaitcontent threaded through loaderData
    14. 在服务器操作中检索语言

      您可能希望从服务器操作或 API 端点内访问当前语言。 您可以使用 intlayer 中的 getLocale 帮助器来实现此目的。

      以下是使用 TanStack Start 服务器函数的示例:

      src/routes/{-$locale}/index.tsx
      import { createServerFn } from "@tanstack/react-start";
      import {
        getRequestHeader,
        getRequestHeaders,
      } from "@tanstack/react-start/server";
      import { getCookie, getIntlayer, getLocale } from "intlayer";
      
      export const getLocaleServer = createServerFn().handler(async () => {
        const locale = await getLocale({
          // 从请求中获取 cookie (默认: 'INTLAYER_LOCALE')
          getCookie: (name) => {
            const cookieString = getRequestHeader("cookie");
      
            return getCookie(name, cookieString);
          },
          // 从请求中获取标头 (默认: 'x-intlayer-locale')
          // 使用 Accept-Language 协商的回退
          getHeader: (name) => getRequestHeader(name),
        });
      
        // 使用 getIntlayerAsync() 检索一些内容
        const content = getIntlayer("app", locale);
      
        return { locale, content };
      });
      
    15. 管理未找到页面

      当用户访问不存在的页面时,你可以显示自定义的未找到页面,语言区域前缀可能会影响未找到页面的触发方式。

      理解 TanStack Router 的 404 处理与 Locale 前缀

      在 TanStack Router 中,处理带本地化路由的 404 页面需要采用多层方法:

      1. 专用 404 路由:用于显示 404 UI 的特定路由
      2. 路由级验证:验证 locale 前缀并将无效的前缀重定向到 404
      3. 捕获所有路由:捕获 locale 段内任何不匹配的路径
      src/routes/{-$locale}/404.tsx
      import { createFileRoute } from "@tanstack/react-router";
      
      // 这创建了一个专用的 /[locale]/404 路由
      // 它既可以作为直接路由使用,也可以作为组件导入到其他文件中
      export const Route = createFileRoute("/{-$locale}/404")({
        component: NotFoundComponent,
      });
      
      // 单独导出以便在 notFoundComponent 和捕获所有路由中重复使用
      export function NotFoundComponent() {
        return (
          <div>
            <h1>404</h1>
          </div>
        );
      }
      
      src/routes/{-$locale}/route.tsx
      import { createFileRoute, Outlet, redirect } from "@tanstack/react-router";
      import { validatePrefix } from "intlayer";
      import { NotFoundComponent } from "./404";
      
      export const Route = createFileRoute("/{-$locale}")({
        // beforeLoad 在路由渲染前运行(在服务器和客户端上)
        // 这是验证 locale 前缀的理想位置
        beforeLoad: ({ params }) => {
          const localeParam = params.locale;
      
          // validatePrefix 检查 locale 是否根据你的 intlayer 配置有效
          const { isValid, localePrefix } = validatePrefix(localeParam);
      
          if (!isValid) {
            // 无效的 locale 前缀 - 重定向到 404 页面并使用有效的 locale 前缀
            throw redirect({
              to: "/{-$locale}/404",
              params: { locale: localePrefix },
            });
          }
        },
        component: Outlet,
        // notFoundComponent 在子路由不存在时调用
        // 例如:/en/non-existent-page 在 /en 布局内触发此方法
        notFoundComponent: NotFoundComponent,
      });
      
      src/routes/{-$locale}/$.tsx
      import { createFileRoute } from "@tanstack/react-router";
      
      import { NotFoundComponent } from "./404";
      
      // $ (splat/catch-all) 路由匹配任何不匹配其他路由的路径
      // 例如:/en/some/deeply/nested/invalid/path
      // 这确保 locale 内所有不匹配的路径都显示 404 页面
      // 没有这个,不匹配的深层路径可能显示空白页面或错误
      export const Route = createFileRoute("/{-$locale}/$")({
        component: NotFoundComponent,
      });
      
    16. 提取组件内容

      可选

      isOptional={true}>

      如果你有现有的 codebase,转换数千个文件可能会很耗时。

      为了简化此过程,Intlayer 提供了一个 compiler / extractor 来转换你的组件并提取内容。

      要设置它,你可以在 intlayer.config.ts 文件中添加 compiler 部分:

      intlayer.config.ts
      import { type IntlayerConfig } from "intlayer";
      
      const config: IntlayerConfig = {
        // ... 配置的其余部分
        compiler: {
          /**
           * 表示编译器是否应启用。
           */
          enabled: true,
      
          /**
           * 定义输出文件路径
           */
          output: ({ fileName, extension }) => `./${fileName}${extension}`,
      
          /**
           * 表示转换后是否应保存组件。
           *
           * - 如果为 `true`,编译器将在磁盘中重写组件文件。因此转换将是永久的,编译器将在下一个过程中跳过转换。这样,编译器可以转换应用程序,然后可以将其删除。
           *
           * - 如果为 `false`,编译器将仅在构建输出中注入 `useIntlayer()` 函数调用,并保持 base codebase 完整。转换将仅在内存中完成。
           */
          saveComponents: false,
      
          /**
           * 字典键前缀
           */
          dictionaryKeyPrefix: "",
        },
      };
      
      export default config;
      

      运行提取器以转换组件并提取内容

      bash
      npx intlayer extract
      
      从 v9 起,intlayerCompiler 包含在 intlayer 插件中。所以你不需要手动添加它。

      更新你的 vite.config.ts 以包含 intlayerCompiler 插件:

      vite.config.ts
      import { defineConfig } from "vite";
      import { intlayer, intlayerCompiler } from "vite-intlayer";
      
      export default defineConfig({
        plugins: [
          intlayer(),
          intlayerCompiler(), // 添加编译器插件
        ],
      });
      
      bash
      npm run build # 或 npm run dev
      
    17. 预渲染 & 生成 Sitemap

      Intlayer 配备了内置的 sitemap 生成器,可以帮助你轻松为应用程序创建 sitemap。它处理本地化路由并为搜索引擎添加必要的元数据。

      Intlayer 生成的 sitemap 支持 xhtml:link 命名空间(Hreflang XML 扩展)。与仅列出原始 URL 的默认 sitemap 生成器不同,Intlayer 自动在页面的所有语言版本之间创建所需的双向链接(例如,/about/about?lang=fr/about?lang=es)。这确保搜索引擎正确索引并向正确的受众提供正确的语言版本。

      要使用它,你首先需要配置你的 vite.config.ts 以为本地化路由启用预渲染并禁用默认的 TanStack Start sitemap 生成。

      vite.config.ts
      import { localeFlatMap } from "intlayer";
      // ... 其他导入
      
      export const pathList = ["", "/about", "/404"];
      
      const localizedPages = localeFlatMap(({ urlPrefix }) =>
        pathList.map((path) => ({
          path: `${urlPrefix}${path}`,
          prerender: {
            enabled: true,
          },
        }))
      );
      
      export default defineConfig({
        plugins: [
          // ... 其他插件
          tanstackStart({
            // ... 其他配置
            sitemap: {
              enabled: false,
            },
            prerender: {
              enabled: true,
              crawlLinks: false,
              concurrency: 10,
            },
            pages: localizedPages,
          }),
        ],
      });
      

      然后,创建一个 src/routes/sitemap[.]xml.ts 路由,使用 generateSitemap 函数:

      src/routes/sitemap[.]xml.ts
      import { createFileRoute } from "@tanstack/react-router";
      import { generateSitemap } from "intlayer";
      
      const SITE_URL = (
        import.meta.env.VITE_SITE_URL ?? "http://localhost:3000"
      ).replace(/\/$/, "");
      
      export const Route = createFileRoute("/sitemap.xml")({
        server: {
          handlers: {
            GET: async () => {
              const sitemap = generateSitemap(
                [
                  { path: "/", changefreq: "daily", priority: 1.0 },
                  { path: "/about", changefreq: "monthly", priority: 0.8 },
                ],
                { siteUrl: SITE_URL }
              );
      
              return new Response(sitemap, {
                headers: { "Content-Type": "application/xml" },
              });
            },
          },
        },
      });
      
    18. 配置 TypeScript

      Intlayer 使用模块扩展以获得 TypeScript 的好处并使你的 codebase 更强大。

      确保你的 TypeScript 配置包含自动生成的类型:

      tsconfig.json
      {
        // ... 你现有的配置
        include: [
          // ... 你现有的 includes
          ".intlayer/**/*.ts", // 包含自动生成的类型
        ],
      }
      

    Git 配置

    建议忽略 Intlayer 生成的文件。这样可以避免将它们提交到您的 Git 仓库中。

    要做到这一点,您可以将以下指令添加到您的 .gitignore 文件中:

    .gitignore
    # 忽略 Intlayer 生成的文件
    .intlayer
    

    VS Code 扩展

    为了改进你使用 Intlayer 的开发体验,你可以安装官方的 Intlayer VS Code 扩展

    从 VS Code 应用市场安装

    此扩展提供以下功能:

    • 自动完成翻译密钥。
    • 实时错误检测缺失的翻译。
    • 内联预览已翻译的内容。
    • 快速操作轻松创建和更新翻译。

    有关如何使用扩展的更多详细信息,请参阅 Intlayer VS Code 扩展文档

    更进一步

    要更进一步,您可以实现可视化编辑器或使用CMS外部化您的内容。

    文档参考

    常见问题

    TanStack Start 本身没有自带的 i18n 层,因此需要选择第三方库:

    • i18next / react-i18nextreact-intl:与框架解耦的消息目录,需要手动接入路由。
    • Lingui:带有编译步骤的 ICU 消息方案。
    • Paraglide:编译型消息方案,仅专注于消息层。
    • Intlayer:最先进的解决方案。内容可以在代码库中的任何位置声明(靠近每个组件或集中管理),在构建时编译,具备类型安全键、支持语言环境的路由、站点地图生成、AI 翻译、可视化编辑器和 CMS。

    在 TanStack Start 上,关键差异在于路由与服务端渲染支持。Intlayer 深度集成了基于文件的路由器、head 函数以及预渲染流程,免去了您手动组装 Provider、语言检测器和站点地图的繁琐工作。请参阅 为什么选择 IntlayerTanStack Start i18n 性能基准

    远少于基于命名空间的方案,因为页面永远不会下载它不渲染的语言目录。服务端渲染的标记在服务端直接解析内容,构建时编译器将 useIntlayer 调用替换为组件使用的确切字典条目,因此未使用的键和未使用的语言都会被自动丢弃,并且 动态字典 会按语言环境拆分剩余内容。与常规替代方案相比,Intlayer 可将 bundle 和页面体积减少高达 50%。请参阅 Bundle 体积优化性能基准

    可以,有两条迁移路径。您可以使用 react-i18next 迁移指南i18next 迁移指南 逐步迁移内容。或者,您可以完全保留当前的 API:兼容性适配器 公开与 react-i18nextreact-intli18next 完全相同的 API,但底层由 Intlayer 字典驱动,因此只需更改导入语句,组件代码完全无需修改。

    可以。JSON 同步插件 将您的 /messages/{locale}/{namespace}.json 文件作为单一真实来源(source of truth),并双向生成 Intlayer 字典。PO 同步插件 对 gettext 目录执行相同的操作,而 按语言环境组织的文件 允许您按语言拆分内容,而不是将所有语言打包到一个文件中。

    不需要。运行 npx intlayer extract,Intlayer 会读取您的组件,提取面向用户的字符串,并在每个组件旁边生成 .content 文件,这样您只需审查 diff,而无需手动逐一复制字符串到语言目录中。本指南的第 15 步详细介绍了此过程。

    如需全自动流程,Intlayer Compiler 可在构建时执行相同操作:它在每次更改时扫描您的 JSX、TSX、Vue 和 Svelte 源代码,生成字典并通过热模块替换 (HMR) 保持同步,因此完全无需手动维护键名。

    开启编译器前有两个限制值得了解:它通过静态分析工作,因此仅在运行时存在的字符串(如 API 错误代码或 CMS 字段)无法被捕获;此外它需要区分用户文本和应用程序逻辑(如 className="active" 或状态代码),在大型代码库中需要少量注解。而 extract 命令 则通过让您参与审查避免了这两个问题。

    共有 5 个工具,均为可选:

    • VS Code 扩展:从 useIntlayer 键跳转到声明它的内容文件,从组件中提取内容,并从命令面板或专属的 Intlayer 选项卡运行 build、fill、test、push 和 pull。
    • LSP 服务器:在任何支持 LSP 的编辑器中提供相同的感知能力,支持跳转到定义、查找所有引用、悬停预览翻译值、键和字段的自动补全,以及在键未声明时发出警告。它还可以解析 i18nextreact-i18nextnext-intluse-intl 调用,助力平滑迁移。
    • MCP 服务器:向 Cursor、VS Code、Claude Desktop、Claude Code 和 ChatGPT 公开 Intlayer 文档与 CLI,使 AI 助手能够基于最新文档进行准确回答,并能自行运行 intlayer fill 等命令。
    • Agent Skills:针对特定领域的技能(如 intlayer-configintlayer-cliintlayer-content,以及每个框架对应的专属技能),教导 AI 代理您的路由配置和内容节点类型。
    • ESLint 插件no-raw-text 规则标记硬编码字符串,并提供针对静态字典键和未使用内容的额外规则。