diff --git a/client/html/index.html b/client/html/index.html
index b58070ad..7da33734 100644
--- a/client/html/index.html
+++ b/client/html/index.html
@@ -3,7 +3,7 @@
-
{schema.ui?.label || path}
- {schema.description && (
-
{schema.description}
+
+ {configurationLabelZhCN(path, schema.ui?.label || path)}
+
+ {configurationDescriptionZhCN(path, schema.description) && (
+
+ {configurationDescriptionZhCN(path, schema.description)}
+
)}
@@ -88,24 +100,17 @@ function Field({ path, schema }: { path: string; schema: any }) {
/>
);
}
-function fieldMatches(path: string, schema: any, query: string): boolean {
- if (!query) return true;
- const label = schema.ui?.label || path;
- const description = schema.description || "";
- return `${label} ${description} ${path}`.toLowerCase().includes(query);
-}
-
function Category({
name,
fields,
@@ -116,12 +121,17 @@ function Category({
query: string;
}) {
const { cfg } = useCfg();
- const visible = fields.filter((f) => fieldMatches(f.path, f.schema, query));
+ const visible = fields.filter((f) =>
+ configurationFieldMatches(f.path, f.schema, query),
+ );
if (visible.length === 0) return null;
- const description = cfg.categories?.[name]?.description;
+ const description = configurationCategoryDescriptionZhCN(
+ name,
+ cfg.categories?.[name]?.description,
+ );
return (
-
{name}
+
{configurationCategoryZhCN(name)}
{description &&
{description}
}
{visible.map((f) => (
@@ -144,7 +154,7 @@ export function ConfigurationTab() {
inputRef={inputRef}
class="cfg-search"
type="text"
- placeholder="Filter configuration options..."
+ placeholder="搜索设置名称、说明或配置路径…"
value={search}
onInput={(e) => setSearch((e.currentTarget as HTMLInputElement).value)}
/>
diff --git a/plugs/configuration-manager/ui/config_zh_cn.test.ts b/plugs/configuration-manager/ui/config_zh_cn.test.ts
new file mode 100644
index 00000000..50833158
--- /dev/null
+++ b/plugs/configuration-manager/ui/config_zh_cn.test.ts
@@ -0,0 +1,66 @@
+import { describe, expect, test } from "vitest";
+import {
+ configurationCategoryDescriptionZhCN,
+ configurationCategoryZhCN,
+ configurationDescriptionZhCN,
+ configurationFieldMatches,
+ configurationLabelZhCN,
+ configurationOptionZhCN,
+} from "./config_zh_cn.ts";
+
+describe("configuration Chinese display names", () => {
+ test("translates built-in categories and fields", () => {
+ expect(configurationCategoryZhCN("Editor")).toBe("编辑器");
+ expect(configurationCategoryDescriptionZhCN("Journal")).toContain(
+ "每日笔记",
+ );
+ expect(
+ configurationLabelZhCN("journal.prefix", "Journal page prefix"),
+ ).toBe("日记保存位置");
+ expect(
+ configurationDescriptionZhCN(
+ "sync.documents",
+ "Sync document files locally",
+ ),
+ ).toContain("离线");
+ expect(
+ configurationOptionZhCN("frontmatterFolding.foldByDefault", "always"),
+ ).toBe("总是");
+ });
+
+ test("preserves configuration supplied by third-party extensions", () => {
+ expect(configurationCategoryZhCN("My Extension")).toBe("My Extension");
+ expect(configurationLabelZhCN("my.setting", "Custom setting")).toBe(
+ "Custom setting",
+ );
+ expect(
+ configurationDescriptionZhCN("my.setting", "Custom description"),
+ ).toBe("Custom description");
+ expect(configurationOptionZhCN("my.setting", "custom-value")).toBe(
+ "custom-value",
+ );
+ });
+});
+
+describe("configuration search", () => {
+ const schema = {
+ description: "Page name to use as the template for new journal entries",
+ ui: { label: "Journal template page" },
+ };
+
+ test.each([
+ "日记模板",
+ "journal template",
+ "journal.template",
+ ])("matches %s without changing the underlying path", (query) => {
+ expect(configurationFieldMatches("journal.template", schema, query)).toBe(
+ true,
+ );
+ });
+
+ test("does not match unrelated text", () => {
+ expect(
+ configurationFieldMatches("journal.template", schema, "GitHub"),
+ ).toBe(false);
+ });
+});
diff --git a/plugs/configuration-manager/ui/config_zh_cn.ts b/plugs/configuration-manager/ui/config_zh_cn.ts
new file mode 100644
index 00000000..6f4b2fa1
--- /dev/null
+++ b/plugs/configuration-manager/ui/config_zh_cn.ts
@@ -0,0 +1,208 @@
+type LocalizedConfiguration = {
+ label: string;
+ description?: string;
+ options?: Record
;
+};
+
+const categories: Record = {
+ Editor: {
+ label: "编辑器",
+ description: "设置括号补全、双链显示和页面属性折叠等编辑体验。",
+ },
+ "Smart Quotes": {
+ label: "智能引号",
+ description: "输入时自动替换直引号,并设置使用的引号字符。",
+ },
+ Comments: {
+ label: "批注",
+ description: "设置行内批注的署名信息。",
+ },
+ Sync: {
+ label: "同步",
+ description: "设置当前设备与服务器之间同步的内容。",
+ },
+ Indexing: {
+ label: "索引",
+ description: "选择哪些段落、列表项和待办可以被查询。",
+ },
+ Query: {
+ label: "查询",
+ description: "设置查询结果中的文本比较和排序规则。",
+ },
+ Widgets: {
+ label: "页面组件",
+ description: "开启并设置目录、关联提及和关联待办等内置组件。",
+ },
+ Journal: {
+ label: "日记",
+ description: "设置每日笔记的模板、保存位置和标签。",
+ },
+ GitHub: {
+ label: "GitHub 集成",
+ description: "设置写入 GitHub 仓库和 Gist 时使用的身份与凭据。",
+ },
+};
+
+const fields: Record = {
+ "sync.documents": {
+ label: "同步附件文件",
+ description: "将非 Markdown 文件同步到本机,离线时也能访问附件。",
+ },
+ "index.paragraph.all": {
+ label: "索引所有段落",
+ description: "没有标签的普通段落也加入索引。",
+ },
+ "index.item.all": {
+ label: "索引所有列表项",
+ description: "没有标签的列表项也加入索引。",
+ },
+ "index.task.all": {
+ label: "索引所有待办",
+ description: "没有标签的待办也加入索引。",
+ },
+ shortWikiLinks: {
+ label: "简化双链名称",
+ description: "双链只显示最后一段名称,例如“人物/小明”显示为“小明”。",
+ },
+ "frontmatterFolding.foldByDefault": {
+ label: "自动折叠页面属性",
+ description: "打开笔记时,何时自动折叠顶部的 YAML 页面属性。",
+ options: {
+ never: "从不",
+ long: "内容较长时",
+ always: "总是",
+ },
+ },
+ "frontmatterFolding.foldByDefaultLines": {
+ label: "页面属性折叠行数",
+ description: "页面属性超过此行数时自动折叠,仅在“内容较长时”生效。",
+ },
+ "smartQuotes.enabled": {
+ label: "启用智能引号",
+ description: "输入时自动将直引号替换为排版引号。",
+ },
+ "smartQuotes.double.left": {
+ label: "左双引号",
+ description: "左侧双引号使用的字符。",
+ },
+ "smartQuotes.double.right": {
+ label: "右双引号",
+ description: "右侧双引号使用的字符。",
+ },
+ "smartQuotes.single.left": {
+ label: "左单引号",
+ description: "左侧单引号使用的字符。",
+ },
+ "smartQuotes.single.right": {
+ label: "右单引号",
+ description: "右侧单引号使用的字符。",
+ },
+ "comments.author": {
+ label: "批注署名",
+ description: "填写批注签名;留空时只记录日期。",
+ },
+ "queryCollation.enabled": {
+ label: "启用本地化排序",
+ description: "按照语言规则比较文本,而不是只按字符编码排序。",
+ },
+ "queryCollation.locale": {
+ label: "排序语言",
+ description: "用于文本排序的语言标签,例如 zh-CN。",
+ },
+ "std.widgets.toc.enabled": {
+ label: "显示页面目录",
+ description: "在页面顶部显示目录。",
+ },
+ "std.widgets.toc.minHeaders": {
+ label: "显示目录所需标题数",
+ description: "页面至少包含多少个标题时才显示目录。",
+ },
+ "std.widgets.linkedMentions.enabled": {
+ label: "显示关联提及",
+ description: "在页面底部显示其他笔记中对当前页面的引用。",
+ },
+ "std.widgets.linkedTasks.enabled": {
+ label: "显示关联待办",
+ description: "在页面顶部显示与当前页面相关的未完成待办。",
+ },
+ "journal.enabled": {
+ label: "启用日记",
+ description: "启用内置的每日日记功能和相关操作。",
+ },
+ "journal.template": {
+ label: "日记模板页面",
+ description: "新建日记时使用的模板页面。",
+ },
+ "journal.prefix": {
+ label: "日记保存位置",
+ description: "日记页面的路径前缀,例如 Journal/。以 / 结尾可归入文件夹。",
+ },
+ "journal.tag": {
+ label: "日记标签",
+ description: "用于识别日记页面的标签。",
+ },
+ "github.name": {
+ label: "Git 提交者姓名",
+ description: "写入 GitHub 仓库时使用的提交者姓名。",
+ },
+ "github.email": {
+ label: "Git 提交者邮箱",
+ description: "写入 GitHub 仓库时使用的提交者邮箱。",
+ },
+ "github.token": {
+ label: "GitHub 访问令牌",
+ description: "访问 GitHub API 所需的个人令牌。",
+ },
+};
+
+export function configurationCategoryZhCN(name: string): string {
+ return categories[name]?.label ?? name;
+}
+
+export function configurationCategoryDescriptionZhCN(
+ name: string,
+ fallback?: string,
+): string | undefined {
+ return categories[name]?.description ?? fallback;
+}
+
+export function configurationLabelZhCN(path: string, fallback: string): string {
+ return fields[path]?.label ?? fallback;
+}
+
+export function configurationDescriptionZhCN(
+ path: string,
+ fallback?: string,
+): string | undefined {
+ return fields[path]?.description ?? fallback;
+}
+
+export function configurationOptionZhCN(path: string, option: string): string {
+ return fields[path]?.options?.[option] ?? option;
+}
+
+export function configurationFieldMatches(
+ path: string,
+ schema: any,
+ query: string,
+): boolean {
+ if (!query) return true;
+ const originalLabel = schema.ui?.label || path;
+ const originalDescription = schema.description || "";
+ const localizedLabel = configurationLabelZhCN(path, originalLabel);
+ const localizedDescription = configurationDescriptionZhCN(
+ path,
+ originalDescription,
+ );
+ return [
+ path,
+ originalLabel,
+ originalDescription,
+ localizedLabel,
+ localizedDescription,
+ ]
+ .filter(Boolean)
+ .join(" ")
+ .toLocaleLowerCase()
+ .includes(query.toLocaleLowerCase());
+}
diff --git a/plugs/configuration-manager/ui/configuration.scss b/plugs/configuration-manager/ui/configuration.scss
index 1eea3a91..849fbdec 100644
--- a/plugs/configuration-manager/ui/configuration.scss
+++ b/plugs/configuration-manager/ui/configuration.scss
@@ -24,7 +24,7 @@ html[data-theme="dark"] {
--cfg-recording-bg: #3a3520;
}
-html, body { height: 100%; background: var(--cfg-bg); color: var(--cfg-color); }
+html, body { width: 100%; height: 100%; overflow: hidden; background: var(--cfg-bg); color: var(--cfg-color); }
#cfg-root {
display: flex; flex-direction: column; height: 100%;
@@ -293,9 +293,16 @@ html[data-theme="dark"] .lib-badge-pull { background: #1f2a40; color: #89a0e5; }
@media only screen and (max-width: 600px) {
#cfg-header { padding: 12px 14px 0; }
- #cfg-content { padding: 14px; }
+ #cfg-content {
+ min-width: 0;
+ padding: 14px;
+ overflow-x: hidden;
+ overscroll-behavior: contain;
+ -webkit-overflow-scrolling: touch;
+ }
#cfg-footer { padding: 10px 14px; flex-wrap: wrap; gap: 8px; }
#cfg-close { top: 8px; right: 8px; }
+ #cfg-edit-config { display: none; }
.cfg-field {
flex-direction: column;
@@ -303,6 +310,7 @@ html[data-theme="dark"] .lib-badge-pull { background: #1f2a40; color: #89a0e5; }
gap: 8px;
}
.cfg-field-control {
+ min-width: 0;
margin-left: 0;
justify-content: flex-end;
}
@@ -311,6 +319,7 @@ html[data-theme="dark"] .lib-badge-pull { background: #1f2a40; color: #89a0e5; }
.cfg-field-control select {
flex: 1;
min-width: 0;
+ max-width: 100%;
}
.lib-row { flex-direction: column; align-items: stretch; }