使用您最喜欢的AI助手总结文档,并引用此页面和AI提供商
版本历史
- "初始历史"v9.1.32025/8/6
此页面的内容已使用 AI 翻译。
查看英文原文的最新版本如果您有改善此文档的想法,请随时通过在GitHub上提交拉取请求来贡献。
文档的 GitHub 链接复制文档 Markdown 到剪贴板
使用 Intlayer 翻译你的 SolidStart 网站 | 国际化 (i18n)
目录
本指南涵盖了一个服务端渲染的 SolidStart 应用程序:语言检测在请求时发生,页面在服务端以正确的语言渲染,并且搜索引擎所需的 <html lang>、hreflang 和 sitemap 信号都是在服务端生成的。
为什么选择 Intlayer 而不是其他替代方案?
与 @solid-primitives/i18n 或 i18next 等主流解决方案相比,Intlayer 是一个带有集成优化的解决方案,例如:
Intlayer 经过优化,可与 Solid 完美配合,提供组件级内容划分、响应式翻译以及扩展国际化 (i18n) 所需的所有功能。
无需将庞大的 JSON 文件加载到页面中,只需加载必要的内容。Intlayer 有助于将打包文件和页面体积减少高达 50%。
对应用程序的内容进行局部作用域划分有助于大型应用程序的维护。你可以复制或删除单个功能文件夹,而无需心理负担去审查整个内容代码库。此外,Intlayer 是完全类型化的,以确保内容的准确性。
将内容协同定位减少了大语言模型 (LLM) 所需的上下文。Intlayer 还附带了一套工具,例如用于测试缺失翻译的 CLI、LSP、MCP 和 agent skills,使 AI 代理的开发人员体验 (DX) 更加顺畅。
在 CI/CD 流水线中使用你选择的 LLM 按照 AI 提供商的成本自动进行翻译。Intlayer 还提供了一个编译器来自动提取内容,以及一个 Web 平台 来帮助在后台进行翻译。
将庞大的 JSON 文件连接到组件可能会导致性能和响应性问题。Intlayer 在构建时优化了内容加载。
在 SolidStart 应用程序中设置 Intlayer 的分步指南
安装依赖项
使用 npm 安装必要的软件包:
bash复制代码复制代码到剪贴板
--interactive标志是可选的。如果你是 AI 代理,请使用intlayer-cli init。此命令将检测你的环境并安装所需的软件包。例如:
bash复制代码复制代码到剪贴板
intlayer
solid-intlayer
将 Intlayer 与 Solid 应用程序集成的软件包。它为 Solid 国际化提供上下文提供程序 (context providers) 和钩子 (hooks)。
vite-intlayer
包含用于将 Intlayer 与 Vite 打包器 集成的 Vite 插件,以及检测用户偏好语言、管理 cookie 和处理 URL 重定向的语言路由句柄。
这里
vite-intlayer是一个服务端关注点,不仅是构建时的关注点:它提供了 SolidStart 的 Nitro 服务器运行的请求句柄。将其保留在dependencies中是安全的默认设置 —— 仅当你要部署包含 Nitro 内联句柄的构建后的.output目录时,才可以将其移动到devDependencies。配置你的项目
创建一个配置文件来配置应用程序的语言:
intlayer.config.ts复制代码复制代码到剪贴板
import { type IntlayerConfig, Locales } from "intlayer"; const config: IntlayerConfig = { internationalization: { locales: [ Locales.ENGLISH, Locales.FRENCH, Locales.SPANISH, // 你的其他语言 ], defaultLocale: Locales.ENGLISH, }, routing: { mode: "prefix-no-default", }, }; export default config;使用
prefix-no-default,默认语言从无前缀的 URL 提供:plaintext复制代码复制代码到剪贴板
通过此配置文件,你可以设置本地化 URL、中间件重定向、cookie 名称、内容声明的位置和扩展名、禁用控制台中的 Intlayer 日志等。有关可用参数的完整列表,请参阅配置文档。
在 Vite 配置中集成 Intlayer
将 Intlayer 插件添加到你的配置中:
vite.config.ts复制代码复制代码到剪贴板
import { solidStart } from "@solidjs/start/config"; import { nitro } from "nitro/vite"; import { defineConfig } from "vite"; import { intlayer } from "vite-intlayer"; export default defineConfig({ plugins: [solidStart(), nitro(), intlayer()], });intlayer()Vite 插件构建你的内容声明文件,在开发模式下监视它们,并在应用程序内部定义 Intlayer 环境变量。它还提供可优化性能的别名。语言路由随插件一起提供
SolidStart 运行在 Nitro 上,并且
intlayer()将其语言路由句柄直接注册到 Nitro 的服务器管道中(通过routing.enableProxy选项,默认为true)。无需配置其他内容:在构建好的服务器上,每个请求在到达路由器之前都会经过检查,并且- 语言从 URL 前缀读取,其次是
INTLAYER_LOCALEcookie,然后是Accept-Language请求头; - 当解析出的语言不是默认语言时,无前缀的 URL 会重定向到对应的本地化页面(
/→/fr); - 冗余前缀的 URL 会重定向回其规范形式(
/en/about→/about); - 语言 cookie 会在响应中写回。
- 语言从 URL 前缀读取,其次是
声明你的内容
创建并管理你的内容声明以存储翻译:
src/contents/home.content.ts复制代码复制代码到剪贴板
import { type Dictionary, t } from "intlayer"; const homeContent = { key: "home-page", content: { title: t({ en: "Hello world!", fr: "Bonjour le monde !", es: "¡Hola mundo!", }), metaTitle: "SolidStart + Intlayer", metaDescription: t({ en: "A SolidStart application internationalized with Intlayer.", fr: "Une application SolidStart internationalisée avec Intlayer.", es: "Una aplicación SolidStart internacionalizada con Intlayer.", }), documentation: t({ en: "Visit start.solidjs.com to learn how to build SolidStart apps.", fr: "Visitez start.solidjs.com pour apprendre à créer des applications SolidStart.", es: "Visita start.solidjs.com para aprender a crear aplicaciones SolidStart.", }), }, } satisfies Dictionary; export default homeContent;⚠️ SolidStart 特别注意点:
src/routes下的每个.ts/.tsx文件都会成为一个路由,而.content.ts文件具有默认导出,因此它会被误识别为一个页面。请将页面的内容声明保留在 routes 目录之外(src/contents/效果很好)。组件的内容可以保持协同定位,因为文件系统路由器不会扫描src/components。只要你的内容声明包含在
contentDir目录(默认为./src)中,并匹配内容声明文件扩展名(默认为.content.{json,ts,tsx,js,jsx,mjs,cjs,md,mdx,yaml,yml}),就可以在应用程序的任何位置定义它们。有关更多详细信息,请参阅内容声明文档。
添加本地化路由
本步骤的目标是赋予每种语言自己的 URL,这也是搜索引擎进行索引的内容。
将你的页面移动到可选动态段下。在 SolidStart 的文件系统路由器中,
[[locale]]编译为:locale?路径模式:plaintext复制代码复制代码到剪贴板
布局文件的唯一工作是将该动态段约束为已配置的语言:
src/routes/[[locale]].tsx复制代码复制代码到剪贴板
@solidjs/router将:locale?扩展为两种模式 —— 一种带有段,一种不带段 —— 并按特异性递减进行匹配。matchFilters是区分正常设置与令人困惑的设置的关键所在:显示表格的所有内容在弹窗中打开表格以清晰地查看所有数据
URL 没有 matchFilters带有 matchFilters/fr/about法语关于页面 法语关于页面 /about关于页面 (静态段胜出) 关于页面 /unknown主页,静默处理,且 locale=unknown不匹配 → 回退到 catch-all 404 如果你使用
'prefix-all'路由模式,请首选[locale](必需),如果是'no-prefix'或'search-params',则完全放弃该段。为你的应用程序提供语言 locale
URL 是语言 locale 的唯一真理来源:中间件已经将请求重定向到其本地化路径,因此在根布局中读取路径可使服务端渲染与客户端水化(hydration)保持一致,并使每次客户端导航都自动更新语言 locale。
src/app.tsx复制代码复制代码到剪贴板
IntlayerProvider会对其localeprop 作出响应,因此在 JSX 中传递访问器调用locale()就足够了 —— Solid 会将其编译为一个 getter,当 URL 改变时整个树都会以新语言重新渲染。在服务端设置 HTML 的 lang 和 dir 属性
<html>元素由entry-server.tsx在Router之外渲染。改为从请求 URL 读取语言 locale:src/entry-server.tsx复制代码复制代码到剪贴板
网络爬虫现在可以在首个字节接收到正确的语言:
html复制代码复制代码到剪贴板
在页面中使用 Intlayer
在整个应用程序中访问你的内容字典:
src/routes/[[locale]]/index.tsx复制代码复制代码到剪贴板
在 Solid 中,
useIntlayer返回响应式内容(例如content)。你可以直接访问其属性。如果你想在
string属性中使用内容,例如alt、title、href、aria-label等,可以使用该函数的值,如下所示:html复制代码复制代码到剪贴板
要了解有关
useIntlayer钩子的更多信息,请参阅文档。内容节点不仅限于纯文本翻译。例如复数形式的计数器:
src/components/Counter.content.ts复制代码复制代码到剪贴板
src/components/Counter.tsx复制代码复制代码到剪贴板
plural()通过针对当前语言的Intl.PluralRules选择类别,因此拥有两种以上复数形式的语言无需任何额外代码即可工作。创建本地化链接组件
创建自定义
Link组件,它会自动向内部 URL 添加当前语言的前缀:src/components/LocalizedLink.tsx复制代码复制代码到剪贴板
src/components/Nav.tsx复制代码复制代码到剪贴板
现在只需编写一次
href="/about",即可根据活动语言生成/about、/fr/about或/es/about—— 页面中的任何位置都无需手动添加前缀。创建语言切换器组件
将切换器渲染为真实的
<a>锚点而非<select>:当前页面的每种语言都会变为可爬取的链接,并且可以在新标签页中打开,这是仅依靠 JavaScript 的控件无法提供的。getPathWithoutLocale会从当前路径中剥离语言段,而getLocalizedUrl会为目标语言重新构建它,因此这些链接会遵循你的路由模式,无需硬编码任何内容。导航是改变渲染语言的原因 ——[[locale]]路由从 URL 中推导语言 —— 而setLocale会将选择保存在INTLAYER_LOCALEcookie 中,以便以后访问无语言前缀的 URL 时能解析为相同的语言。src/components/LocaleSwitcher.tsx复制代码复制代码到剪贴板
import { A, useLocation } from "@solidjs/router"; import { getHTMLTextDir, getLocaleName, getLocalizedUrl, getPathWithoutLocale, } from "intlayer"; import { useIntlayer, useLocale } from "solid-intlayer"; import { type Component, For } from "solid-js"; export const LocaleSwitcher: Component = () => { const content = useIntlayer("locale-switcher"); const location = useLocation(); const { locale, setLocale, availableLocales } = useLocale(); // 当前显示页面的规范(无语言段)路径 const pathWithoutLocale = () => getPathWithoutLocale(location.pathname); return ( <div> <button aria-label={content.label.value} popoverTarget="localePopover" type="button" > {getLocaleName(locale())} </button> <div id="localePopover" popover="auto"> <For each={availableLocales}> {(localeItem) => ( <A dir={getHTMLTextDir(localeItem)} // 仅精确定位,使默认语言链接不会在每个页面上都被标记为 active end href={getLocalizedUrl(pathWithoutLocale(), localeItem)} hreflang={localeItem} lang={localeItem} onClick={() => setLocale(localeItem)} // 确保浏览器的“后退”按钮返回到上一页 replace > {/* 各自语言下的语言名称 - 例如 Français */} {getLocaleName(localeItem)} </A> )} </For> </div> </div> ); };在 Solid 中,来自
useLocale的locale是一个 signal 访问器。使用带有括号的locale()响应式地读取其当前值。getLocaleName(localeItem)会以各自的语言渲染每种语言名称 ——English / Français / Español。传递第二个参数可以将其翻译为当前显示语言:例如getLocaleName(localeItem, locale())在英语中为English / French / Spanish,在法语中为anglais / français / espagnol。<A>已经在匹配当前 URL 的链接上设置了aria-current="page",因此无需额外添加处理。replace由路由器从渲染的属性中读取:它会替换历史记录条目而不是推入新条目,因此浏览器的“后退”按钮会返回切换前访问的页面,而不是返回前一种语言的同一页面。每个链接上的
dir和hreflang属性可使从右到左的语言名称保持正确的方向,并告知辅助技术和网络爬虫每个链接指向哪种语言。要了解有关
useLocale钩子的更多信息,请参阅文档。生成规范 canonical 和 hreflang 链接
可选hreflang注释告知搜索引擎/about、/fr/about和/es/about是不同语言下的同一个页面。getMultilingualUrls根据你的路由模式从规范(无语言段)路径中导出它们,因此无需硬编码任何内容:src/components/AlternateLinks.tsx复制代码复制代码到剪贴板
在可获取请求 URL 的文档 head 中渲染它:
src/entry-server.tsx复制代码复制代码到剪贴板
随后
GET /fr/about将响应:html复制代码复制代码到剪贴板
关于
@solidjs/meta的注意事项:在撰写本文时,@solidjs/meta中的<Title>和<Meta>在客户端水化后应用,但不会发散到 SolidStart v2 的服务端渲染<head>中。在 upstream 修复此问题之前,请直接在entry-server.tsx中渲染爬虫无需 JavaScript 即可看到的标签 ——canonical、hreflang以及需要的title/description,如上所示。处理未找到 (404) 页面
可选处于
src/routes根目录的通配符路由(splat route)可以捕获语言段未匹配到的所有路径 —— 包括被matchFilters拒绝的无效语言前缀。由于语言仍通过根布局来自 URL,因此 404 页面将以访问者的语言显示:src/routes/[...404].tsx复制代码复制代码到剪贴板
显示表格的所有内容在弹窗中打开表格以清晰地查看所有数据
请求 预期响应 /xx404—xx不是已配置的语言/nonexistent默认语言下的 404/fr/nonexistent法语下的 404(Page introuvable)生成多语言 sitemap 站点地图
可选Intlayer 的 sitemap 生成器将每个路径扩展为每个语言对应一个条目,并在它们之间连接
xhtml:link备用链接,因此路由只需列出规范的、无语言前缀的路径。与仅生成平铺 URL 的基础生成器不同,Intlayer 在每个页面的每个本地化变体之间建立双向链接,这有助于搜索引擎关联本地化 URL 并将正确的页面提供给正确的受众。
SolidStart 将导出 HTTP 方法的文件转换为 API 路由,并从路径中剥离
.ts扩展名 —— 因此src/routes/sitemap.xml.ts在/sitemap.xml处提供服务:src/routes/sitemap.xml.ts复制代码复制代码到剪贴板
import type { APIEvent } from "@solidjs/start/server"; import { generateSitemap } from "intlayer"; const SITE_URL = process.env.SITE_URL ?? "http://localhost:3000"; export const GET = (_event: APIEvent) => { 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" }, }); };output of GET /sitemap.xml复制代码复制代码到剪贴板
API 路由不支持可选参数,因此请将此文件保留在
src/routes的根目录下,置于[[locale]]段之外。sitemap 已经包含了每种语言。你可以使用
getMultilingualUrls以相同方式构建robots.txt,以便Disallow条目涵盖敏感路径的每个本地化拼写:src/routes/robots.txt.ts复制代码复制代码到剪贴板
import { getMultilingualUrls } from "intlayer"; const SITE_URL = process.env.SITE_URL ?? "http://localhost:3000"; const disallowedPaths = ["/admin", "/private"].flatMap((path) => Object.values(getMultilingualUrls(path)) ); export const GET = () => new Response( [ "User-agent: *", "Allow: /", ...disallowedPaths.map((path) => `Disallow: ${path}`), "", `Sitemap: ${SITE_URL}/sitemap.xml`, ].join("\n"), { headers: { "Content-Type": "text/plain" } } );在服务端函数中检索语言 locale
可选你可能希望在服务端函数或 API 路由内部访问当前语言 locale。
在像这样基于前缀的设置中,URL 具有权威性:
getLocaleFromPath从请求 URL 中读取前缀。getLocale是不带语言前缀的请求的回退机制 —— 它会检查INTLAYER_LOCALEcookie,然后检查x-intlayer-locale请求头,接着协商Accept-Language。src/routes/[[locale]]/index.tsx复制代码复制代码到剪贴板
此处不要仅依赖
getLocale:仅当访问者主动切换语言时才会写入语言 cookie,因此首次访问/fr/...将会被解析为默认语言。提取组件的内容
可选如果你有一个现有的代码库,转换数千个文件可能会非常耗时。
为了简化此过程,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()` 函数调用,并保持基础代码库完好。转换将仅在内存中完成。 */ saveComponents: false, /** * 字典键前缀 */ dictionaryKeyPrefix: "", }, }; export default config;运行提取器以转换组件并提取内容
bash复制代码复制代码到剪贴板
之后,将生成的页面内容文件移出
src/routes,原因如步骤 5 所述。从 v9 开始,
intlayerCompiler已包含在intlayer插件中。因此你无需手动添加它。更新你的
vite.config.ts以包含intlayerCompiler插件:vite.config.ts复制代码复制代码到剪贴板
bash复制代码复制代码到剪贴板
配置 TypeScript
Intlayer 使用模块增强 (module augmentation) 来获得 TypeScript 的优势并增强你的代码库。
确保你的 TypeScript 配置包含自动生成的类型:
tsconfig.json复制代码复制代码到剪贴板
字典键和内容路径现在会在编译时进行检查:
tsx复制代码复制代码到剪贴板
验证你的设置
构建并启动服务器,然后检查这些请求是否按预期运行:
复制代码到剪贴板
在弹窗中打开表格以清晰地查看所有数据
| 请求 | 预期响应 |
|---|---|
GET / | 200 — 英语 |
GET / 带有 Accept-Language: fr | 302 → /fr |
GET / 带有 cookie INTLAYER_LOCALE=es | 302 → /es |
GET /fr | 200 — 法语, <html lang="fr"> |
GET /fr/about | 200 — 法语关于页面 |
GET /en/about | 302 → /about (规范重定向) |
GET /xx | 404 |
GET /fr/nonexistent | 404 法语 |
GET /sitemap.xml | 200 — 多语言 XML sitemap |
在 vite dev 下渲染页面的行行为相同。除非你自己将句柄注册为中间件,否则三个重定向行仅适用于构建后的服务器 —— 参见步骤 3。
请在 Node (vite dev) 上运行开发服务器,而不是在 Bun (bun --bun vite dev) 上:SolidStart 的 SSR 目前在 Bun 运行时下会失败并显示Expected a Response object, but received 'NodeResponse'。这与 Intlayer 无关 —— 它在纯模板上也会复现 —— 并且只影响开发服务器,不影响vite build。
Git 配置
建议忽略由 Intlayer 生成的文件。这可以让你避免将它们提交到 Git 仓库。
为此,你可以将以下指令添加到你的 .gitignore 文件中:
复制代码到剪贴板
VS Code 插件
为了提升你使用 Intlayer 的开发体验,你可以安装官方的 Intlayer VS Code 插件。
此插件提供:
- 翻译键的自动补全。
- 缺失翻译的实时错误检测。
- 翻译内容的行内预览。
- 轻松创建和更新翻译的快速操作。
深入了解
要进一步了解,你可以实现可视化编辑器或使用 CMS 外包你的内容。
