背景

bohrium 前端项目在 Next.js 中使用 tolgee 管理国际化文案时,发现一个问题:

SSR 页面返回的 HTML 源代码中出现了所有语言的所有文案

导致:

  1. 严重影响 SEO:英文状态,源码里面含大量中文,难以让搜索引擎判定页面语言
  2. 严重影响网页性能: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 写入依赖数组中,否则页面文案缺失

现存问题

  1. 英文文案中出现中文key,导致html源代码中有中文

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

  1. 命名空间划分混乱:经常发现一个页面使用多个命名空间

为什么以命名空间为最小单元

是否可以只加载页面中用到的文案?

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

实现步骤:

  1. 编写 Babel/SWC 插件扫描 t() 和 getTranslate() 调用

  2. 生成每个页面的 key 使用清单

  3. 在 SSR 时只传递使用的 key

优点:

  • ✅ 完全自动化

  • ✅ 最精确的优化

  • ✅ 零运行时开销

缺点:

  • ❌ 实现复杂度高(需要编写编译器插件)

  • ❌ 动态 key 难以处理(如 t(variableKey))

  • ❌ 维护成本高

方案 2:两次渲染 + 运行时追踪

原理:先渲染一次收集使用的 key,然后只传递这些 key

优点:

  • ✅ 能追踪动态 key

  • ✅ 不需要修改构建流程

缺点:

  • ❌ 需要渲染两次(性能开销)

  • ❌ 可能需要修改 Tolgee 源码

  • ❌ Next.js RSC 架构下可能不可行

方案 3:混合方案(推荐平衡)

原理:

  1. 开发时:手动标记关键页面的 key 列表

  2. 构建时:自动扫描收集(可选)

  3. 运行时:动态加载缺失的 key

我的建议

基于收益/成本分析:

短期方案(推荐)✅

继续使用 Namespace 方案,因为:

  1. 已经减少了 70% 的数据量

  2. 实现简单,维护成本低

  3. 对大多数应用来说已经足够优化

中期方案(可选)

针对超大页面做定制优化:

  • 识别翻译数据特别大的 namespace(如 database, journal)

  • 手动拆分成更细的 sub-namespace

  • 或者创建页面专用的小 namespace

长期方案(如果确实需要)

考虑实现静态分析(方案 1),但需要:

  1. 评估团队技术栈(是否有编写编译器插件的能力)

  2. 考虑维护成本

  3. 先做 POC 验证可行性

我的最终建议

  1. 先用 Namespace 方案(已实现),它能解决 80% 的问题

  2. 监控实际效果:测量首屏加载时间、HTML 大小

  3. 如果仍有性能瓶颈:

  • 考虑拆分超大 namespace

  • 或者为关键页面(首页、落地页)做定制优化

  1. 只有在有明确收益时,才考虑实现完整的 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 是动态拼接的

结论

  1. Tolgee 确实没有这个功能(已确认)

  2. 业界也没有运行时的自动方案(技术限制)

  3. Namespace 方案已经是业界标准(所有主流库都这样)

  4. 可以通过拆分 namespace 进一步优化(页面级粒度)

我的建议:先实施 namespace 方案,看实际效果。如果某些 namespace 确实很大(>100KB),再考虑拆分或定制优化。你觉得呢?要不要我帮你分析一下当前哪些 namespace 最大,优先拆分那些?