news 2026/9/15 11:25:14

TanStack Start + Paraglide 实战:在 React 全栈框架中落地类型安全的 URL 国际化

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
TanStack Start + Paraglide 实战:在 React 全栈框架中落地类型安全的 URL 国际化

TanStack Start + Paraglide 实战:在 React 全栈框架中落地类型安全的 URL 国际化

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

导读

本文以仓库中的start-i18n-paraglide官方示例为主体,系统讲解如何在 TanStack Start 全栈框架中集成 Paraglide JS,实现基于 URL 前缀的国际化路由、服务端翻译、类型安全的路径名翻译与预渲染多语言页面。读完本文,你将掌握从paraglideVitePlugin配置、路由 rewrite、服务端中间件到离线重定向与预渲染的完整 i18n 落地链路。

示例概览:项目结构一窥

本示例位于 examples/react/start-i18n-paraglide,是一个包含en/de双语言的 TanStack Start 应用。关键文件布局如下:

examples/react/start-i18n-paraglide/ ├── messages/ # 消息源文件(JSON 格式) │ ├── en.json │ └── de.json ├── project.inlang/ # inlang 项目配置目录 │ ├── project_id │ └── settings.json ├── src/ │ ├── routes/ # 文件路由 │ │ ├── __root.tsx # 根路由:html lang 动态化 + 语言切换 │ │ ├── index.tsx # 首页:loader 与 Server Function 中的翻译 │ │ └── about.tsx # 关于页 │ ├── utils/ │ │ ├── prerender.ts # 预渲染路由(localizeHref) │ │ ├── translated-pathnames.ts # 类型安全的翻译路径名 │ │ └── seo.ts # SEO meta 辅助函数 │ ├── router.tsx # 路由实例:rewrite 配置 │ ├── server.ts # 服务端入口:paraglideMiddleware │ └── vite.config.ts # paraglideVitePlugin 集成

其中vite.config.tsrouter.tsxserver.tssrc/routes/__root.tsx是本示例的核心四件套,下文逐一展开。

一、初始化 Paraglide JS 并接入 Vite 插件

1.1 初始化项目

在你的 TanStack Start 项目中先初始化 Paraglide JS:

npx @inlang/paraglide-js@latest init

该命令会生成project.inlang/项目目录与messages/消息目录。本示例的消息文件为 messages/en.json 与 messages/de.json,采用 inlang 消息格式:

// messages/en.json { "$schema": "https://inlang.com/schema/inlang-message-format", "example_message": "Hello world {username}", "server_message": "Server message {emoji}", "about_message": "About message", "home_page": "Home page", "about_page": "About page" }
// messages/de.json { "example_message": "Guten Tag {username}", "server_message": "Server Nachricht {emoji}", "about_message": "Über uns", "home_page": "Startseite", "about_page": "Über uns" }

可以看到消息支持{username}{emoji}这样的具名参数,编译后会被生成为带参数的函数调用。

1.2 配置 Vite 插件

在 vite.config.ts 中把paraglideVitePlugin加入插件列表(注意要与tanstackStart()共存):

import { paraglideVitePlugin } from '@inlang/paraglide-js' import { defineConfig } from 'vite' import { tanstackStart } from '@tanstack/react-start/plugin/vite' import viteReact from '@vitejs/plugin-react' import tailwindcss from '@tailwindcss/vite' const config = defineConfig({ resolve: { tsconfigPaths: true, }, plugins: [ paraglideVitePlugin({ project: './project.inlang', outdir: './src/paraglide', outputStructure: 'message-modules', cookieName: 'PARAGLIDE_LOCALE', strategy: ['url', 'cookie', 'preferredLanguage', 'baseLocale'], urlPatterns: [ { pattern: '/', localized: [['en', '/en'], ['de', '/de']] }, { pattern: '/about', localized: [['en', '/en/about'], ['de', '/de/ueber']] }, { pattern: '/:path(.*)?', localized: [['en', '/en/:path(.*)?'], ['de', '/de/:path(.*)?']] }, ], }), tanstackStart(), viteReact(), tailwindcss(), ], }) export default config

各核心参数说明:

  • project:指向project.inlang目录,Paraglide 从这里读取项目配置;
  • outdir:编译产物输出目录(本示例为./src/paraglide,README 示例中写的是./app/paraglide,按实际项目结构调整即可)。编译生成的runtime.jsmessages.jsserver.js等模块都出自这里;
  • outputStructure:输出结构,message-modules表示按消息模块组织;
  • cookieName:持久化当前语言的 Cookie 名称(PARAGLIDE_LOCALE);
  • strategy:语言解析策略的优先级数组,本示例为['url', 'cookie', 'preferredLanguage', 'baseLocale'],即优先从 URL 读取语言,其次 Cookie、浏览器首选语言,最后回退到 baseLocale;
  • urlPatterns:定义每个路由模式对应各语言的 URL 形态。最后一个/:path(.*)?是 catch-all 模式,确保/en/anything形式的路径都能被正确解析——这正是"改写后的 URL 与真实路由解耦"的关键。

二、在组件、Loader 与 Server Function 中使用翻译

Paraglide 会把messages/*.json编译成类型安全的m消息对象,并生成getLocale/setLocale/locales等运行时 API。示例中的用法覆盖了三个典型场景:

2.1 组件内直接渲染

在 src/routes/about.tsx 中:

import { createFileRoute } from '@tanstack/react-router' import { m } from '@/paraglide/messages' export const Route = createFileRoute('/about')({ component: RouteComponent, }) function RouteComponent() { return <div>{m.about_message()}</div> }

2.2 Loader 中预取消息

在 src/routes/index.tsx 中,消息可以在路由 loader 中获取,并支持传参:

export const Route = createFileRoute('/')({ component: Home, loader: async () => { return { messageFromLoader: m.example_message({ username: 'John Doe' }), serverFunctionMessage: await getServerMessage({ data: '📩' }), } }, })

2.3 Server Function 中的服务端翻译

Paraglide 支持服务端渲染/执行,因此消息也可以直接在createServerFn内部调用:

import { createServerFn } from '@tanstack/react-start' const getServerMessage = createServerFn() .validator((emoji: string) => emoji) .handler((ctx) => { return m.server_message({ emoji: ctx.data }) })

这正是示例 README 中强调的"Server-side translation"能力:翻译逻辑可以同时运行在客户端与服务端,且由于 TanStack Start 的 SSR 特性,首屏 HTML 即包含目标语言内容,有利于 SEO。

2.4 语言切换与 html lang

在根路由 src/routes/__root.tsx 中,通过getLocale()动态设置<html lang>,并通过setLocale(locale)提供切换按钮:

import { getLocale, locales, setLocale } from '@/paraglide/runtime' import { m } from '@/paraglide/messages' function RootDocument({ children }: { children: React.ReactNode }) { return ( <html lang={getLocale()}> <head> <HeadContent /> </head> <body> <div className="p-2 flex gap-2 text-lg justify-between"> <Link to="/" activeOptions={{ exact: true }}>{m.home_page()}</Link> <Link to="/about">{m.about_page()}</Link> <div className="flex gap-2 text-lg"> {locales.map((locale) => ( <button key={locale} onClick={() => setLocale(locale)}> {locale} </button> ))} </div> </div> <hr /> <div className="p-2">{children}</div> <Scripts /> </body> </html> ) }

locales数组由编译产物自动导出,遍历它即可渲染出全部语言切换按钮,无需手写语言列表。

三、URL 重写:让路由与国际化 URL 解耦

默认情况下,/en/de这样的前缀对 TanStack Router 来说是"多余"的——真实路由只有//about。示例通过 Router 的rewrite机制解决这个问题,见 src/router.tsx:

import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' import { deLocalizeUrl, localizeUrl } from './paraglide/runtime' export const getRouter = () => { return createRouter({ routeTree, scrollRestoration: true, defaultPreloadStaleTime: 0, rewrite: { input: ({ url }) => deLocalizeUrl(url), output: ({ url }) => localizeUrl(url), }, }) }
  • rewrite.input:把浏览器地址栏的/en/about"去本地化"为路由内部使用的/about,路由匹配据此进行;
  • rewrite.output:在生成<Link>href、跳转等场景,把内部路径/about重新"本地化"为/en/about(若当前语言是默认语言且无前缀则保持原样)。

借助deLocalizeUrl/localizeUrl这两个由 Paraglide 生成的对偶函数,应用内部的LinkuseNavigateredirect等全部继续使用"干净"的路径,由框架层统一完成前缀的增删,业务代码完全无感。

四、服务端中间件:让每个请求感知语言

URL 重写解决了"前端看到的 URL"问题,而服务端还需要在入口处拦截请求、解析并设定当前语言。在 src/server.ts 中:

import { paraglideMiddleware } from './paraglide/server.js' import handler from '@tanstack/react-start/server-entry' export default { fetch(req: Request): Promise<Response> { return paraglideMiddleware(req, () => handler.fetch(req)) }, }

paraglideMiddleware按照 Vite 插件里配置的strategy顺序(url → cookie → preferredLanguage → baseLocale)解析请求的语言,注入上下文后再调用 TanStack Start 的服务端入口。这意味着:

  • 首屏 SSR 使用 URL 前缀对应的语言渲染;
  • 若用户通过 Cookie 记忆过语言偏好,后续请求会保持一致;
  • 无前缀、无 Cookie 时回退到浏览器首选语言,最终兜底baseLocale

五、离线应用的语言重定向

如果你的应用需要离线工作(如 PWA),服务端中间件在离线场景不可用,此时需要在客户端处理语言重定向。示例 README 给出在根路由beforeLoad中的写法:

import { shouldRedirect } from '../paraglide/runtime' export const Route = createRootRoute({ beforeLoad: async () => { const decision = await shouldRedirect({ url: window.location.href }) if (decision.redirectUrl) { throw redirect({ href: decision.redirectUrl.href }) } }, // ... })

shouldRedirect会在本地判断当前 URL 是否需要重定向到某个语言版本(例如无前缀但检测到浏览器首选语言为de时),返回redirectUrl后通过 TanStack Router 的redirect抛出重定向即可。

六、类型安全的翻译路径名:杜绝翻译遗漏

当路由较多时,手工维护urlPatterns容易漏译。示例在 src/utils/translated-pathnames.ts 中实现了一个createTranslatedPathnames工厂:

import { Locale } from '@/paraglide/runtime' import { FileRoutesByTo } from '../routeTree.gen' type RoutePath = keyof FileRoutesByTo const excludedPaths = ['admin', 'docs', 'api'] as const type PublicRoutePath = Exclude< RoutePath, `${string}${(typeof excludedPaths)[number]}${string}` > type TranslatedPathname = { pattern: string localized: Array<[Locale, string]> } function toUrlPattern(path: string) { return ( path // catch-all .replace(/\/\$$/, '/:path(.*)?') // optional parameters: {-$param} .replace(/\{-\$([a-zA-Z0-9_]+)\}/g, ':$1?') // named parameters: $param .replace(/\$([a-zA-Z0-9_]+)/g, ':$1') // remove trailing slash .replace(/\/+$/, '') ) } function createTranslatedPathnames( input: Record<PublicRoutePath, Record<Locale, string>>, ): TranslatedPathname[] { return Object.entries(input).map(([pattern, locales]) => ({ pattern: toUrlPattern(pattern), localized: Object.entries(locales).map( ([locale, path]) => [locale as Locale, `/${locale}${toUrlPattern(path)}`] satisfies [ Locale, string, ], ), })) } export const translatedPathnames = createTranslatedPathnames({ '/': { en: '/', de: '/' }, '/about': { en: '/about', de: '/ueber' }, })

其原理与要点:

  1. FileRoutesByTo为类型来源:路由键全部来自routeTree.gen.ts导出的类型,新增路由而未提供翻译时 TypeScript 会直接报错;
  2. 排除私有路径:通过模板字面量类型把包含admindocsapi的路由从PublicRoutePath中剔除;
  3. 路由模式转换toUrlPattern把 TanStack Router 的路径语法($param具名参数、{-$param}可选参数、/$catch-all)转换为 Paraglide 的 URL 模式语法(:param:param?:path(.*)?)。

随后把translatedPathnames作为urlPatterns传入paraglideVitePlugin即可:

import { translatedPathnames } from './i18n/lib' export default defineConfig({ plugins: [ paraglideVitePlugin({ // ... other options urlPatterns: translatedPathnames, }), ], })

七、预渲染多语言页面

对于需要静态输出的场景,可以使用localizeHref把内部路径映射为本地化 URL,再交给 TanStack Start 的预渲染配置。见 src/utils/prerender.ts:

import { localizeHref } from '../paraglide/runtime' export const prerenderRoutes = ['/', '/about'].map((path) => ({ path: localizeHref(path), prerender: { enabled: true, }, }))

注意 README 中的提醒:此方式要求在 build 之前先用 CLI 编译 Paraglide,否则localizeHref尚无产物可导入。也就是说,预渲染流程应为:

  1. npx @inlang/paraglide-js compile(或由插件在构建期完成编译);
  2. 构建时读取prerenderRoutes,对//about的每个语言版本(如/en/de/en/about/de/ueber)分别生成静态页面。

八、SEO 辅助

示例还提供了 src/utils/seo.ts,用于为每个语言版本生成完整的 Open Graph / Twitter Card meta 标签(og:titleog:descriptionog:imagetwitter:card等),配合html lang={getLocale()}与本地化 URL,可保证搜索引擎正确识别每种语言的页面。

结语:完整落地链路回顾

回顾整个示例,TanStack Start + Paraglide 的国际化方案可以总结为一条清晰链路:

  1. 编译期paraglideVitePluginmessages/*.json编译出类型安全的m消息对象与runtime/server模块,并根据urlPatterns生成 URL 映射;
  2. 请求期(服务端)paraglideMiddlewarestrategy优先级解析语言,SSR 直接输出目标语言 HTML;
  3. 导航期(客户端):Router 的rewritedeLocalizeUrl/localizeUrl完成内部路径与本地化 URL 的双向转换,setLocale负责切换并写入 Cookie;
  4. 构建期(可选)createTranslatedPathnames提供类型级防漏译保障,localizeHref支撑多语言页面预渲染。

从 package.json 可以看到,该示例运行脚本为devvite dev --port 3000)、buildvite build)、startnode .output/server/index.mjs)。克隆仓库后进入示例目录安装依赖并执行npm run dev,即可在http://localhost:3000体验/en/de两套语言的完整切换流程。

提示:示例中@inlang/paraglide-js@tanstack/react-start等依赖版本以仓库内 package.json 为准;Paraglide 本身的更多消息与参数用法可参考其官方基础文档。

【免费下载链接】router🤖 A client-first, server-capable, fully type-safe router and full-stack framework for the web (React and more).项目地址: https://gitcode.com/GitHub_Trending/ro/router

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/15 11:23:32

JAVASE笔记

介绍&#xff1a; 两种核心机制&#xff1a; 1.JAVA虚拟机&#xff08;JAVA Virtual Maachine&#xff09;&#xff0c;JVM &#xff1a;一次编写&#xff0c;处处运行 2.垃圾回收机制&#xff08;Garbage Collection&#xff09;&#xff0c;GC &#xff1a;自动进行 面向对象…

作者头像 李华
网站建设 2026/9/15 11:20:08

PPT转微课视频制作全流程指南

1. 微课视频制作的核心价值与适用场景在数字化教育快速发展的今天&#xff0c;微课视频已经成为知识传播的重要载体。相比传统45分钟的课堂录像&#xff0c;8-15分钟的微课视频更符合现代人的注意力周期&#xff0c;特别适合碎片化学习场景。我从事在线教育内容制作6年&#xf…

作者头像 李华
网站建设 2026/9/15 11:19:39

Surya 2 在 NVIDIA GPU 上如何通过 vllm 后端首次运行 surya_ocr?

Surya 2 在 NVIDIA GPU 上如何通过 vllm 后端首次运行 surya_ocr&#xff1f; 【免费下载链接】surya OCR, layout analysis, reading order, table recognition in 90 languages 项目地址: https://gitcode.com/GitHub_Trending/su/surya 本文面向有一块 NVIDIA GPU、想…

作者头像 李华
网站建设 2026/9/15 11:19:13

SpringBoot与SSM框架构建超市POS收银系统实战

1. 项目概述&#xff1a;超市POS收银管理系统的核心价值超市POS收银系统是零售行业最基础也最核心的数字化工具。基于SpringBoot和SSM框架开发的这套系统&#xff0c;本质上是通过技术手段将传统人工收银、库存管理、销售统计等业务流程标准化、自动化。我在实际部署中发现&…

作者头像 李华