From cc35a61788f02f0ec35798bdaecffbf8f459cf19 Mon Sep 17 00:00:00 2001 From: Lan_zhijiang Date: Fri, 21 Aug 2026 19:51:13 +0800 Subject: [PATCH] fix(i18n): normalize CJK punctuation and fix tracking-widest on zh-CN group labels MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit zh-cn.ts settings.providers section used half-width punctuation (commas, semicolons, question mark, parentheses, ellipsis) inconsistent with the rest of the file. en.ts had one ... instead of …. add-flow.tsx group labels applied tracking-widest unconditionally; the 0.1em letter-spacing inflates Latin glyphs beside CJK fallback fonts, making 'API' in 'API 直连' visually read as full-width. Skip tracking-widest for zh locales via useLocale(). Add CJK typography convention to .claude/rules/frontend.md. --- .claude/rules/frontend.md | 1 + .../providers/__tests__/add-flow.test.tsx | 1 + .../src/settings/providers/add-flow.tsx | 10 +++++-- packages/presentation/i18n/src/locales/en.ts | 2 +- .../presentation/i18n/src/locales/zh-cn.ts | 30 +++++++++---------- 5 files changed, 26 insertions(+), 18 deletions(-) diff --git a/.claude/rules/frontend.md b/.claude/rules/frontend.md index 0f60e07a2..584edade6 100644 --- a/.claude/rules/frontend.md +++ b/.claude/rules/frontend.md @@ -39,6 +39,7 @@ parts of `packages/presentation/ui` (`chat`/`shell`) and `packages/client/workbe - **React Compiler: `ref={…}` takes a plain identifier only.** A member expression there (`ref={bag.setHandle}`) makes the compiler infer the whole object as a ref and reject every other render-time read of it ("Cannot access refs during render"). Destructure the ref setter into its own binding (`const { setHandle: paneRef, ...rest } = useSomething(…)`) and pass ref callbacks between components as standalone props, never on a bag object. - **Keyboard ownership.** Focus-local behavior (text editing/submission, menus, dialogs, drag-and-drop, terminal input) stays with the owning component or dependency. Commands that must work independently of focus register through `packages/presentation/ui/src/keyboard` with a ref to the surface they serve; a binding is active only while that owner is connected and outside `inert`, `aria-hidden="true"`, and Base UI's inert markers, so the DOM owns overlay precedence. Each renderer entry sets its platform synchronously before rendering; the shared provider only installs the listeners. Primary/Alt application chords use capture, while bare contextual keys and `Escape` use bubble so local handlers act first. The registry rejects IME composition/`Process`, extra modifiers, and repeats; bindings claim an event only after handling it. Complex local widgets may mark their root with `data-keyboard-shortcut-local`; the registry does not read that marker automatically, so bindings that must yield there opt in through `isKeyboardShortcutLocalTarget`. Palette/panel commands deliberately remain global, while browser history chords stay native in webview and are registered only by the desktop shell. The coss-ui `SidebarProvider` is still deliberately not mounted because its own global ⌘/Ctrl+B would duplicate the registry. - **i18n.** User-facing strings in `packages/presentation/ui`/`packages/client/workbench` go through `use-intl` (`useTranslations('workbench.…')`; dynamic keys within a namespace, like `t(kind)`, are fine). `packages/presentation/i18n/src/locales/zh-cn.ts` is the type source — add new keys there first; `en.ts` must `satisfies LocaleMessages`. + - **CJK typography in locale strings.** Chinese text uses full-width punctuation (`,` `;` `?` `。` `()` `「」` `…`) — never half-width (`,;?.()""...`) adjacent to CJK characters. Latin letters and digits within Chinese strings stay half-width, with a space at every CJK↔Latin/digit boundary (`API 直连`, not `API直连` or `API直连`). `http(s)://` and similar technical notation keep half-width parentheses. The ellipsis is always `…` (U+2026), never three dots `...` — in both `zh-cn.ts` and `en.ts`. Brand names that are entirely Latin (`xAI (Grok)`) use half-width parentheses with a preceding space, matching `en.ts`. - **Renderer ownership boundaries.** Apps own entries and platform construction; `packages/client/workbench` owns data-plane runtime; `packages/presentation/ui` owns presentation. - `apps/desktop` may read from `SystemBridge`, integrate native chrome/window behavior, and construct desktop transport. Pass system values down as props; do not keep shared UI in desktop for a single IPC-derived value. - `apps/webview` may construct browser transport and browser entry/root only. It must not host shared providers or desktop-consumed shells. diff --git a/packages/client/workbench/src/settings/providers/__tests__/add-flow.test.tsx b/packages/client/workbench/src/settings/providers/__tests__/add-flow.test.tsx index a410973fc..dca3f8202 100644 --- a/packages/client/workbench/src/settings/providers/__tests__/add-flow.test.tsx +++ b/packages/client/workbench/src/settings/providers/__tests__/add-flow.test.tsx @@ -12,6 +12,7 @@ function translateKey(key: string): string { } vi.mock('use-intl', () => ({ + useLocale: () => 'en', useTranslations: () => translateKey, })); diff --git a/packages/client/workbench/src/settings/providers/add-flow.tsx b/packages/client/workbench/src/settings/providers/add-flow.tsx index 44534d63f..b83f57c8b 100644 --- a/packages/client/workbench/src/settings/providers/add-flow.tsx +++ b/packages/client/workbench/src/settings/providers/add-flow.tsx @@ -34,7 +34,7 @@ import { ChevronLeftIcon } from 'lucide-react'; import { useState } from 'react'; import type { Control, FieldValues, Path } from 'react-hook-form'; import { Controller, useForm } from 'react-hook-form'; -import { useTranslations } from 'use-intl'; +import { useLocale, useTranslations } from 'use-intl'; import { z } from 'zod'; import type { AgentRuntimeOnboarding } from '../../agent-runtime/onboarding'; import type { ModelSources } from './model-selection'; @@ -137,11 +137,17 @@ export function ServiceCatalogView({ linkCodeGatewayAvailable?: boolean; }): React.ReactNode { const t = useTranslations('settings.providers'); + const locale = useLocale(); + // tracking-widest (0.1em) inflates Latin glyphs beside CJK, making e.g. "API" in + // "API 直连" read as full-width. CJK glyphs carry natural sidebearings, so skip it. + const groupLabelTracking = locale.startsWith('zh') ? '' : ' tracking-widest'; return (
{GROUPS.map((group) => (
- + {t(`group.${group}`)}
diff --git a/packages/presentation/i18n/src/locales/en.ts b/packages/presentation/i18n/src/locales/en.ts index 8351ea6f9..8c8892b67 100644 --- a/packages/presentation/i18n/src/locales/en.ts +++ b/packages/presentation/i18n/src/locales/en.ts @@ -783,7 +783,7 @@ export const en = { settings: { title: 'Settings', back: 'Back', - searchPlaceholder: 'Search settings...', + searchPlaceholder: 'Search settings…', searchNoResults: 'No matching settings.', groups: { personal: 'Personal', diff --git a/packages/presentation/i18n/src/locales/zh-cn.ts b/packages/presentation/i18n/src/locales/zh-cn.ts index 82660b602..db465992b 100644 --- a/packages/presentation/i18n/src/locales/zh-cn.ts +++ b/packages/presentation/i18n/src/locales/zh-cn.ts @@ -768,7 +768,7 @@ export const zhCN = { settings: { title: '设置', back: '返回', - searchPlaceholder: '搜索设置...', + searchPlaceholder: '搜索设置…', searchNoResults: '没有匹配的设置。', groups: { personal: '个人', @@ -1002,7 +1002,7 @@ export const zhCN = { }, agents: { title: '智能体', - hint: '启用状态与运行时;账号与模型的绑定在 Providers 页管理。', + hint: '启用状态与运行时;账号与模型的绑定在 Providers 页管理。', enabled: '启用', followCli: '默认 · 跟随 CLI 登录', translated: '经转换', @@ -1018,7 +1018,7 @@ export const zhCN = { }, providers: { title: '账号与 Provider', - hint: '把订阅、AI 网关或自定义端点接入你的智能体;一个账号可接入多个智能体,每个智能体同一时刻使用一个账号。', + hint: '把订阅、AI 网关或自定义端点接入你的智能体;一个账号可接入多个智能体,每个智能体同一时刻使用一个账号。', searchPlaceholder: '搜索账号…', addAccount: '添加账号', orderHint: '拖动账号调整优先级;新建任务默认使用首个兼容账号的首个模型。', @@ -1073,11 +1073,11 @@ export const zhCN = { translateNote: '本地网关将 Anthropic 协议转为 OpenAI Chat', unavailableOauth: '仅可接入 {agent}', unavailableProtocol: '端点协议与此智能体不兼容', - unavailableEndpointIncomplete: '端点信息不完整,请补全账号设置', + unavailableEndpointIncomplete: '端点信息不完整,请补全账号设置', configPreview: 'config.json 片段 · 此账号写入的内容', configPreviewEmpty: '// 尚未接入任何智能体', remove: '移除账号', - removeTitle: '移除「{label}」?', + removeTitle: '移除「{label}」?', removeInUse: '{agents} 将回退为跟随 CLI 登录。', removeHint: '此账号尚未接入任何智能体。', cancel: '取消', @@ -1093,19 +1093,19 @@ export const zhCN = { 'chatgpt-sub': 'ChatGPT 订阅', 'anthropic-api': 'Anthropic API', 'openai-api': 'OpenAI API', - xai: 'xAI(Grok)', + xai: 'xAI (Grok)', deepseek: 'DeepSeek', stepfun: '阶跃星辰 StepFun', openrouter: 'OpenRouter', 'vercel-gateway': 'Vercel AI Gateway', 'cloudflare-gateway': 'Cloudflare AI Gateway', 'linkcode-gateway': 'LinkCode Gateway', - 'cloudflare-anthropic': 'Cloudflare AI Gateway(Anthropic)', + 'cloudflare-anthropic': 'Cloudflare AI Gateway (Anthropic)', custom: '自定义端点', }, serviceHint: { - 'claude-sub': 'Claude Code 登录,Pro / Max 计划', - 'chatgpt-sub': 'Codex 登录,Plus / Pro 计划', + 'claude-sub': 'Claude Code 登录,Pro / Max 计划', + 'chatgpt-sub': 'Codex 登录,Plus / Pro 计划', 'anthropic-api': 'console.anthropic.com 的密钥', 'openai-api': 'platform.openai.com 的密钥', xai: 'console.x.ai 的密钥', @@ -1114,20 +1114,20 @@ export const zhCN = { 'linkcode-gateway': '无需自带 API 密钥,直接使用 LinkCode 额度', openrouter: '聚合网关', 'vercel-gateway': '服务端翻译网关', - 'cloudflare-gateway': '兼容网关,需 Account / Gateway ID', - 'cloudflare-anthropic': '透传网关,需 Anthropic 密钥与 Account / Gateway ID', + 'cloudflare-gateway': '兼容网关,需 Account / Gateway ID', + 'cloudflare-anthropic': '透传网关,需 Anthropic 密钥与 Account / Gateway ID', custom: '任意 Base URL + 协议', }, - oauthEditHint: '订阅凭证跟随 CLI 登录;这里只能修改账号名称。', + oauthEditHint: '订阅凭证跟随 CLI 登录;这里只能修改账号名称。', form: { label: '名称', - model: '默认模型(可选)', + model: '默认模型(可选)', submit: '添加账号', save: '保存更改', credentialType: '类型', secret: '密钥 / 令牌', - baseUrl: '端点 URL(可选)', - protocol: '协议(可选)', + baseUrl: '端点 URL(可选)', + protocol: '协议(可选)', protocolNone: '默认', }, },