- 后端
- 前端
- CRM
- 人工智能
- AI Agent
【免费下载链接】crm
Comp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.
这是一篇围绕仓库内 adrs/i18n.md 架构决策记录展开的技术解读。Comp AI CRM(项目根目录)目前将全部用户可见文案硬编码为英文,该 ADR 提出引入 next-intl 目录(catalog)机制,让"新增一种语言"从一次全局重构变成一次纯翻译提交。读完本文,你将掌握这套方案的设计边界(哪些字符串要进目录、哪些格式不翻译)、语言选择与路由策略(cookie +
user.locale,不引入 URL segment)、packages/ui的零依赖设计、CI 强制检查与伪语言校验手段,以及约十个 PR 的渐进式落地路线。
一、背景与动机:为什么需要让 CRM 说多种语言
ADR 以真实使用场景开场:提案作者同时在越南的两家公司(一家初创、一家成熟企业)重度使用本 CRM。当前仓库里所有文案都硬编码为英文——打开 apps/app 下的任意页面组件,都能看到直接写在 JSX 里的句子。这种做法的直接后果是:
- 本地化即 fork 风暴:想让某个地区使用自己的语言,就必须 fork 每一个渲染句子的文件;
- 上游同步成本极高:一旦上游更新文案或组件结构,fork 后的本地化补丁需要重新逐文件对账,工作量巨大;
- 社区贡献无入口:其他国家的用户即使愿意贡献语言包,也没有一个统一的、可被自动化校验的载体。
因此提案的核心诉求是:把"说另一种语言"从一次重构降级为一次翻译任务。这也是adrs/README.md所鼓励的提案格式——说明想改什么、为什么现有行为是问题、替代方案会破坏什么。
从当前仓库现状看,该 ADR 属于"提案待落地"状态:apps/app/package.json的依赖列表中尚没有next-intl,packages/db/prisma/schema.prisma的User模型也还没有locale列。文中所有方案描述均以 ADR 提案为准,并标注可对照的源码证据。
二、方案设计:一切用户可见字符串进入统一目录
2.1 目标范围:apps/app与packages/ui
提案明确圈定改造范围:apps/app和packages/ui中每一个用户可见字符串都必须经由目录(catalog)输出。这一范围覆盖了 Web 前端的所有渲染层——包括 app 目录 下的页面组件,以及 packages/ui/src/components 下的通用组件库。
这一约束非常彻底:不仅是页面正文,还包括可翻译的属性(translatable attributes),例如aria-label、placeholder、title等;以及toast 通知文案。当前仓库中 toast 是 sonner 驱动的,例如 create-company-sheet.tsx/[slug]/companies/create-company-sheet.tsx#L76-L89) 中的toast.success(\${company.name} added.`)与toast.error(error.message)`,这些都属于必须进入目录的字符串。
2.2 本轮只做英文:语言是翻译工作,不是重构
方案采用 next-intl 作为基础设施,但本轮只内置英文。这背后的设计意图值得注意:
"Strings only; date, number and currency formatting stay."
即只翻译字符串,不翻译格式。日期、数字、货币的呈现格式保持现状不动。从源码看,packages/ui的日历组件 calendar.tsx 已经通过react-day-picker的Locale类型与locale?.code参数支持了按区域设置格式化(如date.toLocaleString(locale?.code, { month: "short" })),说明"格式层"已有独立的国际化能力,与文案层天然解耦,这正是该边界可行性的底层依据。
之所以"英文优先",是为了把改造风险降到最低:渲染出的英文文本与改造前逐字一致,整个工作是"管道铺设(plumbing)"而非"文案校对(copy edit)"——CI 可以据此验证没有引入任何措辞变化。
2.3 语言选择与路由策略:cookie +user.locale,不要 URL segment
这是本方案最有个性的设计决策。常见的多语言站点会在 URL 上挂语言段(如/vi/companies)并配套 middleware 重写,但 ADR 明确拒绝这条路,理由是本应用整体位于登录墙之后:
- 无 URL segment:不在路径中引入
/en/、/vi/前缀; - 无 middleware:不为语言切换增加任何请求拦截逻辑;
- 一个 cookie 加一个
user.locale列即可:语言偏好存储在客户端 cookie(快速响应)+ 用户表列(持久化)。
当前仓库的 proxy.ts 展示了应用的门控结构:未登录请求会被重定向到/sign-in,登录后按 workspace 门控分流到[slug]路径。整个应用处于认证之后,因此语言偏好完全可以通过登录态携带,无需依赖 URL 暴露。从数据结构看,schema.prisma 的User模型当前还没有locale字段,这正是 ADR 提出要新增的列;而其下AppSetting单行 upsert 的读写模式(参见 packages/db/src/settings.ts 的SETTINGS_ID = "app"与upsert用法)可作为"设置类字段持久化"的现成参考实现。
三、packages/ui的零 i18n 依赖策略
ADR 对组件库提出了一条硬性约束:packages/ui不引入任何 i18n 依赖。
设计模式是:
- 包内自带英文默认值:组件库本身直接书写英文文案,保持其独立可运行、可测试;
- 通过 Provider 覆盖:由上层应用(
apps/app)注入翻译覆盖,组件渲染时优先取注入的翻译,取不到则回退英文。
这样做的好处是双重的。其一,组件库保持纯净,任何不关心 i18n 的消费方(包括测试环境、文档演示)都能零成本使用;其二,将"翻译职责"收敛到应用层,packages/ui的消费者只需对接一个 provider 契约。这与上面日历组件locale作为可选 prop 透传的风格一致——组件库只提供"接缝",由上层决定注入什么。
四、强制检查:让目录成为唯一事实来源
只靠约定无法长期维持"所有文案进目录",因此 ADR 配套了两层强制执行机制:
4.1 CI 中的 AST 检查器
在 CI 中运行一个基于 AST(抽象语法树)的静态检查器,出现以下情况即构建失败:
- 硬编码的 JSX 文本:例如直接写在 JSX 里的
New company、Add company、Cancel(当前大量存在于 create-company-sheet.tsx/[slug]/companies/create-company-sheet.tsx#L41-L48) 这类组件中); - 可翻译属性:
placeholder、aria-label等属性上的硬编码字符串; - toast 文案:sonner 的
toast.success(...)/toast.error(...)传入的字面量。
该检查器的存在意味着每新增一个字符串都必须进入目录,否则 CI 失败——文案的"单一来源"由机制保证,而非开发者的自觉。这与仓库现有工程纪律一脉相承:仓库内已有自定义 oxlint 插件集(见 tools/oxlint/anti-slop),通过@oxlint/plugins定义了一系列拒绝低质量模式的规则,说明"以自定义静态检查约束代码模式"在该项目中是成熟的实践路径。
4.2 伪语言(pseudo-locale)兜底
静态分析无法发现所有问题——比如字符串拼接、隐式插值、运行时才暴露的硬编码。为此 ADR 提出伪语言校验:用一套刻意加长的伪翻译(例如把每个字符替换为带重音符号的变体并拉长文本)渲染整个应用,人工或截图对比即可发现:
- 哪些文案没有被目录接管(仍显示纯英文);
- 哪些布局在文本变长后会溢出、截断或错位(硬编码宽度/固定高度的隐患)。
这层校验捕捉的是"静态分析看不到的东西",两者互补形成完整防线。
五、成本与取舍
ADR 不回避代价,明确列出两条:
/(落地页)放弃完整静态预渲染,改为输出一个静态外壳(static shell),动态部分(如语言偏好)在客户端解析。当前落地页 page.tsx/page.tsx#L11-L15) 是带metadata的纯静态页面,改造后其预渲染范围会收窄;- 其他预渲染路由保持原有预渲染能力——只有依赖 cookie 的页面受影响,不会波及全部路由。这里可参考
apps/app/lib/env.ts的isMarketing()逻辑(IS_MARKETING环境变量控制落地页是否对外公开,见 env.ts),落地页与非营销页面本就分流处理,改造影响面可控。
其余成本约束包括:每个新字符串必须进目录否则 CI 失败;英文保持唯一内置语言;语言包以贡献形式合入,缺失的 key 按字符串粒度回退到英文,因此发布流程永远不必等待某一种翻译完成。
六、落地路线:约十个 PR,渐进式推进
ADR 给出的执行计划是分片落地、每片小且绿(small and green):
- 第 1 片:基础设施——引入 next-intl、搭建目录结构与 Provider、建立 CI 检查器与伪语言管线;
- 后续 N 片:按功能区域逐个提取文案——公司/联系人/商机/设置/仪表盘等模块各自一个 PR,将硬编码英文迁移进目录;
- 全部改造已在提案作者的 fork 上验证运行。
最终形态是:英文是唯一内置语言,越南语作为第一个社区语言包由提案作者贡献,并附带"如何用编码 Agent 最高效地翻译语言包"的指南——这也呼应了本仓库 apps/agent 所代表的 Agent 优先定位:翻译工作本身也可借助 Agent 完成。
从仓库现状看,apps/app 下还有大量待迁移的硬编码文案(例如各处 settings 页面的按钮、表格、sheet 标题),这与 ADR 描述的"本轮全量英文迁移"工作量一致;而 packages/ui 的组件文本同样如此。任何贡献者若想跟进该方案,都可以从"基础设施 PR"或"单个模块的提取 PR"入手,并遵守目录 key 的命名约定与 CI 检查。
七、总结:一份可以照着实现的 i18n 决策记录
这份 ADR 的价值在于它把"多语言支持"这个常见但容易失控的需求,收敛成了一组清晰、可执行、有边界的决策:
| 决策点 | 方案 |
|---|---|
| 基础设施 | next-intl 目录(catalog),本轮仅内置英文 |
| 翻译边界 | 只翻译字符串;日期、数字、货币格式保持现状 |
| 语言选择 | cookie +user.locale列;无 URL segment、无 middleware |
| 组件库策略 | packages/ui零 i18n 依赖:英文默认值 + Provider 覆盖 |
| 强制机制 | CI AST 检查器(JSX 文本/可翻译属性/toast)+ 伪语言兜底 |
| 回退策略 | 缺失 key 按字符串粒度回退英文,发布不等待翻译 |
| 性能取舍 | /转为静态外壳,其余预渲染路由保持 |
| 落地节奏 | 约十个 PR:基础设施先行,再按模块分片提取 |
对想要参与 Comp AI CRM 的开发者而言,这份文档既是一份待执行的技术蓝图,也提供了清晰的第一贡献入口;对任何正在为"Agent 优先"产品设计多语言方案的技术团队,它则是一份可迁移的决策模板——尤其是"语言是翻译工作而非重构"与"静态检查 + 伪语言双重防线"这两个判断,值得直接复用。
- 后端
- 前端
- CRM
- 人工智能
- AI Agent
【免费下载链接】crm
Comp AI CRM is an open source, CRM designed for AI agents. Agentic-first CRM.
相关推荐
Payload 多语言实战:基于 Localization 与 next-intl 搭建国际化网站
Payload 多语言实战:基于 Localization 与 next intl 搭建国际化网站 本篇以 examples/localization 示例 h
后端CMS基于 Turborepo 的 Monorepo 工程化实践:以 Comp AI CRM 的 apps/packages 架构为例
基于 Turborepo 的 Monorepo 工程化实践:以 Comp AI CRM 的 apps/packages 架构为例 本篇技术指南围绕 .agent
后端前端CRM人工智能AI Agent如何用 next-intl 把 React Email 模板改造为多语言版本
如何用 next intl 把 React Email 模板改造为多语言版本 如果你已经用 React Email 写好了一套英文邮件模板,现在需要让同一封邮件
前端UI组件
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考