Skip to content

Commit fcd9618

Browse files
baozhoutaoclaude
andcommitted
fix(service-settings): complete zh-CN coverage for auth/sms/ai settings
The auth, sms and ai manifests grew new groups and fields (breach/complexity/ history/expiry password policy, anti-abuse, MFA, extra session controls, network allow-list; the whole SMS Delivery namespace; DeepSeek/DashScope/ Cloudflare/SiliconFlow/OpenRouter providers + conversation-title options) but the zh-CN bundle wasn't updated, so those settings rendered English under a Chinese UI (objectui#2851 P1). - Fill every missing zh-CN group title + field label/help for auth, sms, ai. - Add a coverage guard test that pins zh-CN to full title/group/key coverage of every built-in settings manifest (option labels excluded — several are codes/format specimens that must not be translated). localization option labels (timezones/currencies/format specimens) remain a tracked follow-up; ja-JP / es-ES parity likewise. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent e295ad1 commit fcd9618

2 files changed

Lines changed: 188 additions & 0 deletions

File tree

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
// Copyright (c) 2026 ObjectStack. Licensed under the Apache-2.0 license.
2+
3+
/**
4+
* zh-CN settings-translation coverage guard.
5+
*
6+
* Every built-in settings manifest ships English labels inline; the console
7+
* localizes them by resolving `settings.<namespace>.…` against the shipped
8+
* translation bundles (see `useSettingsLabel`). When a manifest gains a new
9+
* group or field but the zh-CN bundle isn't updated, the setting silently
10+
* renders English under a Chinese UI (objectui#2851).
11+
*
12+
* This test pins zh-CN to full coverage of every manifest's structural
13+
* strings — namespace title/description, each group title, and each field
14+
* label. Dropdown *option* labels are intentionally excluded: several are
15+
* codes or format specimens (e.g. `date_format` = "YYYY-MM-DD",
16+
* `number_format` = "1,234.56") that must not be "translated".
17+
*/
18+
19+
import { describe, it, expect } from 'vitest';
20+
import * as manifestsModule from '../manifests/index';
21+
import { zhCN } from './index';
22+
23+
type Specifier = { type?: string; id?: string; key?: string; label?: string; description?: string };
24+
type Manifest = { namespace: string; description?: string; specifiers: Specifier[] };
25+
26+
const manifests = Object.values(manifestsModule).filter(
27+
(v): v is Manifest =>
28+
!!v && typeof v === 'object' && 'namespace' in v && Array.isArray((v as Manifest).specifiers),
29+
);
30+
31+
const settings = (zhCN as { settings?: Record<string, any> }).settings ?? {};
32+
33+
describe('zh-CN settings translation coverage', () => {
34+
it('covers every built-in settings manifest', () => {
35+
expect(manifests.length).toBeGreaterThanOrEqual(10);
36+
});
37+
38+
for (const m of manifests) {
39+
it(`covers settings.${m.namespace} (title / groups / keys)`, () => {
40+
const tr = settings[m.namespace];
41+
const missing: string[] = [];
42+
if (tr?.title == null) missing.push('title');
43+
if (m.description && tr?.description == null) missing.push('description');
44+
for (const s of m.specifiers) {
45+
if (s.type === 'group') {
46+
if (s.id && tr?.groups?.[s.id]?.title == null) missing.push(`group:${s.id}`);
47+
} else if (s.key) {
48+
if (tr?.keys?.[s.key]?.label == null) missing.push(`key:${s.key}`);
49+
}
50+
}
51+
expect(missing, `zh-CN missing for ${m.namespace}: ${missing.join(', ')}`).toEqual([]);
52+
});
53+
}
54+
});

packages/services/service-settings/src/translations/zh-CN.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,43 @@ export const zhCN: TranslationData = {
4949
},
5050
},
5151

52+
sms: {
53+
title: '短信投递',
54+
description: '用于 OTP 登录、邀请与通知的短信服务商配置。',
55+
groups: {
56+
provider: { title: '服务商', description: '选择此工作区如何发送外发短信。' },
57+
aliyun: { title: '阿里云短信' },
58+
twilio: { title: 'Twilio' },
59+
},
60+
keys: {
61+
provider: {
62+
label: '服务商',
63+
options: {
64+
log: '无(仅记录日志 — 不真实发送)',
65+
aliyun: '阿里云短信',
66+
twilio: 'Twilio',
67+
},
68+
},
69+
aliyun_access_key_id: { label: 'AccessKey ID' },
70+
aliyun_access_key_secret: { label: 'AccessKey Secret' },
71+
aliyun_sign_name: { label: '短信签名' },
72+
aliyun_template_code: {
73+
label: '默认短信模板',
74+
help: '当发送未指定模板时使用。含单个 ${content} 变量的通用模板可用于发送通用通知短信。',
75+
},
76+
twilio_account_sid: { label: 'Account SID' },
77+
twilio_auth_token: { label: 'Auth Token' },
78+
twilio_from_number: {
79+
label: '发信号码',
80+
help: 'E.164 格式的发信方,例如 +15005550006。此项与 Messaging Service SID 二选一。',
81+
},
82+
twilio_messaging_service_sid: { label: 'Messaging Service SID' },
83+
},
84+
actions: {
85+
test: { label: '发送测试短信' },
86+
},
87+
},
88+
5289
branding: {
5390
title: '品牌',
5491
description: '工作区名称、Logo 与主题色。',
@@ -126,10 +163,22 @@ export const zhCN: TranslationData = {
126163
title: '密码策略',
127164
description: '由认证提供商在注册和重置密码时强制的长度限制。',
128165
},
166+
anti_abuse: {
167+
title: '防滥用',
168+
description: '暴力破解防护:按身份的账户锁定,以及按 IP 对认证端点的限流。',
169+
},
170+
multi_factor: {
171+
title: '多因素',
172+
description: '要求成员使用身份验证器应用(TOTP)保护账户。',
173+
},
129174
sessions: {
130175
title: '会话',
131176
description: '登录会话的有效时长。',
132177
},
178+
network: {
179+
title: '网络',
180+
description: '限制用户可以从哪里登录。',
181+
},
133182
social: {
134183
title: '社交登录',
135184
description: '配置内置的 Google 登录提供商。部署环境变量仍优先生效。',
@@ -141,8 +190,68 @@ export const zhCN: TranslationData = {
141190
require_email_verification: { label: '要求邮箱验证' },
142191
password_min_length: { label: '密码最小长度' },
143192
password_max_length: { label: '密码最大长度', help: '防止超长密码哈希导致的拒绝服务。' },
193+
password_reject_breached: {
194+
label: '拒绝已泄露的密码',
195+
help: '通过 Have I Been Pwned 拦截出现在公开泄露库中的密码(k-匿名区间校验,绝不发送完整密码)。',
196+
},
197+
password_require_complexity: {
198+
label: '要求复杂密码',
199+
help: '在注册及修改/重置密码时,要求密码混合多种字符类别(大写、小写、数字、符号)。',
200+
},
201+
password_min_classes: {
202+
label: '最少字符类别数',
203+
help: '密码至少需包含四类(大写/小写/数字/符号)中的几类。',
204+
},
205+
password_history_count: {
206+
label: '密码历史(禁止重用)',
207+
help: '在修改/重置时禁止重用最近这么多个旧密码。0 表示关闭该检查。',
208+
},
209+
password_expiry_days: {
210+
label: '密码有效期(天)',
211+
help: '超过这么多天后强制修改密码。0 表示不过期。密码过期期间,用户在修改密码前将被阻止访问数据。',
212+
},
213+
lockout_threshold: {
214+
label: '账户锁定阈值',
215+
help: '连续登录失败达到此次数后锁定账户。0 表示关闭锁定。锁定期间即使密码正确也会拒绝登录。',
216+
},
217+
lockout_duration_minutes: {
218+
label: '锁定时长(分钟)',
219+
help: '越过阈值后账户保持锁定的时长。',
220+
},
221+
rate_limit_max: {
222+
label: '认证限流:最大请求数',
223+
help: '每个 IP、每个时间窗内对登录 / 注册 / 重置密码端点的最大请求数。',
224+
},
225+
rate_limit_window_seconds: {
226+
label: '认证限流:时间窗(秒)',
227+
help: '统计上述请求上限所用的滑动时间窗。',
228+
},
229+
mfa_required: {
230+
label: '要求多因素认证',
231+
help: '未注册身份验证器的用户在宽限期结束后将被阻止访问数据。启用此项也会开启两步验证功能,以便用户注册。',
232+
},
233+
mfa_grace_period_days: {
234+
label: 'MFA 宽限期(天)',
235+
help: '用户在被硬阻断前可延迟注册的时长。0 表示立即阻断。',
236+
},
144237
session_expiry_days: { label: '会话有效期(天)', help: '登录后会话在此天数后过期。' },
145238
session_refresh_days: { label: '刷新阈值(天)', help: '活跃会话在超过此时长后自动续期。' },
239+
session_idle_timeout_minutes: {
240+
label: '空闲超时(分钟)',
241+
help: '在这么多分钟无活动后将用户登出。0 表示关闭。',
242+
},
243+
session_absolute_max_hours: {
244+
label: '会话绝对有效期(小时)',
245+
help: '登录后经过这么多小时强制重新认证,与是否活跃无关。0 表示关闭。',
246+
},
247+
max_concurrent_sessions_per_user: {
248+
label: '每用户最大并发会话数',
249+
help: '限制每个用户同时在线的会话数;超出上限后最旧的会话会被登出。0 表示不限制。',
250+
},
251+
allowed_ip_ranges: {
252+
label: '允许的 IP 段',
253+
help: 'CIDR 段或精确 IP(每行一个,或用逗号分隔),例如 203.0.113.0/24。设置后,来自这些范围之外的登录将被拒绝。留空表示不限制。需要可信代理设置 X-Forwarded-For。',
254+
},
146255
google_enabled: {
147256
label: '启用 Google 登录',
148257
help: '需要在 Google Cloud Console 中创建的 Google OAuth 客户端 ID 与密钥。',
@@ -222,6 +331,12 @@ export const zhCN: TranslationData = {
222331
openai: { title: 'OpenAI' },
223332
anthropic: { title: 'Anthropic' },
224333
google: { title: 'Google' },
334+
deepseek: { title: 'DeepSeek', description: 'https://api.deepseek.com 上的 OpenAI 兼容 API,Base URL 自动填充。' },
335+
dashscope: { title: '阿里通义 DashScope', description: 'dashscope.aliyuncs.com/compatible-mode/v1 上的 OpenAI 兼容端点,Base URL 自动填充。' },
336+
cloudflare: { title: 'Cloudflare AI Gateway', description: '通过 Cloudflare AI Gateway 代理 OpenAI 兼容模型。' },
337+
siliconflow: { title: '硅基流动 SiliconFlow', description: 'api.siliconflow.cn/v1 上的 OpenAI 兼容端点,Base URL 自动填充。' },
338+
openrouter: { title: 'OpenRouter', description: 'openrouter.ai/api/v1 上的多提供商路由,Base URL 自动填充。' },
339+
titles: { title: '会话标题', description: '为新会话自动生成简短的摘要标题。' },
225340
defaults: { title: '生成默认值', description: '当 Agent 或聊天请求未指定时使用。' },
226341
observability: { title: '可观测性' },
227342
embedder: { title: 'Embedder', description: '知识库和 RAG 使用的文本→向量提供商,与上方聊天提供商相互独立。' },
@@ -235,6 +350,11 @@ export const zhCN: TranslationData = {
235350
openai: 'OpenAI',
236351
anthropic: 'Anthropic',
237352
google: 'Google Generative AI',
353+
deepseek: 'DeepSeek(OpenAI 兼容)',
354+
dashscope: '阿里通义 DashScope(OpenAI 兼容)',
355+
cloudflare: 'Cloudflare AI Gateway(OpenAI 兼容)',
356+
siliconflow: '硅基流动 SiliconFlow(OpenAI 兼容)',
357+
openrouter: 'OpenRouter(OpenAI 兼容)',
238358
},
239359
},
240360
gateway_model: { label: 'Gateway 模型', help: '作为 AI_GATEWAY_MODEL 转发。示例:openai/gpt-4o' },
@@ -246,6 +366,20 @@ export const zhCN: TranslationData = {
246366
anthropic_model: { label: '模型' },
247367
google_api_key: { label: 'Google API Key', help: '作为 GOOGLE_GENERATIVE_AI_API_KEY 转发,加密存储。' },
248368
google_model: { label: '模型' },
369+
deepseek_api_key: { label: 'DeepSeek API Key', help: 'sk-…,在 platform.deepseek.com 签发。' },
370+
deepseek_model: { label: '模型', help: '示例:deepseek-chat(V3)、deepseek-reasoner(R1 推理)。' },
371+
dashscope_api_key: { label: 'DashScope API Key', help: 'sk-…,在 dashscope.console.aliyun.com 签发。' },
372+
dashscope_model: { label: '模型' },
373+
cloudflare_account_id: { label: 'Cloudflare Account ID', help: '来自 Cloudflare 控制台 URL 的 32 位十六进制 ID。' },
374+
cloudflare_gateway_id: { label: 'Gateway ID', help: '在 Cloudflare → AI Gateway 中配置的网关名称,默认为 `default`。' },
375+
cloudflare_api_key: { label: 'Cloudflare AI Gateway Token', help: '在 AI Gateway →「API tokens」标签页签发(cfut_… 或 sk_…)。' },
376+
cloudflare_model: { label: '模型' },
377+
siliconflow_api_key: { label: 'SiliconFlow API Key' },
378+
siliconflow_model: { label: '模型', help: '示例:Qwen/Qwen2.5-72B-Instruct、deepseek-ai/DeepSeek-V3、meta-llama/Meta-Llama-3.1-8B-Instruct。' },
379+
openrouter_api_key: { label: 'OpenRouter API Key' },
380+
openrouter_model: { label: '模型', help: '格式:provider/model(例如 anthropic/claude-3.5-sonnet、deepseek/deepseek-chat)。' },
381+
title_generation_enabled: { label: '自动为会话生成摘要标题' },
382+
title_max_length: { label: '标题最大长度(字符)', help: '生成标题的硬上限,超出部分在服务端截断。' },
249383
temperature: { label: '温度', help: '0 = 确定性,2 = 高度发散。' },
250384
max_tokens: { label: '最大输出 tokens', help: '单次响应生成的硬上限。' },
251385
request_timeout_ms: { label: '请求超时(毫秒)' },

0 commit comments

Comments
 (0)