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
26 changes: 26 additions & 0 deletions desktop/src/App.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1977,6 +1977,32 @@ describe("desktop command center", () => {
.map((button) => button.textContent),
).toEqual(["通用", "模型", "插件", "智能体预设"]);
expect(screen.getByRole("button", { name: "打开配置文件" })).toBeTruthy();
expect(
within(dialog).getByRole("group", { name: "外观模式" }),
).toBeTruthy();
expect(
within(dialog).getByRole("button", { name: "浅色" }),
).toBeTruthy();
expect(
within(dialog).getByRole("button", { name: "深色" }),
).toBeTruthy();
expect(
within(dialog).getByRole("button", { name: "跟随系统" }),
).toBeTruthy();
expect(within(dialog).getByLabelText(/^对话宽度/)).toBeTruthy();
expect(within(dialog).getByLabelText(/^字号/)).toBeTruthy();
expect(within(dialog).getByLabelText("首选字体")).toBeTruthy();
expect(
within(dialog).getByRole("option", { name: "纸张 · 暖色低蓝光" }),
).toBeTruthy();
expect(
within(dialog).getByText(
"这些显示设置仅保存在本机,会立即生效,不属于项目配置。",
),
).toBeTruthy();
expect(
within(dialog).getByRole("button", { name: "恢复默认设置" }),
).toBeTruthy();

// Switching back restores English for the remaining tests' queries.
fireEvent.change(
Expand Down
20 changes: 20 additions & 0 deletions desktop/src/app/i18n.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,26 @@ const ZH_CN: Record<string, string> = {
"settings.appearance.light": "浅色",
"settings.appearance.dark": "深色",
"settings.appearance.system": "跟随系统",
"settings.appearance.mode": "外观模式",
"settings.appearance.theme": "主题",
"settings.appearance.conversationWidth": "对话宽度",
"settings.appearance.fontSize": "字号",
"settings.appearance.fontFamily": "首选字体",
"settings.appearance.paper": "纸张 · 暖色低蓝光",
"settings.appearance.midnight": "午夜 · 深邃冷色",
"settings.appearance.claude": "Claude · 象牙白与陶土色",
"settings.appearance.claudeDark": "Claude 深色 · 石板灰与陶土色",
"settings.appearance.contrast": "高对比度 · AAA",
"settings.appearance.fontPlaceholder": "例如:更纱黑体 SC、Inter",
"settings.appearance.addInstalledFont": "添加已安装字体…",
"settings.appearance.fontGroup.interface": "界面字体",
"settings.appearance.fontGroup.monospace": "等宽字体",
"settings.appearance.fontGroup.cjk": "中日韩字体",
"settings.appearance.fontDescription":
"以逗号分隔的字体会优先于内置字体尝试,便于为中英文混排选择一致的字体。系统会跳过未安装的字体,因此可以安全地列出多个字体。",
"settings.appearance.localOnly":
"这些显示设置仅保存在本机,会立即生效,不属于项目配置。",
"settings.appearance.reset": "恢复默认设置",
"settings.language.title": "语言",
"settings.language.eyebrow": "界面语言",
"settings.language.label": "界面语言",
Expand Down
80 changes: 62 additions & 18 deletions desktop/src/features/settings/AppearanceSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,29 @@ const THEME_LABELS: Record<ThemePreference, string> = {
contrast: "High contrast — AAA",
};

function themeTranslationKey(preference: ThemePreference): string {
const suffix = preference === "claude-dark" ? "claudeDark" : preference;
return `settings.appearance.${suffix}`;
}

const FONT_GROUPS = [
{
value: "Interface",
key: "settings.appearance.fontGroup.interface",
defaultValue: "Interface",
},
{
value: "Monospace",
key: "settings.appearance.fontGroup.monospace",
defaultValue: "Monospace",
},
{
value: "CJK",
key: "settings.appearance.fontGroup.cjk",
defaultValue: "CJK",
},
] as const;

/**
* Appearance controls, rendered from `APPEARANCE_SETTINGS`.
*
Expand Down Expand Up @@ -77,7 +100,7 @@ export function AppearanceSettings() {
<div
className={modeStyles.modeRow}
role="group"
aria-label="Appearance mode"
aria-label={t("settings.appearance.mode", "Appearance mode")}
>
{MODE_CARDS.map((mode) => {
const Icon = mode.icon;
Expand All @@ -90,7 +113,7 @@ export function AppearanceSettings() {
onClick={() => set("theme", mode.value)}
>
<Icon size={18} />
{mode.label}
{t(themeTranslationKey(mode.value), mode.label)}
</button>
);
})}
Expand All @@ -100,11 +123,12 @@ export function AppearanceSettings() {
{APPEARANCE_SETTINGS.map((setting) => {
const id = `${fieldId}-${setting.key}`;
const value = appearance[setting.key];
const label = t(`settings.appearance.${setting.key}`, setting.label);

if (setting.key === "theme") {
return (
<label key={setting.key} htmlFor={id}>
{setting.label}
{label}
<select
id={id}
value={value as ThemePreference}
Expand All @@ -114,7 +138,10 @@ export function AppearanceSettings() {
>
{THEME_PREFERENCES.map((preference) => (
<option key={preference} value={preference}>
{THEME_LABELS[preference]}
{t(
themeTranslationKey(preference),
THEME_LABELS[preference],
)}
</option>
))}
</select>
Expand All @@ -126,7 +153,7 @@ export function AppearanceSettings() {
const { min, max, step, unit } = setting.range;
return (
<label key={setting.key} htmlFor={id}>
{setting.label}
{label}
<span className={styles.note}>
{value}
{unit}
Expand All @@ -151,19 +178,25 @@ export function AppearanceSettings() {

return (
<label key={setting.key} htmlFor={id}>
{setting.label}
{label}
<input
id={id}
type="text"
value={value as string}
placeholder="e.g. Sarasa Mono SC, Inter"
placeholder={t(
"settings.appearance.fontPlaceholder",
"e.g. Sarasa Mono SC, Inter",
)}
onChange={(event) =>
set(setting.key as "fontFamily", event.target.value)
}
/>
{installed.length > 0 ? (
<select
aria-label="Add an installed font"
aria-label={t(
"settings.appearance.addInstalledFont",
"Add an installed font",
)}
value=""
onChange={(event) => {
if (!event.target.value) return;
Expand All @@ -173,14 +206,22 @@ export function AppearanceSettings() {
);
}}
>
<option value="">Add an installed font…</option>
{["Interface", "Monospace", "CJK"].map((group) => {
<option value="">
{t(
"settings.appearance.addInstalledFont",
"Add an installed font…",
)}
</option>
{FONT_GROUPS.map((group) => {
const members = installed.filter(
(candidate) => candidate.group === group,
(candidate) => candidate.group === group.value,
);
if (members.length === 0) return null;
return (
<optgroup key={group} label={group}>
<optgroup
key={group.value}
label={t(group.key, group.defaultValue)}
>
{members.map((candidate) => (
<option
key={candidate.family}
Expand All @@ -200,24 +241,27 @@ export function AppearanceSettings() {
</div>

<p className={styles.note}>
{
{t(
"settings.appearance.fontDescription",
APPEARANCE_SETTINGS.find((setting) => setting.key === "fontFamily")
?.description
}
?.description ?? "Choose fonts for the interface.",
)}
</p>

<footer className={styles.formActions}>
<span>
These are per-machine display settings. They apply immediately and
are not part of project configuration.
{t(
"settings.appearance.localOnly",
"These are per-machine display settings. They apply immediately and are not part of project configuration.",
)}
</span>
<button
className={styles.primaryButton}
type="button"
disabled={isDefault}
onClick={reset}
>
Reset to defaults
{t("settings.appearance.reset", "Reset to defaults")}
</button>
</footer>
</section>
Expand Down
Loading