首页
/ daisyUI 文档站国际化体系:分块翻译文件结构、路由分块加载与同步工作流解析

daisyUI 文档站国际化体系:分块翻译文件结构、路由分块加载与同步工作流解析

2026-09-05 19:53:53作者:农烁颖Land

daisyUI 文档站(基于 SvelteKit 2 + Svelte 5 构建)通过一套「语言 × 路由分块」的 JSON 翻译文件体系支撑 26 种语言的界面与文档多语言展示。本文以 docs 仓库开发说明 为主体,结合 i18n 运行时加载器翻译同步/校验脚本 的源码实现,完整讲清翻译文件的命名规则、按路由懒加载分块的底层原理、同步命令与必须遵守的翻译规则,帮助你在维护 daisyUI 文档站多语言内容时做到「文件结构不乱、键值跨语言一致、占位符不损坏」。

文档站技术栈与开发约定

在深入翻译体系之前,先明确文档站的开发前提。开发说明文档 开头给出的约定是:

  • 文档站基于 SvelteKit 2 和 Svelte 5 构建,只允许使用 Svelte 5 语法与特性(不得使用 Svelte 4 写法);
  • 交互逻辑统一使用 Svelte Runes(如 $props$state 等);
  • 对 SvelteKit / Svelte 语法有疑问时,可借助 Context7 MCP 服务器查询;
  • 编写页面标记(markup)时,应使用 daisyUI 官方文档中描述的组件与类名。

文档站 package.json 可以确认这些约定与实际依赖一致:svelte 5.56.8、@sveltejs/kit 2.70.1、@sveltejs/vite-plugin-svelte 7.2.0、daisyuiworkspace:* 形式引用本仓库的 daisyui 包,运行环境要求 Node >=20.18.1,脚本执行使用 bun(例如 devtestverify)。

其中与翻译体系直接相关的 npm scripts 在 package.json 中定义为:

"test": "bun test src",
"verify": "bun run --parallel test lang:validate",
"lang:add": "bun src/lib/scripts/addTranslations.js --write",
"lang:prune": "bun src/lib/scripts/pruneTranslations.js",
"lang:prune:write": "bun src/lib/scripts/pruneTranslations.js --write",
"lang:report": "bun src/lib/scripts/reportTranslations.js",
"lang:validate": "bun src/lib/scripts/validateTranslations.js"

verify 会并行执行单元测试与翻译校验,是改动翻译文件后的标准验证入口。

翻译文件结构:<language>.<chunk>.json 分块命名

文件位置与命名模式

开发说明文档 对翻译文件结构的定义如下:

  • 所有翻译文件是位于 packages/docs/src/translation/ 目录下的 JSON 文件;
  • 文件按「语言 + 路由分块」拆分,命名模式为 <language>.<chunk>.json
  • 实际示例如 en.common.jsonen.docs.jsonfa.components.jsonzh_hans.home.json
  • 第一段是语言代码(如 endeeszh_hant),第二段是分块名。

当前的 5 个分块(chunk)及其用途在源码 translationConfig.js 中被集中定义为 chunkNames = ["common", "home", "docs", "components", "other"],与文档说明一一对应:

分块 内容范围 对应路由
common 跨路由共享的字符串:布局、导航、页脚与通用 UI 文案 全站共享 UI(src/lib/ 下的组件)
home 首页 / 的文案 首页
docs /docs 下的文档页 /docssrc/routes/(routes)/docs/
components /components 下的组件页 /componentssrc/routes/(routes)/components/
other 不属于 home、docs、components 的其余页面 其他所有路由

「按路径选分块」的规则在 开发说明文档 中写得很明确:

  • src/routes/(routes)/+page.svelte+page.md*.home.json
  • src/routes/(routes)/docs/...*.docs.json
  • src/routes/(routes)/components/...*.components.json
  • 共享布局、导航、页脚与可复用 UI 文案 → *.common.json
  • 其他任何路由或页面 → *.other.json

源码中的路径到分块映射

上述规则的实现位于 getTranslationChunkForFile,判断顺序是:

export const getTranslationChunkForFile = (filePath) => {
  const normalizedPath = normalizeFilePath(filePath)

  if (normalizedPath.includes("/src/lib/")) return "common"
  if (normalizedPath.includes("/routes/(routes)/docs/")) return "docs"
  if (normalizedPath.includes("/routes/(routes)/components/")) return "components"
  if (normalizedPath.endsWith("/routes/(routes)/+page.md")) return "home"
  if (normalizedPath.endsWith("/routes/(routes)/+page.svelte")) return "home"

  return "other"
}

注意两个细节:

  1. src/lib/ 下的源文件(导航、页脚等可复用 UI 组件)一律归入 common,这正是「共享 UI 文案进 common」规则在提取端的落地;
  2. 兜底分支返回 other,即任何没被显式覆盖的路由都落入 other 分块,保证新增页面不会漏掉归属。

单元测试 translationConfig.test.js 用一组真实路径验证了该映射:/routes/(routes)/+page.mdhomedocs/colors/+page.mddocscomponents/button/+page.mdcomponentssrc/lib/components/Nav.sveltecommonstore/+page.svelteother

键值组织方式:键即英文原文

查看真实文件可以发现,翻译 JSON 的键(key)就是英文源文本本身(通常是完整的句子,甚至包含 HTML 标记),值则是对应语言的译文。例如 en.home.json

"install-title": "Install daisyUI",
"homepage_h1": "Faster, cleaner, easier <br />Tailwind&nbsp;CSS development",
"install-btn": "Install guide"

对应地,zh_hans.home.json 中键完全相同、只有值被翻译:

"install-title": "安装 daisyUI",
"homepage_h1": "更快、更简洁、更简单的<br/>Tailwind&nbsp;CSS 开发。",
"install-btn": "安装指南"

en.common.json 开头带有语言元数据键:

"__code": "EN",
"__direction": "ltr",
"__name": "English",
"__status": ""

这带来两个关键约束(也是 开发说明文档 中「Keeping translation files in sync」一节的核心):

  • 键必须跨语言完全一致,值才允许不同。对某个 <language>.<chunk>.json 增、删、改键时,必须在同分块的所有语言文件中做相同的键变更;
  • 修改既有字符串时,先在同分块的英文文件中找到该键,再把同一键更新到所有语言文件;
  • 新增可翻译字符串时,把键放进由源文件路径决定的英文文件,再把同一键补进该分块的所有语言文件。

开发说明文档 还强调:不要为翻译文件创造新的文件形态(除非同步修改 i18n 加载器);保持 JSON 合法;如果一个字符串在多个分块间复用,只有当它确实是共享 UI / 导航文案时才放进 common,否则保留在它实际出现的路由分块中。

i18n 运行时:common + 当前路由分块的懒加载

开发说明文档 指出:「Route chunks are selected in packages/docs/src/lib/i18n.svelte.js. The site loads common plus the current route chunk.」i18n.svelte.js 的源码完整实现了这一策略。

首屏:eager 加载英文 common + home

首次加载时只打入全局字符串与首页字符串(源码):

// For the initial load, only include global strings and homepage strings.
const defaultCommonModule = import.meta.glob("../translation/en.common.json", { eager: true })
const defaultHomeModule = import.meta.glob("../translation/en.home.json", { eager: true })
const defaultTranslation = {
  ...defaultCommonModule["../translation/en.common.json"].default,
  ...defaultHomeModule["../translation/en.home.json"].default,
}

其余语言文件通过惰性 glob 注册(源码),并刻意排除已 eager 加载的 en.common.json / en.home.json,避免同一模块被以两种 import 模式重复打包。从源码结构看,这是为了控制首屏体积:只有 commonhome 两个英文分块进入初始 bundle,其他语言、其他分块(docs/components/other)在访问对应路由或切换语言时才按需加载。

路由分块的判定与加载

分块判定逻辑 getRouteChunk 与文件侧的映射规则一致:

const getRouteChunk = (pathname = "/") => {
  if (pathname === "/") return "home"
  if (pathname === "/docs" || pathname.startsWith("/docs/")) return "docs"
  if (pathname === "/components" || pathname.startsWith("/components/")) return "components"
  return "other"
}

对外暴露的 loadRouteTranslations 固定加载 ["common", currentRouteChunk] 两个分块:

export const loadRouteTranslations = async (pathname = "/") => {
  currentRouteChunk = getRouteChunk(pathname)
  return loadTranslationChunks(get(currentLang), ["common", currentRouteChunk])
}

底层 loadTranslationChunk 做了三层保护,值得注意:

  1. 加载状态缓存loadedChunks[lang] 记录已加载分块,重复请求直接返回;
  2. 并发去重loadingChunks${lang}.${chunk} 为键缓存进行中的 Promise,同一分块的多路并发请求只会发起一次网络/模块加载;
  3. 合并写入:加载结果通过 mergeLoadedTranslations 合并进 loadedTranslations store,而不是整体替换,保证各分块互不覆盖。

此外,hasChunkedTranslation 通过检查是否存在 <lang>.common.json 来判断某语言是否采用分块文件;若某语言没有分块文件,loadTranslationChunks 会回退到加载整文件 <lang>.json 的旧形态(源码)。这解释了为什么 开发说明文档 要求「不要创造新的文件形态」——加载器对文件形态的识别是写死在 glob 与 chunkNames 中的。

语言切换、回退与文案处理

语言切换入口是 setLang:先加载 ["common", currentRouteChunk],再依次更新 store、把 ?lang=xx 写入 URL query、写入 localStorage(键名 lang)、并更新 <html>lang / dir 属性(方向取自语言元数据的 __direction,如 arfaheurrtl,其余 ltr)。跨标签页场景下,windowstorage 事件监听器会同步其他标签页的语言变更。

语言元数据有两处来源:硬编码在 languageMetadata 中的 26 种语言(用于翻译加载前下拉框展示),以及各语言 *.common.json 中的 __name / __code / __direction 键。运行时 translateSync 对以 __ 开头的键会优先返回语言元数据。

缺失键回退开发说明文档 中「Do not leave a key only in English... Missing keys fall back to English at runtime, but translation files should still be synchronized」一说的实现:handleMissingTranslation 在当前语言缺失某键时,会后台补载该语言全部分块(防止键其实在未加载分块中),同时加载英文全部分块并返回英文值(找不到英文值则直接显示键本身),并在控制台输出告警。因此运行时回退是安全网,而文件层面的键同步仍然是硬性要求

翻译值在渲染前还会经过两级处理:

  • convertBackticksToCode:把值中反引号包裹的文本(如 `btn`)转为 HTML <code> 标签,并对 & < > " ' 做实体转义;
  • replaceVariables:把 {{variable}} 占位符替换为调用方传入的变量值。

页面侧则通过极简的 Translate.svelte 组件消费翻译:{@html $t(text, variables)},其中 $t 即上面导出、由 currentLangloadedTranslations 两个响应式源派生的翻译函数。

同步工作流:从源码提取字符串到跨语言补齐

字符串从哪里来

脚本端以 translationConfig.js 为核心,扫描 src/routes 下所有 .md / .svelte / .js 源文件(sourceFilePattern),提取可翻译字符串:

  • Svelte / JS 文件extractSvelteTranslations 匹配 $t("...")$t('...')、$t(`...`) 调用,以及 <Translate text="..."> 组件的字面量文本;
  • Markdown 文件extractMarkdownTranslations 先剥离 frontmatter 与 <script>/{#each} 块,按行解析,跳过 fenced code block 内的行,通过 parseMarkdownLine 把链接渲染为 HTML <a> 后作为键的一部分保留;
  • 跳过规则shouldSkipText 内置大量模式,跳过纯数字、--css-variable、表格分隔行、HTML 标签行、class="..."、Svelte 表达式、箭头符号等不适合翻译的文本。

两处显式的「不翻译」机制与 开发说明文档 的「Do not translate if there's a comment saying don't translate a specific section」对应:

  1. 整页排除路径 excludedPathsdocs/upgradedocs/v5(marketing)blogstoresrc/lib/scriptsCHANGELOG.md,命中即返回空提取结果;
  2. 行内标记 doNotTranslateAfterMarker<!-- DO NOT TRANSLATE ANYTHING BELOW THIS LINE -->):对配置在 skipAfterMarkerPaths 中的文件(目前是 docs/layout-and-typography/+page.md),标记之后的内容一律不提取。

测试 translationConfig.test.js 对这些行为做了覆盖:排除路径判定、DO NOT TRANSLATE 标记前后文本、组件示例标题(### ~...)跳过、$t<Translate> 提取、{{name}} 占位符保留、Markdown 链接转为 HTML 链接等。

lang:add:一键补齐缺失键

addTranslations.jslang:add 脚本的入口,package.json 中已固定带上 --write 参数。它调用 syncMissingTranslations,流程为:

  1. 从全部源文件提取「期望键」,按分块聚合(getExpectedKeysByChunk);
  2. 对每个分块,把期望键中缺失的键写入英文文件 en.<chunk>.json,新键的值初始为键本身;
  3. 再遍历其他所有语言,若某语言文件已存在但缺该键,则用英文值(即键本身)补齐,并在 --write 时落盘;
  4. 无论是否写盘,都输出 add <文件>: <键> 变更清单供人工翻译。

这里也印证了 开发说明文档 的描述:「addTranslations.js uses the source file path to choose the English translation file when extracting new strings」——即源文件路径决定新键进入哪个英文分块文件(经由 getTranslationChunkForFile)。

lang:prunelang:report:清理与统计

  • pruneTranslations 找出「只被排除路径使用、不再出现在任何允许源文件中」的过期键,lang:prune 干跑预览,lang:prune:write 真正从所有语言文件中删除;
  • reportTranslations.js 以表格形式输出每种语言的 unused / extra / missing 键数量,以及每个分块的源键数与英文键数,-v 可追加列出「英文键但源码已不存在」的过期键。

校验:键一致性与占位符完整性

validateTranslationslang:validate 的核心,检查项分为六类问题类型:

问题类型 含义
missing-source-key 键被 docs/components/other 等分块的源文件使用,但不在对应 en.<chunk>.json
missing-file 某语言缺少 <lang>.<chunk>.json 文件
missing-translation-key 某语言文件缺少英文文件已有的键
extra-translation-key 某语言文件多出英文文件没有的键
placeholder-mismatch 译文中的 {{variable}} 占位符集合与英文不一致
excluded-route-key 键存在于翻译文件中,却只被排除路径的源文件使用(应删除或移入允许路径)

其中占位符比对基于 extractPlaceholders{{\s*[\w.]+\s*}} 模式,把英文与译文两侧的占位符排序后逐一比较——这正是 开发说明文档「Keep placeholders and markup intact in translated values, including {{variable}}」规则的自动化保障。

此外,translationConfig.test.js 中的测试「all language chunk files have the same keys as English」直接遍历全部语言 × 分块,断言每个非英文文件与英文文件键集合完全一致(无 extra、无 missing);另一条断言确保 excluded-route-key 问题为零。因此执行 bun run verify(并行 testlang:validate)是保证翻译文件体系一致性的标准门禁。

翻译规则清单(内容规范)

开发说明文档 的「Translation rules」一节定义了译文内容层面的硬性规则,逐条继承如下:

  1. 品牌拼写:必须写作 "daisyUI"——首字母小写 "d","UI" 部分大写;
  2. 不翻译标记:源文件中标注「此段以下不翻译」的注释区域,其内容不翻译;
  3. 包名与代码标识符原样保留:例如 npm 包名 daisyui 是小写,因为它就是包名;
  4. 不翻译技术性术语:编程、开发、Web 设计、设计系统领域的技术术语保持原文;
  5. 代码内容不翻译:fenced code block、行内反引号、<code>...</code>、命令名、包名、文件名、文件路径、URL、CSS 变量、选择器、属性、取值、Svelte 表达式、键盘快捷键;
  6. daisyUI 组件名与类名不翻译
  7. 品牌名、商标、产品名、库名、框架名、工具名不翻译
  8. daisyUI 颜色名不翻译
  9. 结构与标记保真:翻译值中必须完整保留 Markdown 与 HTML 结构——相同的标签、属性、实体、链接 URL、链接 target 与 rel 属性,只翻译可见的叙述性文字(且该文字不是受保护的品牌名或代码 token);不得引入、删除、重排或改写标记;如果源文本把 <li> 表示为代码或转义文本,翻译中也要保持代码/转义形态,不能变成真正的 HTML 标签;
  10. 插值与占位符保真:完整保留 {{variable}} 占位符以及 {data.themes.length} 这类 Svelte 风格表达式;
  11. 语言元数据键*.common.json 中):__code 保持短大写语言代码,__direction 只能是 ltrrtl__name 只翻译成该语言的本地显示名(对照 languageMetadata 可看到 26 种语言的 __name 均为本地名,且 arfaheurrtl);
  12. 排除路径不翻译(与脚本侧 excludedPaths 一致):
    • packages/docs/src/routes/(routes)/docs/upgrade
    • packages/docs/src/routes/(routes)/docs/v5
    • packages/docs/src/routes/(routes)/(marketing)
    • packages/docs/src/routes/(routes)/blog
    • packages/docs/src/routes/(routes)/store
    • packages/docs/src/lib/scripts
    • CHANGELOG.md

维护速查:关键文件与命令

事项 位置 / 命令
开发规范总入口 packages/docs/AGENTS.md
翻译文件目录 packages/docs/src/translation/<language>.<chunk>.json
运行时 i18n 加载器 packages/docs/src/lib/i18n.svelte.js
翻译提取/同步/校验核心 packages/docs/src/lib/scripts/translationConfig.js
补齐缺失键 bun run lang:add(在 packages/docs 下,等价于 addTranslations.js --write
预览 / 执行清理过期键 bun run lang:prune / bun run lang:prune:write
翻译健康报告 bun run lang:report-v 显示过期英文键)
键一致性校验 bun run lang:validate
测试 + 校验一体化 bun run verify
提取/路径规则测试 packages/docs/src/lib/scripts/translationConfig.test.js

总结来说,daisyUI 文档站的多语言体系可以概括为三层:结构层(语言 × 分块的 JSON 文件 + 路径到分块的确定性映射)、运行时层(首屏 eager 英文 common/home、按路由懒加载当前分块、缺失键英文回退、占位符替换与反引号转 <code>)、治理层(从源码提取字符串、跨语言同步补齐、过期键清理、键与占位符一致性校验)。三层共享同一份 chunkNames 与排除路径配置,使得「文档约定、运行时行为、脚本校验」三者在 packages/docs/AGENTS.mdi18n.svelte.jstranslationConfig.js 之间保持严格一致。

登录后查看全文
热门项目推荐
相关项目推荐