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 (