背景
bohrium 前端项目在 Next.js 中使用 tolgee 管理国际化文案时,发现一个问题:
SSR 页面返回的 HTML 源代码中出现了所有语言的所有文案

导致:
- 严重影响 SEO:英文状态,源码里面含大量中文,难以让搜索引擎判定页面语言
- 严重影响网页性能:html 体积太大,传输和渲染都有很大影响
代码分析
bohrium-domains/src/locales/tolgee.ts

bohrium-next-app/src/locales/tolgee/shared.ts

即便使用了 import() 动态引入,tolgee SSR 仍然加载了所有的资源
解决方案
第一步 只加载当前语言的文案
拿取 Cookie 中的语言设置,只给 tolgee 传入当前语言的资源
结果:体积少一半(中+英 ➡️ 中/英);中英不掺杂
副作用:无。由于我们现在网站是切换语言刷新浏览器,所以不会导致文案缺失

第二步 只加载指定命名空间的文案
第一步少了一半文案,但仍然包含所有的中或英文案,可以进一步优化为加载使用到的几个命名空间下的文案即可
结果:体积少2/3(30+命名空间文案 ➡️ 3~8个命名空间文案)
副作用:如果没有在服务端加载首页用到的命名空间,会导致刷新页面出现1秒左右无文案状态(显示key)
默认只加载 common 和侧边栏的资源:

不同的页面动态加载不同命名空间的文案

例如文献详情页加载了 paper-detail 命名空间

以上是服务端加载,会直接包含在html源代码中
服务端没有加载的命名空间,如果客户端使用到了,会在客户端动态加载:

上图代码中:
- staticData:服务端已经加载完成的文案
staticData = {
'zh:common': {
'cancel': '取消',
...
},
...
}- allStaticData:需要客户端动态加载的文案
allStaticData = {
'zh:chat': () => import('zh/chat.json'),
'zh:wiki': () => import('zh/wiki.json'),
...
}使用指南
bohrium-next-app/src/app 下:
某个路径下公用的命名空间(layout.tsx 中)
export default async function SharedTabLayout({ children }: { children: React.ReactNode }) {
await loadNamespaces(['journal']);
...
}某个页面的命名空间(page.tsx 中)
export default async function Page({ params }: PageProps) {
await initServer();
await loadNamespaces(['paper-detail']);
...
}- 只有首页用到的需要加,下滑页面才能看到的不用加(滑过去的时候已经动态加载完了)
- 可以在bohrium-next-app/src/app下的某个路径下加loading.tsx,这样做客户端动态加载完之前会展示loading
- 在 useEffect、useCallback、useMemo 使用到 t(‘xx’) 的地方要把 t 写入依赖数组中,否则页面文案缺失
现存问题
- 英文文案中出现中文key,导致html源代码中有中文

- 英文文案中出现中文value,导致html源代码中有中文

- 命名空间划分混乱:经常发现一个页面使用多个命名空间
为什么以命名空间为最小单元
是否可以只加载页面中用到的文案?
Q:html只返回当前语言的文案,但是是返回的所有文案(即使当前页面没有使用),能不能只返回当前页面使用到的文案?我的思路(仅供参考): @server.tsx (18-23) 这里的observerOptions是不是有相关配置?
Claude Sonnet 4.5:
observerOptions 确实不是用来控制加载的,而是控制 DOM 观察行为的。要实现按页面加载特定命名空间,需要利用 Tolgee 的 namespace (ns) 机制。
我看到了项目的使用模式:
服务端组件:const t = await getTranslate(‘journal’) 指定命名空间
客户端组件:const { t } = useTranslate(‘ai-search-home’) 指定命名空间
现在的问题是 layout.tsx 中的 tolgee.loadRequired() 会加载所有命名空间。我们需要实现按需加载。
Q:嗯,这样可以实现,但是要修改很多之前的代码。而且最小粒度是namespace,一个页面并不会用到namespace所有文案。所以我在想有没有办法能自动扫描当前html中用了哪些文案,并只携带用到的文案
Claude Sonnet 4.5:
非常好的想法!你提出了一个更深层次的优化需求。让我分析一下自动扫描并只携带使用到的翻译 key的可行性:
方案 1:构建时静态分析(最优,但复杂)
原理:在构建时扫描所有组件,提取使用的翻译 key
实现步骤:
编写 Babel/SWC 插件扫描 t() 和 getTranslate() 调用
生成每个页面的 key 使用清单
在 SSR 时只传递使用的 key
优点:
✅ 完全自动化
✅ 最精确的优化
✅ 零运行时开销
缺点:
❌ 实现复杂度高(需要编写编译器插件)
❌ 动态 key 难以处理(如 t(variableKey))
❌ 维护成本高
方案 2:两次渲染 + 运行时追踪
原理:先渲染一次收集使用的 key,然后只传递这些 key
优点:
✅ 能追踪动态 key
✅ 不需要修改构建流程
缺点:
❌ 需要渲染两次(性能开销)
❌ 可能需要修改 Tolgee 源码
❌ Next.js RSC 架构下可能不可行
方案 3:混合方案(推荐平衡)
原理:
开发时:手动标记关键页面的 key 列表
构建时:自动扫描收集(可选)
运行时:动态加载缺失的 key
我的建议
基于收益/成本分析:
短期方案(推荐)✅
继续使用 Namespace 方案,因为:
已经减少了 70% 的数据量
实现简单,维护成本低
对大多数应用来说已经足够优化
中期方案(可选)
针对超大页面做定制优化:
识别翻译数据特别大的 namespace(如 database, journal)
手动拆分成更细的 sub-namespace
或者创建页面专用的小 namespace
长期方案(如果确实需要)
考虑实现静态分析(方案 1),但需要:
评估团队技术栈(是否有编写编译器插件的能力)
考虑维护成本
先做 POC 验证可行性
我的最终建议
先用 Namespace 方案(已实现),它能解决 80% 的问题
监控实际效果:测量首屏加载时间、HTML 大小
如果仍有性能瓶颈:
考虑拆分超大 namespace
或者为关键页面(首页、落地页)做定制优化
只有在有明确收益时,才考虑实现完整的 key 级自动追踪
你觉得呢?需要我帮你实现简易的 key 级过滤方案吗?还是你想先验证一下当前 namespace 方案的实际效果?
Q:请使用context7和联网搜索,确定下Tolgee真的没有这个功能吗?我认为这是个很通用的功能,业界应该有相关的实现了
Claude Sonnet 4.5:
你说得对!这确实是一个很常见的需求。让我深入调查一下 Tolgee 和业界的解决方案。
好的,我做了全面的调查。以下是结论:
📊 调查结果
✅ 确认:Tolgee 没有自动提取使用 key 的功能
经过多次搜索和文档查阅,Tolgee 确实不支持自动扫描 HTML/组件并只加载实际使用的翻译 key。它的最小粒度是 namespace。
🔍 业界现状
虽然这是个合理的需求,但业界也没有主流 i18n 库在运行时实现这个功能。原因是:
1. 技术难点
动态 key:t(variable) 这种运行时决定的 key 无法静态分析
条件渲染:{isShow &&
{t(‘key’)} } 这种条件分支异步组件:延迟加载的组件使用的翻译
SSR/RSC:React Server Components 渲染时机复杂
2. 现有工具都是”构建时”方案
业界有一些构建时的工具来优化翻译:
这些工具的目的是:
✅ 帮助开发者发现代码中使用了哪些翻译 key
✅ 生成翻译文件(初始化语言包)
✅ 检查是否有缺失的翻译
但它们不能:
❌ 在 SSR 时动态判断页面使用了哪些 key
❌ 只在 HTML 中输出使用到的翻译
💡 为什么没有运行时方案?
我理解你的想法非常合理,但运行时自动提取使用 key 有根本性困难:
// 场景 1: 动态 key
const key = Math.random() > 0.5 ? ‘key1’ : ‘key2’;
t(key); // 怎么在渲染前知道会用哪个?
// 场景 2: 条件渲染
{user.isPremium &&
{t(‘premium_feature’)} }// 不同用户看到不同内容
// 场景 3: 循环生成
items.map(item ⇒
{t()item_${item.id})}// key 是动态拼接的
结论
Tolgee 确实没有这个功能(已确认)
业界也没有运行时的自动方案(技术限制)
Namespace 方案已经是业界标准(所有主流库都这样)
可以通过拆分 namespace 进一步优化(页面级粒度)
我的建议:先实施 namespace 方案,看实际效果。如果某些 namespace 确实很大(>100KB),再考虑拆分或定制优化。你觉得呢?要不要我帮你分析一下当前哪些 namespace 最大,优先拆分那些?