daisyUI 文档站国际化体系:分块翻译文件结构、路由分块加载与同步工作流解析
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、daisyui 以 workspace:* 形式引用本仓库的 daisyui 包,运行环境要求 Node >=20.18.1,脚本执行使用 bun(例如 dev、test、verify)。
其中与翻译体系直接相关的 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.json、en.docs.json、fa.components.json、zh_hans.home.json; - 第一段是语言代码(如
en、de、es、zh_hant),第二段是分块名。
当前的 5 个分块(chunk)及其用途在源码 translationConfig.js 中被集中定义为 chunkNames = ["common", "home", "docs", "components", "other"],与文档说明一一对应:
| 分块 | 内容范围 | 对应路由 |
|---|---|---|
common |
跨路由共享的字符串:布局、导航、页脚与通用 UI 文案 | 全站共享 UI(src/lib/ 下的组件) |
home |
首页 / 的文案 |
首页 |
docs |
/docs 下的文档页 |
/docs 及 src/routes/(routes)/docs/ |
components |
/components 下的组件页 |
/components 及 src/routes/(routes)/components/ |
other |
不属于 home、docs、components 的其余页面 | 其他所有路由 |
「按路径选分块」的规则在 开发说明文档 中写得很明确:
src/routes/(routes)/+page.svelte或+page.md→*.home.jsonsrc/routes/(routes)/docs/...→*.docs.jsonsrc/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"
}
注意两个细节:
src/lib/下的源文件(导航、页脚等可复用 UI 组件)一律归入common,这正是「共享 UI 文案进 common」规则在提取端的落地;- 兜底分支返回
other,即任何没被显式覆盖的路由都落入other分块,保证新增页面不会漏掉归属。
单元测试 translationConfig.test.js 用一组真实路径验证了该映射:/routes/(routes)/+page.md → home、docs/colors/+page.md → docs、components/button/+page.md → components、src/lib/components/Nav.svelte → common、store/+page.svelte → other。
键值组织方式:键即英文原文
查看真实文件可以发现,翻译 JSON 的键(key)就是英文源文本本身(通常是完整的句子,甚至包含 HTML 标记),值则是对应语言的译文。例如 en.home.json:
"install-title": "Install daisyUI",
"homepage_h1": "Faster, cleaner, easier <br />Tailwind CSS development",
"install-btn": "Install guide"
对应地,zh_hans.home.json 中键完全相同、只有值被翻译:
"install-title": "安装 daisyUI",
"homepage_h1": "更快、更简洁、更简单的<br/>Tailwind 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 模式重复打包。从源码结构看,这是为了控制首屏体积:只有 common 与 home 两个英文分块进入初始 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 做了三层保护,值得注意:
- 加载状态缓存:
loadedChunks[lang]记录已加载分块,重复请求直接返回; - 并发去重:
loadingChunks以${lang}.${chunk}为键缓存进行中的 Promise,同一分块的多路并发请求只会发起一次网络/模块加载; - 合并写入:加载结果通过
mergeLoadedTranslations合并进loadedTranslationsstore,而不是整体替换,保证各分块互不覆盖。
此外,hasChunkedTranslation 通过检查是否存在 <lang>.common.json 来判断某语言是否采用分块文件;若某语言没有分块文件,loadTranslationChunks 会回退到加载整文件 <lang>.json 的旧形态(源码)。这解释了为什么 开发说明文档 要求「不要创造新的文件形态」——加载器对文件形态的识别是写死在 glob 与 chunkNames 中的。
语言切换、回退与文案处理
语言切换入口是 setLang:先加载 ["common", currentRouteChunk],再依次更新 store、把 ?lang=xx 写入 URL query、写入 localStorage(键名 lang)、并更新 <html> 的 lang / dir 属性(方向取自语言元数据的 __direction,如 ar、fa、he、ur 为 rtl,其余 ltr)。跨标签页场景下,window 的 storage 事件监听器会同步其他标签页的语言变更。
语言元数据有两处来源:硬编码在 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 即上面导出、由 currentLang 与 loadedTranslations 两个响应式源派生的翻译函数。
同步工作流:从源码提取字符串到跨语言补齐
字符串从哪里来
脚本端以 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」对应:
- 整页排除路径 excludedPaths:
docs/upgrade、docs/v5、(marketing)、blog、store、src/lib/scripts与CHANGELOG.md,命中即返回空提取结果; - 行内标记
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.js 是 lang:add 脚本的入口,package.json 中已固定带上 --write 参数。它调用 syncMissingTranslations,流程为:
- 从全部源文件提取「期望键」,按分块聚合(
getExpectedKeysByChunk); - 对每个分块,把期望键中缺失的键写入英文文件
en.<chunk>.json,新键的值初始为键本身; - 再遍历其他所有语言,若某语言文件已存在但缺该键,则用英文值(即键本身)补齐,并在
--write时落盘; - 无论是否写盘,都输出
add <文件>: <键>变更清单供人工翻译。
这里也印证了 开发说明文档 的描述:「addTranslations.js uses the source file path to choose the English translation file when extracting new strings」——即源文件路径决定新键进入哪个英文分块文件(经由 getTranslationChunkForFile)。
lang:prune 与 lang:report:清理与统计
- pruneTranslations 找出「只被排除路径使用、不再出现在任何允许源文件中」的过期键,
lang:prune干跑预览,lang:prune:write真正从所有语言文件中删除; - reportTranslations.js 以表格形式输出每种语言的 unused / extra / missing 键数量,以及每个分块的源键数与英文键数,
-v可追加列出「英文键但源码已不存在」的过期键。
校验:键一致性与占位符完整性
validateTranslations 是 lang: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(并行 test 与 lang:validate)是保证翻译文件体系一致性的标准门禁。
翻译规则清单(内容规范)
开发说明文档 的「Translation rules」一节定义了译文内容层面的硬性规则,逐条继承如下:
- 品牌拼写:必须写作 "daisyUI"——首字母小写 "d","UI" 部分大写;
- 不翻译标记:源文件中标注「此段以下不翻译」的注释区域,其内容不翻译;
- 包名与代码标识符原样保留:例如 npm 包名
daisyui是小写,因为它就是包名; - 不翻译技术性术语:编程、开发、Web 设计、设计系统领域的技术术语保持原文;
- 代码内容不翻译:fenced code block、行内反引号、
<code>...</code>、命令名、包名、文件名、文件路径、URL、CSS 变量、选择器、属性、取值、Svelte 表达式、键盘快捷键; - daisyUI 组件名与类名不翻译;
- 品牌名、商标、产品名、库名、框架名、工具名不翻译;
- daisyUI 颜色名不翻译;
- 结构与标记保真:翻译值中必须完整保留 Markdown 与 HTML 结构——相同的标签、属性、实体、链接 URL、链接 target 与 rel 属性,只翻译可见的叙述性文字(且该文字不是受保护的品牌名或代码 token);不得引入、删除、重排或改写标记;如果源文本把
<li>表示为代码或转义文本,翻译中也要保持代码/转义形态,不能变成真正的 HTML 标签; - 插值与占位符保真:完整保留
{{variable}}占位符以及{data.themes.length}这类 Svelte 风格表达式; - 语言元数据键(
*.common.json中):__code保持短大写语言代码,__direction只能是ltr或rtl,__name只翻译成该语言的本地显示名(对照 languageMetadata 可看到 26 种语言的__name均为本地名,且ar、fa、he、ur为rtl); - 排除路径不翻译(与脚本侧
excludedPaths一致):packages/docs/src/routes/(routes)/docs/upgradepackages/docs/src/routes/(routes)/docs/v5packages/docs/src/routes/(routes)/(marketing)packages/docs/src/routes/(routes)/blogpackages/docs/src/routes/(routes)/storepackages/docs/src/lib/scriptsCHANGELOG.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.md、i18n.svelte.js 与 translationConfig.js 之间保持严格一致。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00