Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions frontend/src/i18n/__tests__/zhNumberUnitSpacing.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import fs from 'fs';
import path from 'path';

// TASK-179, lily-shen's ruling (2026-09-27): zh writes a number and its unit
// together — 「24小时」, 「{{n}}天前」 — which is also what the browser's own zh-CN
// Intl.RelativeTimeFormat and NumberFormat produce (ux-lead's 75076). The catalog
// was 31 values the other way, so the convention gets a guard rather than a
// reviewer's eye: this fails on the next value that reintroduces a space.
//
// The detector is deliberately narrow. It is a number — literal digits or a
// {{placeholder}} — followed by spaces and then a unit. A space after a
// placeholder that is followed by an ordinary word stays (「{{count}} 个智能体」):
// that is the placeholder boundary, not the number↔unit one.

const UNITS = ['分钟', '小时', '个月', '天', '周', '月', '年', '秒'];
const AMOUNT = String.raw`(?:\d+|\{\{[a-zA-Z]+\}\})`;
const spacedAmount = new RegExp(`${AMOUNT} +(?=(?:${UNITS.join('|')}))`);
const attachedAmount = new RegExp(`${AMOUNT}(?=(?:${UNITS.join('|')}))`);

type Tree = { [key: string]: string | Tree };

const flatten = (tree: Tree, prefix = ''): Array<[string, string]> => Object.entries(tree).flatMap(([key, value]) => {
const fullKey = prefix ? `${prefix}.${key}` : key;
return typeof value === 'string' ? [[fullKey, value] as [string, string]] : flatten(value, fullKey);
});

const zhValues = flatten(JSON.parse(
fs.readFileSync(path.join(__dirname, '..', 'locales', 'zh-CN.json'), 'utf8'),
));

describe('zh-CN number and unit spacing', () => {
it('never separates a number from its unit', () => {
expect(zhValues.filter(([, value]) => spacedAmount.test(value)).map(([key]) => key)).toEqual([]);
});

it('is not green because the detector matches nothing', () => {
// Vacuity control. The same detector has to be able to fire — on a
// hand-written spaced pair and on the attached pairs this catalog does
// contain — or the test above would pass just as well if the regex were
// blind, which is the failure it cannot otherwise report (TASK-164's
// null==null pass was this shape).
expect(spacedAmount.test('24 小时')).toBe(true);
expect(spacedAmount.test('24小时')).toBe(false);
expect(attachedAmount.test('24小时')).toBe(true);
expect(zhValues.filter(([, value]) => attachedAmount.test(value)).length).toBeGreaterThan(0);
});
});
16 changes: 9 additions & 7 deletions frontend/src/i18n/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@
},
"firstRun": {
"eyebrow": "Your first five minutes",
"title": "Bring your agent into the room",
"title": "Bring your agent into your pod",
"lede": "Connect Claude Code, Cursor, or Codex, then start a private conversation without leaving your workspace.",
"skip": "Skip for now",
"reopen": "Guide",
Expand Down Expand Up @@ -422,7 +422,7 @@
"goneDark": "{{agentName}} hasn't checked in recently. A reply may take longer.",
"unknown": "{{agentName}}'s availability is unknown. A reply may take longer.",
"waiting": "Waiting for {{agentName}} to reply…",
"timedOut": "{{agentName}} hasn't replied yet. Your message is still in this room."
"timedOut": "{{agentName}} hasn't replied yet. Your message is still in this conversation."
},
"typing": {
"one": "{{name}} is thinking…",
Expand Down Expand Up @@ -785,7 +785,7 @@
},
"browse": {
"title": "Browse before you join",
"text": "Discover rooms and the agents in them before you commit to the conversation."
"text": "Discover pods and the agents in them before you commit to the conversation."
},
"marketplace": {
"title": "Install agents & apps",
Expand Down Expand Up @@ -1434,7 +1434,7 @@
"usTitle": "What we do differently",
"closeTitle": "The honest version",
"close": "Want a hosted product and don't mind a closed coordination layer? Raft is good, and shipping. Already committed to Nostr, or want Block's distribution behind you? Buzz is excellent. Tracking work as issues rather than conversation? Multica is the better shape.",
"closeUs": "Want agents from different vendors to be first-class members of one room, on infrastructure you can own outright, with no per-agent tax? That's Commonly.",
"closeUs": "Want agents from different vendors to be first-class members of one pod, on infrastructure you can own outright, with no per-agent tax? That's Commonly.",
"note": "Comparison reflects each product's public positioning and published documents at time of writing. Product names are trademarks of their respective owners; this page is not affiliated with or endorsed by any of them.",
"nav": {
"primaryLabel": "Primary",
Expand Down Expand Up @@ -2027,7 +2027,6 @@
"budget": "budget",
"budgetTotal": "{{calls}} calls",
"budgetWindow": "{{calls}} calls per {{window}}",
"by": "by",
"calls": "calls",
"cancel": "Cancel",
"changeAccess": "Change access",
Expand All @@ -2053,7 +2052,8 @@
"grantedByOn": "Granted by {{member}} {{rel}}.",
"grantedCount": "{{count}} granted",
"grantedOn": "Granted {{rel}}.",
"grantedTo": "granted to",
"grantedToPod": "granted to <b>{{pod}}</b>",
"grantedToPodBy": "granted to <b>{{pod}}</b> by <b>{{member}}</b>",
"granting": "Granting…",
"installGitHubApp": "Install GitHub App",
"installationId": "installation ID",
Expand All @@ -2071,6 +2071,7 @@
"noSeats": "no agent in this pod yet",
"noTools": "no tools on the allow-list",
"nobody": "no agent",
"nobodyMayUse": "nobody may use it",
"notEnabled": "not enabled on this instance · ask your operator",
"notGranted": "not granted",
"nothingMatches": "Nothing matches.",
Expand Down Expand Up @@ -2099,7 +2100,8 @@
"trailEmpty": "No calls yet.",
"trailError": "Could not read the trail.",
"viewGrant": "View {{tool}} in {{pod}}",
"listSeparator": ", "
"listSeparator": ", ",
"sentenceSeparator": " "
},
"time": {
"age": {
Expand Down
74 changes: 38 additions & 36 deletions frontend/src/i18n/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
"windowAriaLabel": "动态时间范围",
"windows": {
"today": "今天",
"7d": "7 天"
"7d": "7天"
},
"podScopeLabel": "Pod 范围",
"allPods": "所有 Pod",
Expand Down Expand Up @@ -316,7 +316,7 @@
},
"firstRun": {
"eyebrow": "最初的五分钟",
"title": "把你的智能体请进房间",
"title": "把你的智能体请进你的 Pod",
"lede": "接入 Claude Code、Cursor 或 Codex,然后直接在工作空间中开始一对一私信。",
"skip": "暂时跳过",
"reopen": "指南",
Expand Down Expand Up @@ -357,9 +357,9 @@
"expiresAfter": "有效期",
"expiryLabel": "邀请有效期",
"never": "永不过期",
"hours24": "24 小时",
"days7": "7 天",
"days30": "30 天",
"hours24": "24小时",
"days7": "7天",
"days30": "30天",
"maximumUses": "最多使用次数",
"maximumUsesLabel": "邀请最多使用次数",
"unlimited": "不限次数",
Expand All @@ -385,12 +385,12 @@
"time": {
"recently": "最近",
"justNow": "刚刚",
"daysAgo_one": "{{formattedCount}} 天前",
"daysAgo_other": "{{formattedCount}} 天前",
"hoursAgo_one": "{{formattedCount}} 小时前",
"hoursAgo_other": "{{formattedCount}} 小时前",
"minutesAgo_one": "{{formattedCount}} 分钟前",
"minutesAgo_other": "{{formattedCount}} 分钟前"
"daysAgo_one": "{{formattedCount}}天前",
"daysAgo_other": "{{formattedCount}}天前",
"hoursAgo_one": "{{formattedCount}}小时前",
"hoursAgo_other": "{{formattedCount}}小时前",
"minutesAgo_one": "{{formattedCount}}分钟前",
"minutesAgo_other": "{{formattedCount}}分钟前"
},
"expiry": {
"neverExpires": "永不过期",
Expand Down Expand Up @@ -555,9 +555,9 @@
"catchup": {
"title": "回顾",
"justNow": "刚刚",
"minutesAgo": "{{count}} 分钟前",
"hoursAgo": "{{count}} 小时前",
"daysAgo": "{{count}} 天前",
"minutesAgo": "{{count}}分钟前",
"hoursAgo": "{{count}}小时前",
"daysAgo": "{{count}}天前",
"empty": "还没有摘要",
"summarize": "生成摘要",
"refresh": "刷新",
Expand Down Expand Up @@ -818,7 +818,7 @@
"sub": "各有专长的 agent 和一位独立创始人一起做出了这个项目——每个 agent 都有自己的记忆和工位。每个由 agent 提交的 PR 都有标注;{{count}} 份架构决策记录写清了缘由。",
"stats": {
"agents": "智能体",
"messages": "过去 24 小时消息",
"messages": "过去 24小时消息",
"pods": "活跃 Pod"
}
},
Expand All @@ -845,7 +845,7 @@
"items": {
"unlimited": "不限数量的自带 agent —— 跑在你自己的机器上,免费接入",
"private": "私密 Pod 与邀请制 Pod",
"history": "30 天消息历史",
"history": "30天消息历史",
"connect": "通过 webhook、CLI wrapper 或 MCP 接入",
"card": "无需信用卡"
}
Expand All @@ -857,7 +857,7 @@
"note": "固定价格。无论你跑多少个 agent —— 我们绝不按 agent 收费。",
"items": {
"everything": "包含 Cloud free 的全部功能",
"history": "你创建的 Pod 内消息历史无限 —— 不再有 30 天过期",
"history": "你创建的 Pod 内消息历史无限 —— 不再有 30天过期",
"community": "把你的 Pod 收录进「社区」,方便团队找到",
"hosted": "10 个托管 agent 席位 —— 我们的算力,零配置(即将推出,上线即包含)",
"support": "优先支持"
Expand Down Expand Up @@ -900,8 +900,8 @@
"closed": "已关闭"
},
"lifecycle": {
"joinedDays_one": "加入 {{count}} 天",
"joinedDays_other": "加入 {{count}} 天",
"joinedDays_one": "加入 {{count}}天",
"joinedDays_other": "加入 {{count}}天",
"joinedUnknown": "加入 —",
"pods_one": "{{count}} 个 Pod",
"pods_other": "{{count}} 个 Pod",
Expand Down Expand Up @@ -1289,7 +1289,7 @@
"usTitle": "我们不一样的地方",
"closeTitle": "实话实说",
"close": "想要托管产品、也不介意协调层是闭源的?Raft 很好,而且已经在跑。已经选定 Nostr,或希望有 Block 的分发能力支撑?Buzz 非常出色。习惯用 issue 而不是对话来跟踪工作?Multica 的形态更合适。",
"closeUs": "希望来自不同厂商的智能体,在同一个房间里都是一等公民,跑在你完全拥有的基础设施上,且不按智能体收费?那就是 Commonly。",
"closeUs": "希望来自不同厂商的智能体,在同一个 Pod 里都是一等公民,跑在你完全拥有的基础设施上,且不按智能体收费?那就是 Commonly。",
"note": "本对比基于各产品在撰写时的公开定位与公开文档。各产品名称归其各自所有者所有;本页面与它们无关联,也未获其背书。",
"nav": {
"primaryLabel": "主导航",
Expand Down Expand Up @@ -1350,7 +1350,7 @@
"agentByo": {
"eyebrow": "连接任意运行时",
"title": "接入你自己的智能体",
"description": "用 MCP 把 Claude Code、Cursor 或 Codex 接入你的 Commonly Pod,约 2 分钟。",
"description": "用 MCP 把 Claude Code、Cursor 或 Codex 接入你的 Commonly Pod,约 2分钟。",
"form": {
"nameLabel": "智能体名称",
"nameHint": "小写字母、数字和连字符。这是你的智能体发帖时使用的身份。",
Expand Down Expand Up @@ -1436,7 +1436,7 @@
"byoHint": "从你的电脑连接 Claude Code、Cursor 或 Codex。",
"recommended": "推荐",
"yours": "你的电脑",
"hostedMeta": "约 10 秒上线——无需终端与密钥",
"hostedMeta": "约 10秒上线——无需终端与密钥",
"byoMeta": "MCP 令牌 + 编辑器代码片段",
"machine": "在我的电脑上",
"machineHint": "在你已添加的电脑上运行——你的文件、你的 CLI",
Expand Down Expand Up @@ -1563,7 +1563,7 @@
"title": "使用情况",
"subtitle": "来自本实例自有数据库的第一方数据 — 不会发送给任何第三方。",
"timeRangeAria": "时间范围",
"rangeDays": "{{days}} 天",
"rangeDays": "{{days}}天",
"userAdminLink": "用户管理 →",
"loading": "正在加载分析数据…",
"errors": {
Expand All @@ -1572,18 +1572,18 @@
"cards": {
"dau": {
"label": "今日活跃",
"hint": "过去 24 小时内活跃的人类用户"
"hint": "过去 24小时内活跃的人类用户"
},
"wau": {
"label": "本周活跃",
"hint": "过去 7 天内活跃的人类用户"
"hint": "过去 7天内活跃的人类用户"
},
"signups": {
"label": "注册数({{days}} 天)",
"label": "注册数({{days}}天)",
"hint": "该时间范围内新增的人类账户"
},
"messages": {
"label": "消息数({{days}} 天)",
"label": "消息数({{days}}天)",
"hint": "所有消息,含智能体"
},
"total": {
Expand All @@ -1603,7 +1603,7 @@
},
"funnel": {
"title": "激活漏斗",
"summary": "在过去 {{days}} 天的 {{signups}} 个注册中:{{attachRate}}% 挂载了智能体 · {{messageRate}}% 发送了消息 · {{d1Rate}}% 在第 1 天后回访 · {{d7Rate}}% 在第 7 天后回访。",
"summary": "在过去 {{days}}天的 {{signups}} 个注册中:{{attachRate}}% 挂载了智能体 · {{messageRate}}% 发送了消息 · {{d1Rate}}% 在第 1天后回访 · {{d7Rate}}% 在第 7天后回访。",
"table": {
"cohort": "群组",
"signups": "注册数",
Expand Down Expand Up @@ -1783,9 +1783,9 @@
"activity": {
"noRecent": "近期无活动",
"justNow": "刚刚",
"minutesAgo": "{{count}} 分钟前",
"hoursAgo": "{{count}} 小时前",
"daysAgo": "{{count}} 天前",
"minutesAgo": "{{count}}分钟前",
"hoursAgo": "{{count}}小时前",
"daysAgo": "{{count}}天前",
"neverConnected": "从未连接"
},
"runtime": {
Expand Down Expand Up @@ -1826,7 +1826,7 @@
"free": "免费版",
"pro": "Pro · $12/月"
},
"freeNote": "自带 agent 不限数量,消息历史保留 30 天。升级后,你创建的 Pod 内历史记录无限保留,并可将 Pod 收录进「社区」。",
"freeNote": "自带 agent 不限数量,消息历史保留 30天。升级后,你创建的 Pod 内历史记录无限保留,并可将 Pod 收录进「社区」。",
"proNote": "你创建的 Pod 内历史记录无限保留、社区收录,以及托管 agent 席位(上线即包含)。",
"cancelling": "你的方案将在本计费周期结束时取消。在此之前仍可使用 Pro。",
"upgrade": "升级到 Pro —— $12/月",
Expand All @@ -1848,7 +1848,7 @@
"enableHint": "打开与 Commonly 机器人的私聊",
"enableHintSend": ",然后发送:",
"waitingTelegram": "在 Telegram 聊天中发送 /commonly-enable 命令。",
"codeExpires": "代码将在 {{minutes}} 分钟后过期",
"codeExpires": "代码将在 {{minutes}}分钟后过期",
"liveRelay": "中继",
"liveRelayHint": "聊天消息会发布到 Pod 并唤醒被提及的智能体;智能体的升级消息会回到频道。",
"newTitle": "连接频道",
Expand Down Expand Up @@ -2019,7 +2019,6 @@
"budget": "额度",
"budgetTotal": "{{calls}} 次调用",
"budgetWindow": "每 {{window}}可调用 {{calls}} 次",
"by": "授权者",
"calls": "调用",
"cancel": "取消",
"changeAccess": "更改访问权限",
Expand All @@ -2045,7 +2044,8 @@
"grantedByOn": "{{rel}}由 {{member}} 授权。",
"grantedCount": "已授权 {{count}} 个",
"grantedOn": "{{rel}}授权。",
"grantedTo": "授权给",
"grantedToPod": "授权给 <b>{{pod}}</b>",
"grantedToPodBy": "由 <b>{{member}}</b> 授权给 <b>{{pod}}</b>",
"granting": "授权中…",
"installGitHubApp": "安装 GitHub App",
"installationId": "安装 ID",
Expand All @@ -2063,6 +2063,7 @@
"noSeats": "此 Pod 中还没有智能体",
"noTools": "允许列表中没有工具",
"nobody": "没有智能体",
"nobodyMayUse": "没有智能体可以使用",
"notEnabled": "此实例尚未启用 · 联系实例管理员",
"notGranted": "未授权",
"nothingMatches": "没有匹配项。",
Expand Down Expand Up @@ -2091,7 +2092,8 @@
"trailEmpty": "还没有调用。",
"trailError": "无法读取调用记录。",
"viewGrant": "在 {{pod}} 中查看 {{tool}}",
"listSeparator": "、"
"listSeparator": "、",
"sentenceSeparator": ""
},
"time": {
"age": {
Expand Down
Loading
Loading