hd-garden 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/garden-ui/SKILL.md +2 -2
- package/.env.en.example +67 -0
- package/README.en.md +2 -2
- package/README.md +2 -2
- package/app/a2/[slug]/page.tsx +4 -3
- package/app/a2/page.tsx +4 -3
- package/app/about/page.tsx +8 -9
- package/app/admin/page.tsx +7 -6
- package/app/admin/preview/[...path]/page.tsx +2 -1
- package/app/admin/ui/blocks/page.tsx +2 -1
- package/app/admin/ui/catalog/page.tsx +2 -1
- package/app/admin/ui/evals/page.tsx +2 -1
- package/app/admin/ui/page.tsx +2 -1
- package/app/blog/[...path]/page.tsx +7 -6
- package/app/blog/page.tsx +4 -2
- package/app/columns/[column]/[...path]/page.tsx +8 -7
- package/app/columns/page.tsx +12 -8
- package/app/components/AdminUiShell.tsx +4 -3
- package/app/components/ArticleMeasure.tsx +6 -3
- package/app/components/ArticleReferences.tsx +4 -1
- package/app/components/ArticleTags.tsx +3 -2
- package/app/components/BlogArchive.tsx +20 -16
- package/app/components/Comments.tsx +16 -10
- package/app/components/ContentEmptyState.tsx +29 -28
- package/app/components/ExternalEmbed.tsx +15 -14
- package/app/components/GardenContentGuide.tsx +10 -2
- package/app/components/GardenDocsNav.tsx +4 -2
- package/app/components/GiscusComments.tsx +2 -1
- package/app/components/GithubLiveOverview.tsx +10 -8
- package/app/components/GithubProfile.tsx +31 -21
- package/app/components/HandbookLangSwitch.tsx +8 -4
- package/app/components/HandbookView.tsx +92 -9
- package/app/components/MarkdownArticle.tsx +31 -7
- package/app/components/MermaidDiagram.tsx +4 -3
- package/app/components/SiteChrome.tsx +3 -3
- package/app/components/SiteFooter.tsx +1 -1
- package/app/components/SiteHeader.tsx +13 -4
- package/app/components/ThoughtCard.tsx +16 -14
- package/app/components/UiBlock.tsx +3 -2
- package/app/components/Webmentions.tsx +11 -11
- package/app/components/WidgetChart.tsx +8 -6
- package/app/components/garden/FilePreview.tsx +2 -2
- package/app/components/garden/GardenBlock.tsx +7 -0
- package/app/components/garden/GardenFile.tsx +2 -2
- package/app/components/garden/RefDemo.tsx +62 -0
- package/app/components/garden/blocks.tsx +56 -8
- package/app/components/garden/patterns.tsx +497 -0
- package/app/garden/[...slug]/page.tsx +3 -2
- package/app/garden/en/page.tsx +7 -7
- package/app/garden/page.tsx +3 -2
- package/app/globals.css +49 -1
- package/app/lib/a2-meta.ts +26 -3
- package/app/lib/content.ts +2 -2
- package/app/lib/garden-docs-meta.ts +24 -0
- package/app/lib/github-id.mjs +8 -0
- package/app/lib/handbook-switch.mjs +12 -0
- package/app/lib/i18n.ts +401 -2
- package/app/lib/rehype-field-tables.mjs +47 -0
- package/app/lib/remark-a2.mjs +18 -8
- package/app/lib/remark-link-card.mjs +22 -0
- package/app/lib/remark-ref-demos.mjs +42 -0
- package/app/lib/tag-filter.ts +9 -2
- package/app/not-found.tsx +32 -4
- package/app/page.tsx +17 -14
- package/app/rss.xml/route.ts +2 -1
- package/app/styles/a2.css +338 -0
- package/app/styles/shell.css +15 -2
- package/app/tags/page.tsx +7 -5
- package/app/thoughts/[slug]/page.tsx +2 -1
- package/app/thoughts/page/[n]/page.tsx +1 -1
- package/app/thoughts/page.tsx +2 -1
- package/app/ui-parity/page.tsx +38 -22
- package/bin/garden.mjs +129 -19
- package/docs/handbook/ai-writing.md +2 -2
- package/docs/handbook/assets/reading-layout-en.svg +26 -0
- package/docs/handbook/components/card.md +43 -22
- package/docs/handbook/components/chart.md +10 -26
- package/docs/handbook/components/choice.md +42 -0
- package/docs/handbook/components/compare.md +32 -17
- package/docs/handbook/components/figure.md +33 -46
- package/docs/handbook/components/file.md +10 -15
- package/docs/handbook/components/html.md +10 -30
- package/docs/handbook/components/interactive.md +9 -35
- package/docs/handbook/components/list.md +65 -0
- package/docs/handbook/components/relations.md +95 -0
- package/docs/handbook/components/stat.md +10 -28
- package/docs/handbook/components/timeline.md +13 -28
- package/docs/handbook/components.md +210 -19
- package/docs/handbook/config.md +5 -3
- package/docs/handbook/deploy.md +1 -1
- package/docs/handbook/design.md +20 -12
- package/docs/handbook/en/ai-writing.md +2 -2
- package/docs/handbook/en/components/card.md +42 -21
- package/docs/handbook/en/components/chart.md +10 -26
- package/docs/handbook/en/components/choice.md +42 -0
- package/docs/handbook/en/components/compare.md +30 -15
- package/docs/handbook/en/components/figure.md +29 -42
- package/docs/handbook/en/components/file.md +9 -14
- package/docs/handbook/en/components/html.md +9 -29
- package/docs/handbook/en/components/interactive.md +8 -34
- package/docs/handbook/en/components/list.md +65 -0
- package/docs/handbook/en/components/relations.md +95 -0
- package/docs/handbook/en/components/stat.md +9 -27
- package/docs/handbook/en/components/timeline.md +9 -24
- package/docs/handbook/en/components.md +211 -19
- package/docs/handbook/en/config.md +32 -30
- package/docs/handbook/en/deploy.md +12 -12
- package/docs/handbook/en/design.md +19 -12
- package/docs/handbook/en/index.md +2 -2
- package/docs/handbook/en/reference/card.md +85 -0
- package/docs/handbook/en/reference/chart.md +68 -0
- package/docs/handbook/en/reference/choice.md +69 -0
- package/docs/handbook/en/reference/compare.md +59 -0
- package/docs/handbook/en/reference/figure.md +84 -0
- package/docs/handbook/en/reference/file/checklist.md +17 -0
- package/docs/handbook/en/reference/file.md +50 -0
- package/docs/handbook/en/reference/html.md +62 -0
- package/docs/handbook/en/reference/interactive.md +71 -0
- package/docs/handbook/en/reference/list.md +72 -0
- package/docs/handbook/en/reference/relations.md +113 -0
- package/docs/handbook/en/reference/stat.md +60 -0
- package/docs/handbook/en/reference/timeline.md +56 -0
- package/docs/handbook/en/reference.md +26 -0
- package/docs/handbook/en/start.md +2 -2
- package/docs/handbook/en/workflow.md +6 -5
- package/docs/handbook/en/writing.md +3 -2
- package/docs/handbook/index.md +1 -1
- package/docs/handbook/reference/card.md +85 -0
- package/docs/handbook/reference/chart.md +68 -0
- package/docs/handbook/reference/choice.md +69 -0
- package/docs/handbook/reference/compare.md +59 -0
- package/docs/handbook/reference/figure.md +84 -0
- package/docs/handbook/reference/file/checklist.md +17 -0
- package/docs/handbook/reference/file.md +50 -0
- package/docs/handbook/reference/html.md +62 -0
- package/docs/handbook/reference/interactive.md +71 -0
- package/docs/handbook/reference/list.md +72 -0
- package/docs/handbook/reference/relations.md +113 -0
- package/docs/handbook/reference/stat.md +60 -0
- package/docs/handbook/reference/timeline.md +56 -0
- package/docs/handbook/reference.md +26 -0
- package/docs/handbook/start.md +1 -1
- package/docs/handbook/workflow.md +6 -5
- package/docs/handbook/writing.md +3 -2
- package/next.config.ts +6 -0
- package/package.json +2 -1
- package/public/images/garden/ref-card-book.svg +1 -0
- package/public/images/garden/ref-card-lamp.svg +1 -0
- package/scripts/build-content.mjs +37 -16
- package/scripts/favicon-ico.mjs +9 -6
- package/scripts/file-preview.mjs +10 -7
- package/scripts/handbook-allowlist.mjs +18 -0
- package/scripts/handbook-facts.mjs +305 -11
- package/scripts/link-assets.mjs +7 -0
- package/scripts/new-post.mjs +18 -9
- package/scripts/prepare-pages-output.mjs +30 -18
- package/scripts/redirects.mjs +17 -6
- package/templates/garden-blocks.md +67 -0
- package/templates/garden-snippets.plist +61 -0
- package/templates/site/Makefile.en +45 -0
- package/templates/site/README.en.md +15 -0
- package/templates/site/site.config.en.yaml +55 -0
- package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
- package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
- package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
- package/templates/skills-en/garden-post-review/SKILL.md +63 -0
- package/templates/skills-en/garden-ui/SKILL.md +118 -0
- package/templates/workflows/pages.en.yml +195 -0
- package/templates/workflows/pages.yml +195 -0
- package/ui/garden/catalog.mjs +205 -4
- package/ui/garden/check.mjs +6 -0
- package/ui/garden/file-block.mjs +8 -7
- package/ui/garden/html-lang.mjs +73 -1
- package/ui/garden/markdown-links.mjs +7 -4
- package/ui/garden/parse.mjs +45 -0
- package/ui/garden/relations.mjs +252 -0
- package/ui/garden/search-index.mjs +6 -4
- package/ui/garden/source-icon.mjs +54 -0
- package/ui/parity-fixtures.mjs +77 -0
- package/ui/preview/dist/garden-ui.esm.js +41 -4
- package/ui/preview/dist/garden-ui.iife.js +41 -4
- package/ui/prompt/rules.md +28 -1
- package/ui/react/components/chart.tsx +1 -1
- package/ui/react/components/display.tsx +1 -1
- package/ui/react/lang.tsx +4 -0
- package/ui/react/parity.tsx +8 -6
- package/worker/index.ts +8 -2
package/app/lib/a2-meta.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
const shanghaiOffsetMs = 8 * 60 * 60 * 1000;
|
|
2
|
+
const EN_MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] as const;
|
|
3
|
+
|
|
4
|
+
export type DateLang = "zh" | "en";
|
|
2
5
|
|
|
3
6
|
function shanghaiDate(value: string) {
|
|
4
7
|
const date = new Date(value);
|
|
@@ -6,23 +9,43 @@ function shanghaiDate(value: string) {
|
|
|
6
9
|
return new Date(date.getTime() + shanghaiOffsetMs);
|
|
7
10
|
}
|
|
8
11
|
|
|
9
|
-
/** Calendar date in Asia/Shanghai,
|
|
10
|
-
export function demoDateLabel(value: string) {
|
|
12
|
+
/** Calendar date in Asia/Shanghai, formatted for the active site language. */
|
|
13
|
+
export function demoDateLabel(value: string, lang: DateLang = "zh") {
|
|
11
14
|
const date = shanghaiDate(value);
|
|
12
15
|
if (!date) return value;
|
|
16
|
+
if (lang === "en") {
|
|
17
|
+
return `${EN_MONTHS[date.getUTCMonth()]} ${date.getUTCDate()}, ${date.getUTCFullYear()}`;
|
|
18
|
+
}
|
|
13
19
|
return `${date.getUTCFullYear()} 年 ${date.getUTCMonth() + 1} 月 ${date.getUTCDate()} 日`;
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
/** Home list label. The current Shanghai year drops the year so the column stays narrow. */
|
|
17
|
-
export function homeListDate(value: string, now = new Date()) {
|
|
23
|
+
export function homeListDate(value: string, now = new Date(), lang: "zh" | "en" = "zh") {
|
|
18
24
|
const date = shanghaiDate(value);
|
|
19
25
|
if (!date) return value;
|
|
20
26
|
const today = shanghaiDate(now.toISOString());
|
|
27
|
+
const sameYear = Boolean(today && date.getUTCFullYear() === today.getUTCFullYear());
|
|
28
|
+
if (lang === "en") {
|
|
29
|
+
return new Intl.DateTimeFormat("en-US", {
|
|
30
|
+
timeZone: "UTC",
|
|
31
|
+
month: "short",
|
|
32
|
+
day: "numeric",
|
|
33
|
+
...(sameYear ? {} : { year: "numeric" }),
|
|
34
|
+
}).format(date);
|
|
35
|
+
}
|
|
21
36
|
const monthDay = `${date.getUTCMonth() + 1} 月 ${date.getUTCDate()} 日`;
|
|
22
37
|
if (today && date.getUTCFullYear() === today.getUTCFullYear()) return monthDay;
|
|
23
38
|
return `${date.getUTCFullYear()} 年 ${monthDay}`;
|
|
24
39
|
}
|
|
25
40
|
|
|
41
|
+
/** Full list date: 「2025 年 7 月 21 日」 or "Jul 21, 2025". */
|
|
42
|
+
export function fullListDate(value: string, lang: "zh" | "en" = "zh") {
|
|
43
|
+
if (lang !== "en") return demoDateLabel(value);
|
|
44
|
+
const date = shanghaiDate(value);
|
|
45
|
+
if (!date) return value;
|
|
46
|
+
return new Intl.DateTimeFormat("en-US", { timeZone: "UTC", year: "numeric", month: "short", day: "numeric" }).format(date);
|
|
47
|
+
}
|
|
48
|
+
|
|
26
49
|
export function demoDateTime(value: string) {
|
|
27
50
|
const date = shanghaiDate(value);
|
|
28
51
|
if (!date) return value.slice(0, 10);
|
package/app/lib/content.ts
CHANGED
|
@@ -21,7 +21,7 @@ import { unified } from "unified";
|
|
|
21
21
|
import { visit } from "unist-util-visit";
|
|
22
22
|
import { articleUsesHeadingNumbers, autoNumbersHeadings, splitHeadingNumber } from "./heading-number";
|
|
23
23
|
import { siteHref } from "./site-href";
|
|
24
|
-
import { ui } from "./i18n";
|
|
24
|
+
import { siteLang, ui } from "./i18n";
|
|
25
25
|
|
|
26
26
|
export { columns, gardenDocs, gardenDocsEn, gardenIndexDoc, gardenIndexDocEn, posts, thoughts };
|
|
27
27
|
export type { ContentEntry, GardenDocEntry, MediaItem };
|
|
@@ -44,7 +44,7 @@ export const columnIndexHref = (slug: string) => siteHref(`/columns/#${slug}`);
|
|
|
44
44
|
export const thoughtHref = (slug: string) => siteHref(`/thoughts/${slug}`);
|
|
45
45
|
|
|
46
46
|
export const formatDate = (value: string, withTime = false) => {
|
|
47
|
-
if (!withTime) return demoDateLabel(value);
|
|
47
|
+
if (!withTime) return demoDateLabel(value, siteLang);
|
|
48
48
|
const canonical = canonicalSiteDate(value) ?? value;
|
|
49
49
|
const date = new Date(canonical);
|
|
50
50
|
if (Number.isNaN(date.getTime())) return value;
|
|
@@ -70,8 +70,13 @@ export const HANDBOOK_SECTIONS: HandbookSectionMeta[] = [
|
|
|
70
70
|
{ id: "writing-and-reading", title: "写作与排版", titleEn: "Writing & Layout" },
|
|
71
71
|
{ id: "components", title: "内置组件", titleEn: "Built-in Components" },
|
|
72
72
|
{ id: "workflow", title: "检查与补全", titleEn: "Checks & Shortcuts" },
|
|
73
|
+
{ id: "reference", title: "组件参考", titleEn: "Component reference" },
|
|
73
74
|
];
|
|
74
75
|
|
|
76
|
+
/** Component reference pages: one per component, all fields and error codes. */
|
|
77
|
+
export const REFERENCE_SECTION = "reference";
|
|
78
|
+
export const isReferencePath = (docPath: string) => docPath === REFERENCE_SECTION || docPath.startsWith(`${REFERENCE_SECTION}/`);
|
|
79
|
+
|
|
75
80
|
export const COMPONENT_BADGE: Record<string, string> = {
|
|
76
81
|
"components/card": "card",
|
|
77
82
|
"components/stat": "stat",
|
|
@@ -82,6 +87,21 @@ export const COMPONENT_BADGE: Record<string, string> = {
|
|
|
82
87
|
"components/html": "html",
|
|
83
88
|
"components/interactive": "interactive",
|
|
84
89
|
"components/file": "file",
|
|
90
|
+
"components/relations": "relations",
|
|
91
|
+
"components/list": "list",
|
|
92
|
+
"components/choice": "choice",
|
|
93
|
+
"reference/card": "card",
|
|
94
|
+
"reference/stat": "stat",
|
|
95
|
+
"reference/compare": "compare",
|
|
96
|
+
"reference/timeline": "timeline",
|
|
97
|
+
"reference/chart": "chart",
|
|
98
|
+
"reference/figure": "figure",
|
|
99
|
+
"reference/html": "html",
|
|
100
|
+
"reference/interactive": "interactive",
|
|
101
|
+
"reference/file": "file",
|
|
102
|
+
"reference/relations": "relations",
|
|
103
|
+
"reference/list": "list",
|
|
104
|
+
"reference/choice": "choice",
|
|
85
105
|
};
|
|
86
106
|
|
|
87
107
|
export const NAV_SHORT_TITLE: Record<string, string> = {
|
|
@@ -101,7 +121,11 @@ export const NAV_SHORT_TITLE: Record<string, string> = {
|
|
|
101
121
|
"components/html": "嵌入网页片段",
|
|
102
122
|
"components/interactive": "可调公式与参数",
|
|
103
123
|
"components/file": "文件下载与预览",
|
|
124
|
+
"components/relations": "关系图",
|
|
125
|
+
"components/list": "图标列表",
|
|
126
|
+
"components/choice": "选择卡",
|
|
104
127
|
workflow: "语法检查与快捷输入",
|
|
128
|
+
reference: "总览",
|
|
105
129
|
};
|
|
106
130
|
|
|
107
131
|
export function groupHandbookDocs<T extends GardenDocNavItem>(docs: T[]) {
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The template placeholder written by `garden init`; not a real GitHub account. */
|
|
2
|
+
export const GITHUB_ID_PLACEHOLDER = "your-github-id";
|
|
3
|
+
|
|
4
|
+
/** True when the configured GitHub id is a real account worth calling the GitHub API for. */
|
|
5
|
+
export function hasRealGithubId(id) {
|
|
6
|
+
const value = String(id ?? "").trim();
|
|
7
|
+
return value !== "" && value.toLowerCase() !== GITHUB_ID_PLACEHOLDER;
|
|
8
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** Handbook page language from its path: /garden/en/... is English, the rest of /garden/ is Chinese. */
|
|
2
|
+
export const handbookPageLang = (pathname) => (/\/garden\/en(\/|$)/u.test(String(pathname ?? "")) ? "en" : "zh");
|
|
3
|
+
|
|
4
|
+
/** The 中 / EN switch always offers the other language: Chinese pages offer English, English pages offer Chinese. */
|
|
5
|
+
export const handbookSwitchTarget = (pageLang) => (pageLang === "en" ? "zh" : "en");
|
|
6
|
+
|
|
7
|
+
/** aria-label of the live link. */
|
|
8
|
+
export const handbookSwitchLabel = (pageLang) => (handbookSwitchTarget(pageLang) === "en" ? "Read in English" : "Switch to Chinese");
|
|
9
|
+
|
|
10
|
+
/** Tooltip while there is no counterpart link (built HTML, before hydration). */
|
|
11
|
+
export const handbookSwitchTitle = (pageLang, siteLang) =>
|
|
12
|
+
pageLang === "en" ? "No Chinese version of this page yet" : siteLang === "en" ? "No English version of this page yet" : "这一页还没有英文版";
|
package/app/lib/i18n.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
import { siteLinks } from "@garden/site-config";
|
|
1
2
|
import { siteConfig } from "../site.config";
|
|
3
|
+
import {
|
|
4
|
+
inAppPath as resolveInAppPath,
|
|
5
|
+
isHandbookPath as resolveIsHandbookPath,
|
|
6
|
+
} from "../../ui/garden/html-lang.mjs";
|
|
2
7
|
|
|
3
8
|
/**
|
|
4
9
|
* Framework chrome strings. The site language is chosen at build time with
|
|
@@ -7,12 +12,30 @@ import { siteConfig } from "../site.config";
|
|
|
7
12
|
* their own language explicitly.
|
|
8
13
|
*/
|
|
9
14
|
export type UiLang = "zh" | "en";
|
|
15
|
+
export type SiteLang = UiLang;
|
|
10
16
|
|
|
11
17
|
export const langOf = (locale: string | undefined): UiLang => (/^en/i.test(locale ?? "") ? "en" : "zh");
|
|
12
18
|
export const siteLang: UiLang = langOf(siteConfig.locale);
|
|
13
19
|
export const htmlLang = (lang: UiLang) => (lang === "en" ? "en" : "zh-CN");
|
|
14
20
|
|
|
15
21
|
const zh = {
|
|
22
|
+
homeAbout: "关于我",
|
|
23
|
+
homeRss: "RSS 订阅",
|
|
24
|
+
homeLatest: "最新文章",
|
|
25
|
+
homeRecent: "最近更新",
|
|
26
|
+
homeAllPosts: (n: number) => `全部 ${n} 篇`,
|
|
27
|
+
homePinned: "置顶",
|
|
28
|
+
homeColumns: "专栏",
|
|
29
|
+
homeAllColumns: "全部专栏",
|
|
30
|
+
columnCount: (n: number, status: string) => `${n} 篇,${status}`,
|
|
31
|
+
columnDefault: "持续更新",
|
|
32
|
+
columnOngoing: "连载中",
|
|
33
|
+
columnCompleted: "已完结",
|
|
34
|
+
columnReadingList: (n: number, status: string) => `阅读清单,${n} 篇,${status}`,
|
|
35
|
+
columnSideCount: (n: number, status: string) => `${n} 篇 · ${status}`,
|
|
36
|
+
columnCoverAlt: (title: string | undefined) => `${title} 封面`,
|
|
37
|
+
columnFallbackDesc: "按顺序组织的专题文章系列。",
|
|
38
|
+
columnNewestFirst: (title: string | undefined) => `${title},从新到旧`,
|
|
16
39
|
toc: "本篇目录",
|
|
17
40
|
tocShort: "目录",
|
|
18
41
|
tocPage: "本页目录",
|
|
@@ -57,6 +80,8 @@ const zh = {
|
|
|
57
80
|
fullscreen: "全屏",
|
|
58
81
|
exitFullscreen: "退出全屏",
|
|
59
82
|
chart: "图表",
|
|
83
|
+
relations: "关系图",
|
|
84
|
+
relationsLegend: "连线说明",
|
|
60
85
|
dataTable: "查看数据表",
|
|
61
86
|
item: "项目",
|
|
62
87
|
value: "数值",
|
|
@@ -107,9 +132,193 @@ const zh = {
|
|
|
107
132
|
escToClose: "关闭",
|
|
108
133
|
noResults: (q: string) => (q ? `没有找到和「${q}」相关的文章` : "还没有可以搜索的内容"),
|
|
109
134
|
navLabels: { "/blog/": "文章", "/thoughts/": "随想", "/columns/": "专栏", "/tags/": "标签", "/about/": "关于" } as Record<string, string>,
|
|
135
|
+
// Home page
|
|
136
|
+
homeLatestBadge: "最新文章",
|
|
137
|
+
homeRecentHeading: "最近更新",
|
|
138
|
+
homeAllPostsCount: (n: number) => `全部 ${n} 篇`,
|
|
139
|
+
homePinnedHeading: "置顶",
|
|
140
|
+
homeColumnsHeading: "专栏",
|
|
141
|
+
homeColumnCountStatus: (count: number, status: string) => `${count} 篇,${status}`,
|
|
142
|
+
columnDefaultStatus: "持续更新",
|
|
143
|
+
// Blog archive
|
|
144
|
+
blogHeading: "文章",
|
|
145
|
+
blogDeck: (n: number) => `阶段性的完整沉淀,关于工程、架构与技术思考。共 ${n} 篇,按年份归档。`,
|
|
146
|
+
unknownYear: "未知",
|
|
147
|
+
tagFilterLabel: "按标签筛选",
|
|
148
|
+
tagCollapse: "收起",
|
|
149
|
+
tagExpandAll: (n: number) => `展开全部 ${n} 个标签`,
|
|
150
|
+
tagClear: "清除",
|
|
151
|
+
densityLabel: "列表密度",
|
|
152
|
+
densityDetailed: "摘要",
|
|
153
|
+
densityCompact: "紧凑",
|
|
154
|
+
yearPostCount: (n: number) => `${n} 篇`,
|
|
155
|
+
pinnedBadge: "置顶",
|
|
156
|
+
tagEmptyMatch: "没有同时带这些标签的文章。",
|
|
157
|
+
tagDropLast: (tag: string) => `去掉「${tag}」`,
|
|
158
|
+
browseAllPosts: "看全部文章",
|
|
159
|
+
railJumpByYear: "按年份跳转",
|
|
160
|
+
railByYear: "按年份",
|
|
161
|
+
railColumns: "专栏",
|
|
162
|
+
// Post & column entry pages
|
|
163
|
+
permalinkLabel: (title: string) => `${title} 永久链接`,
|
|
164
|
+
updatedOn: (date: string) => `更新于 ${date}`,
|
|
165
|
+
originalLink: "原文 ↗",
|
|
166
|
+
coverLabel: (title: string) => `${title} 封面`,
|
|
167
|
+
referencesHeading: "参考",
|
|
168
|
+
articleTagsLabel: "标签",
|
|
169
|
+
allColumnsBack: "所有专栏",
|
|
170
|
+
seriesEyebrow: "主题专栏",
|
|
171
|
+
seriesCountStatus: (count: number, status: string) => `${count} 篇 · ${status}`,
|
|
172
|
+
// Columns index
|
|
173
|
+
columnsFallbackTitle: "专栏",
|
|
174
|
+
columnsMeta: (count: number, status: string) => `阅读清单,${count} 篇,${status}`,
|
|
175
|
+
columnsFallbackDesc: "按顺序组织的专题文章系列。",
|
|
176
|
+
columnsOrderLabel: (title: string) => `${title},从新到旧`,
|
|
177
|
+
// Tags page
|
|
178
|
+
tagsTitle: "标签",
|
|
179
|
+
tagsDescription: "全部标签。",
|
|
180
|
+
tagsDeck: (n: number) => `${n} 个标签,按文章数排序。点一个标签,查看带这个标签的文章。`,
|
|
181
|
+
tagsIndexLabel: "全部标签",
|
|
182
|
+
tagsEmpty: "还没有标签。",
|
|
183
|
+
// Thoughts
|
|
184
|
+
thoughtsEyebrow: "THOUGHTS / 随想",
|
|
185
|
+
backToThoughts: "返回随想",
|
|
186
|
+
thoughtMixedLabel: (n: number) => `混合记录 · ${n} 个附件`,
|
|
187
|
+
thoughtImageLabel: "一张照片",
|
|
188
|
+
thoughtAudioLabel: "一段声音",
|
|
189
|
+
thoughtVideoLabel: "一段影像",
|
|
190
|
+
thoughtLinkLabel: "一个链接",
|
|
191
|
+
thoughtTextLabel: "随想",
|
|
192
|
+
thoughtGalleryLabel: (n: number) => `${n} 张照片`,
|
|
193
|
+
thoughtPhotoAlt: (title: string, idx: number) => `${title} · 照片 ${idx}`,
|
|
194
|
+
thoughtNowPlaying: (title: string) => `当前播放 · ${title}`,
|
|
195
|
+
audioFallback: "你的浏览器不支持音频播放。",
|
|
196
|
+
thoughtPermalinkShort: "永久链接",
|
|
197
|
+
// About & GitHub profile
|
|
198
|
+
aboutEyebrow: "关于",
|
|
199
|
+
aboutFallbackHeading: "关于",
|
|
200
|
+
aboutFallbackParagraphs: [
|
|
201
|
+
"一名工程师。平时写代码,也把想清楚的问题记下来。",
|
|
202
|
+
"这里放文章、随想和专栏。",
|
|
203
|
+
] as string[],
|
|
204
|
+
aboutFocusHeading: "现在关注",
|
|
205
|
+
aboutFallbackFocus: ["软件", "笔记", "日常"] as string[],
|
|
206
|
+
aboutFallbackCta: "浏览博文",
|
|
207
|
+
githubProfileAria: "GitHub 资料",
|
|
208
|
+
githubAvatarAlt: (login: string) => `${login} 的 GitHub 头像`,
|
|
209
|
+
githubOverviewPrefix: "GitHub 公开资料",
|
|
210
|
+
githubSyncSynced: "本次访问已同步",
|
|
211
|
+
githubSyncSnapshot: "暂用发布时快照",
|
|
212
|
+
githubSyncSyncing: "正在同步当前资料",
|
|
213
|
+
githubViewProfile: "查看 GitHub",
|
|
214
|
+
githubMetricRepos: "公开仓库",
|
|
215
|
+
githubMetricCommits: "近 30 天 commits",
|
|
216
|
+
githubPinnedEyebrow: "PINNED / 置顶仓库",
|
|
217
|
+
githubPinnedHeading: "正在做的项目",
|
|
218
|
+
githubPinnedFallbackDesc: "查看这个公开仓库的代码与最近更新。",
|
|
219
|
+
githubPinnedEmpty: "配置 GitHub Token 或 author.githubPinned 后,这里会展示置顶仓库。",
|
|
220
|
+
githubContribEyebrow: "CONTRIBUTIONS / 贡献",
|
|
221
|
+
githubContribHeading: "过去一年的公开贡献",
|
|
222
|
+
githubContribCount: (n: number) => `${n} 次贡献`,
|
|
223
|
+
githubContribNeedToken: "需要 GitHub Token",
|
|
224
|
+
githubContribAria: (n: number) => `过去一年 ${n} 次公开贡献`,
|
|
225
|
+
githubContribDayTitle: (date: string, n: number) => `${date} · ${n} 次贡献`,
|
|
226
|
+
githubContribDayAria: (date: string, n: number) => `${date},${n} 次贡献`,
|
|
227
|
+
githubContribEmpty: "正式发布时会使用工作流的只读令牌生成贡献热点图。",
|
|
228
|
+
githubActivityEyebrow: "CONTRIBUTION ACTIVITY / 最近活动",
|
|
229
|
+
githubActivityHeading: "最近在 GitHub 上做了什么",
|
|
230
|
+
githubActivityEmpty: "GitHub 暂时没有返回最近的公开贡献活动。",
|
|
231
|
+
githubEventPush: "推送了代码",
|
|
232
|
+
githubEventPrClosed: "完成了 Pull Request",
|
|
233
|
+
githubEventPrOpen: "提交了 Pull Request",
|
|
234
|
+
githubEventIssueClosed: "关闭了 Issue",
|
|
235
|
+
githubEventIssueOpen: "参与了 Issue",
|
|
236
|
+
githubEventCreateRepo: "创建了仓库",
|
|
237
|
+
githubEventCreateTag: "创建了标签",
|
|
238
|
+
githubEventCreateBranch: "创建了分支",
|
|
239
|
+
githubEventRelease: "发布了新版本",
|
|
240
|
+
// Comments & Webmentions
|
|
241
|
+
commentsSummaryReady: "评论由 GitHub Discussions 保存和管理。",
|
|
242
|
+
commentsSummaryUnconfigured: "评论还没开放。配好下面四项后,这里才会出现评论框。",
|
|
243
|
+
commentsGiscusGuideUrl: "https://giscus.app/zh-CN",
|
|
244
|
+
commentsSetupBefore: "打开",
|
|
245
|
+
commentsSetupMiddle: ",安装到一个已开启 Discussions 的公开仓库,把仓库、仓库 ID、分类、分类 ID 写入 ",
|
|
246
|
+
commentsSetupAnd: " 和 GitHub Actions Variables:",
|
|
247
|
+
commentsSetupJoin: "、",
|
|
248
|
+
commentsSetupEnd: "。",
|
|
249
|
+
giscusLang: "zh-CN",
|
|
250
|
+
webmentionIdleNote: "接收与展示代码已经就绪,注册正式域名后即可开始收集回应。",
|
|
251
|
+
webmentionLoading: "正在读取开放网络回应…",
|
|
252
|
+
webmentionUnavailable: "暂时无法读取回应,稍后会自动恢复。",
|
|
253
|
+
webmentionEmpty: "还没有站外回应。任何链接到这篇文章的网站都可以向它发送 Webmention。",
|
|
254
|
+
webmentionReactionCount: (n: number) => `${n} 个喜欢、转发或收藏`,
|
|
255
|
+
webmentionRespondersLabel: "回应者",
|
|
256
|
+
webmentionViewResponse: "查看站外回应",
|
|
257
|
+
webmentionAuthorFallback: "开放网络访客",
|
|
258
|
+
webmentionViewOriginal: "查看原文",
|
|
259
|
+
// Empty states, embeds, widgets, UI blocks
|
|
260
|
+
emptyPostsEyebrow: "暂无博文",
|
|
261
|
+
emptyPostsTitle: "暂无博文。",
|
|
262
|
+
emptyPostsDesc: "文章会按时间排在这里。",
|
|
263
|
+
emptyPostsCta: "返回首页",
|
|
264
|
+
emptyThoughtsEyebrow: "暂无随想",
|
|
265
|
+
emptyThoughtsTitle: "暂无随想。",
|
|
266
|
+
emptyThoughtsDesc: "短笔记和链接会在这里。",
|
|
267
|
+
emptyThoughtsCta: "先读博文",
|
|
268
|
+
emptyColumnsEyebrow: "暂无专栏",
|
|
269
|
+
emptyColumnsTitle: "暂无专栏。",
|
|
270
|
+
emptyColumnsDesc: "一组文章会按顺序列在这里。",
|
|
271
|
+
emptyColumnsCta: "浏览全部博文",
|
|
272
|
+
embedProviderWechat: "微信公众号",
|
|
273
|
+
embedProviderWeb: "网页摘要",
|
|
274
|
+
embedParseError: "无法解析这个嵌入块,请检查 URL 与 YAML 格式。",
|
|
275
|
+
embedFrameTitle: (label: string) => `${label} 嵌入内容`,
|
|
276
|
+
embedOpenOriginal: "在原网站打开",
|
|
277
|
+
embedFallbackTitle: "打开这条外部内容",
|
|
278
|
+
embedFallbackNoScript: "平台不提供稳定的无脚本嵌入,Garden 会保留清晰、可访问的原文入口。",
|
|
279
|
+
embedFallbackWeb: "查看原网页中的完整内容与上下文。",
|
|
280
|
+
widgetParseError: "无法解析这个嵌入块。请在与 Markdown 同名的目录里放一份 .html 或 .pdf,并在围栏中写 src 与 caption。",
|
|
281
|
+
widgetOpenPage: "打开页面",
|
|
282
|
+
widgetOpenPdf: "打开 PDF",
|
|
283
|
+
widgetPageKind: "页面",
|
|
284
|
+
widgetCollapse: "收起",
|
|
285
|
+
widgetExpand: "展开",
|
|
286
|
+
uiBlockCorrupt: "这块交互的数据损坏了,请重新运行 garden sync。",
|
|
287
|
+
// Admin static notice & metadata
|
|
288
|
+
staticAdminHeading: "后台在另一个地址。",
|
|
289
|
+
staticAdminBody: "公开博客上看不到这一页。写随想和改交互,去单独部署的后台。",
|
|
290
|
+
staticAdminRootBody: "这个页面是公开博客。写随想、上传和转发,去后台。",
|
|
291
|
+
staticAdminOpen: "打开后台",
|
|
292
|
+
staticAdminNoUrl: "还没有填写后台地址。",
|
|
293
|
+
staticAdminBack: "返回公开博客",
|
|
294
|
+
adminTitleMain: "内容管理",
|
|
295
|
+
adminTitleUi: "写作台",
|
|
296
|
+
adminTitleBlocks: "交互块",
|
|
297
|
+
adminTitleCatalog: "组件清单",
|
|
298
|
+
adminTitleEvals: "模型自检",
|
|
299
|
+
adminTitlePreview: "草稿预览",
|
|
300
|
+
a2IndexTitle: "A2 对照",
|
|
301
|
+
a2IndexDeck: "对照参考文章的版式。正文没有改字。",
|
|
110
302
|
};
|
|
111
303
|
|
|
112
304
|
const en: typeof zh = {
|
|
305
|
+
homeAbout: "About",
|
|
306
|
+
homeRss: "RSS",
|
|
307
|
+
homeLatest: "Latest",
|
|
308
|
+
homeRecent: "Recent posts",
|
|
309
|
+
homeAllPosts: (n: number) => `All ${n} ${n === 1 ? "post" : "posts"}`,
|
|
310
|
+
homePinned: "Pinned",
|
|
311
|
+
homeColumns: "Columns",
|
|
312
|
+
homeAllColumns: "All columns",
|
|
313
|
+
columnCount: (n: number, status: string) => `${n} ${n === 1 ? "post" : "posts"} · ${status}`,
|
|
314
|
+
columnDefault: "Active",
|
|
315
|
+
columnOngoing: "Ongoing",
|
|
316
|
+
columnCompleted: "Completed",
|
|
317
|
+
columnReadingList: (n: number, status: string) => `Reading list · ${n} ${n === 1 ? "post" : "posts"} · ${status}`,
|
|
318
|
+
columnSideCount: (n: number, status: string) => `${n} ${n === 1 ? "post" : "posts"} · ${status}`,
|
|
319
|
+
columnCoverAlt: (title: string | undefined) => `${title} cover`,
|
|
320
|
+
columnFallbackDesc: "A series of posts in reading order.",
|
|
321
|
+
columnNewestFirst: (title: string | undefined) => `${title}, newest first`,
|
|
113
322
|
toc: "Contents",
|
|
114
323
|
tocShort: "Contents",
|
|
115
324
|
tocPage: "On this page",
|
|
@@ -154,6 +363,8 @@ const en: typeof zh = {
|
|
|
154
363
|
fullscreen: "Full screen",
|
|
155
364
|
exitFullscreen: "Exit full screen",
|
|
156
365
|
chart: "Chart",
|
|
366
|
+
relations: "Relationship diagram",
|
|
367
|
+
relationsLegend: "Link labels",
|
|
157
368
|
dataTable: "Show data table",
|
|
158
369
|
item: "Item",
|
|
159
370
|
value: "Value",
|
|
@@ -204,19 +415,207 @@ const en: typeof zh = {
|
|
|
204
415
|
escToClose: "to close",
|
|
205
416
|
noResults: (q: string) => (q ? `No posts match “${q}”` : "Nothing to search yet"),
|
|
206
417
|
navLabels: { "/blog/": "Posts", "/thoughts/": "Thoughts", "/columns/": "Columns", "/tags/": "Tags", "/about/": "About" },
|
|
418
|
+
// Home page
|
|
419
|
+
homeLatestBadge: "Latest post",
|
|
420
|
+
homeRecentHeading: "Recent updates",
|
|
421
|
+
homeAllPostsCount: (n: number) => `All ${n} ${n === 1 ? "post" : "posts"}`,
|
|
422
|
+
homePinnedHeading: "Pinned",
|
|
423
|
+
homeColumnsHeading: "Columns",
|
|
424
|
+
homeColumnCountStatus: (count: number, status: string) => `${count} ${count === 1 ? "post" : "posts"}, ${status}`,
|
|
425
|
+
columnDefaultStatus: "Ongoing",
|
|
426
|
+
// Blog archive
|
|
427
|
+
blogHeading: "Posts",
|
|
428
|
+
blogDeck: (n: number) => `Essays on engineering, architecture, and systems thinking. ${n} ${n === 1 ? "post" : "posts"}, archived by year.`,
|
|
429
|
+
unknownYear: "Unknown",
|
|
430
|
+
tagFilterLabel: "Filter by tag",
|
|
431
|
+
tagCollapse: "Show less",
|
|
432
|
+
tagExpandAll: (n: number) => `Show all ${n} tags`,
|
|
433
|
+
tagClear: "Clear",
|
|
434
|
+
densityLabel: "List density",
|
|
435
|
+
densityDetailed: "Detailed",
|
|
436
|
+
densityCompact: "Compact",
|
|
437
|
+
yearPostCount: (n: number) => `${n} ${n === 1 ? "post" : "posts"}`,
|
|
438
|
+
pinnedBadge: "Pinned",
|
|
439
|
+
tagEmptyMatch: "No posts match all of the selected tags.",
|
|
440
|
+
tagDropLast: (tag: string) => `Remove “${tag}”`,
|
|
441
|
+
browseAllPosts: "All posts",
|
|
442
|
+
railJumpByYear: "Jump by year",
|
|
443
|
+
railByYear: "By year",
|
|
444
|
+
railColumns: "Columns",
|
|
445
|
+
// Post & column entry pages
|
|
446
|
+
permalinkLabel: (title: string) => `Permanent link to ${title}`,
|
|
447
|
+
updatedOn: (date: string) => `Updated ${date}`,
|
|
448
|
+
originalLink: "Original ↗",
|
|
449
|
+
coverLabel: (title: string) => `${title} cover`,
|
|
450
|
+
referencesHeading: "References",
|
|
451
|
+
articleTagsLabel: "Tags",
|
|
452
|
+
allColumnsBack: "All columns",
|
|
453
|
+
seriesEyebrow: "Column",
|
|
454
|
+
seriesCountStatus: (count: number, status: string) => `${count} ${count === 1 ? "post" : "posts"} · ${status}`,
|
|
455
|
+
// Columns index
|
|
456
|
+
columnsFallbackTitle: "Columns",
|
|
457
|
+
columnsMeta: (count: number, status: string) => `Reading list · ${count} ${count === 1 ? "post" : "posts"} · ${status}`,
|
|
458
|
+
columnsFallbackDesc: "A series of posts organized in reading order.",
|
|
459
|
+
columnsOrderLabel: (title: string) => `${title}, newest to oldest`,
|
|
460
|
+
// Tags page
|
|
461
|
+
tagsTitle: "Tags",
|
|
462
|
+
tagsDescription: "All tags.",
|
|
463
|
+
tagsDeck: (n: number) => `${n} ${n === 1 ? "tag" : "tags"}, ordered by post count. Pick a tag to view matching posts.`,
|
|
464
|
+
tagsIndexLabel: "All tags",
|
|
465
|
+
tagsEmpty: "No tags yet.",
|
|
466
|
+
// Thoughts
|
|
467
|
+
thoughtsEyebrow: "THOUGHTS",
|
|
468
|
+
backToThoughts: "Back to thoughts",
|
|
469
|
+
thoughtMixedLabel: (n: number) => `Mixed note · ${n} ${n === 1 ? "attachment" : "attachments"}`,
|
|
470
|
+
thoughtImageLabel: "Photo",
|
|
471
|
+
thoughtAudioLabel: "Audio",
|
|
472
|
+
thoughtVideoLabel: "Video",
|
|
473
|
+
thoughtLinkLabel: "Link",
|
|
474
|
+
thoughtTextLabel: "Thought",
|
|
475
|
+
thoughtGalleryLabel: (n: number) => `${n} ${n === 1 ? "photo" : "photos"}`,
|
|
476
|
+
thoughtPhotoAlt: (title: string, idx: number) => `${title} · photo ${idx}`,
|
|
477
|
+
thoughtNowPlaying: (title: string) => `Now playing · ${title}`,
|
|
478
|
+
audioFallback: "Your browser does not support audio playback.",
|
|
479
|
+
thoughtPermalinkShort: "Permalink",
|
|
480
|
+
// About & GitHub profile
|
|
481
|
+
aboutEyebrow: "About",
|
|
482
|
+
aboutFallbackHeading: "About",
|
|
483
|
+
aboutFallbackParagraphs: [
|
|
484
|
+
"An engineer who writes code and writes down problems once they are clear.",
|
|
485
|
+
"This site holds posts, thoughts, and columns.",
|
|
486
|
+
],
|
|
487
|
+
aboutFocusHeading: "Current focus",
|
|
488
|
+
aboutFallbackFocus: ["Software", "Notes", "Daily work"],
|
|
489
|
+
aboutFallbackCta: "Browse posts",
|
|
490
|
+
githubProfileAria: "GitHub profile",
|
|
491
|
+
githubAvatarAlt: (login: string) => `${login}'s GitHub avatar`,
|
|
492
|
+
githubOverviewPrefix: "GitHub public profile",
|
|
493
|
+
githubSyncSynced: "Synced on this visit",
|
|
494
|
+
githubSyncSnapshot: "Using build snapshot",
|
|
495
|
+
githubSyncSyncing: "Syncing live profile",
|
|
496
|
+
githubViewProfile: "View GitHub",
|
|
497
|
+
githubMetricRepos: "Public repos",
|
|
498
|
+
githubMetricCommits: "Commits (30d)",
|
|
499
|
+
githubPinnedEyebrow: "PINNED",
|
|
500
|
+
githubPinnedHeading: "Pinned repositories",
|
|
501
|
+
githubPinnedFallbackDesc: "View code and recent updates in this public repository.",
|
|
502
|
+
githubPinnedEmpty: "Configure a GitHub Token or author.githubPinned to show pinned repositories here.",
|
|
503
|
+
githubContribEyebrow: "CONTRIBUTIONS",
|
|
504
|
+
githubContribHeading: "Public contributions in the past year",
|
|
505
|
+
githubContribCount: (n: number) => `${n} ${n === 1 ? "contribution" : "contributions"}`,
|
|
506
|
+
githubContribNeedToken: "GitHub Token required",
|
|
507
|
+
githubContribAria: (n: number) => `${n} public ${n === 1 ? "contribution" : "contributions"} in the past year`,
|
|
508
|
+
githubContribDayTitle: (date: string, n: number) => `${date} · ${n} ${n === 1 ? "contribution" : "contributions"}`,
|
|
509
|
+
githubContribDayAria: (date: string, n: number) => `${date}, ${n} ${n === 1 ? "contribution" : "contributions"}`,
|
|
510
|
+
githubContribEmpty: "Production builds use the workflow's read-only token to generate the contribution heatmap.",
|
|
511
|
+
githubActivityEyebrow: "CONTRIBUTION ACTIVITY",
|
|
512
|
+
githubActivityHeading: "Recent activity on GitHub",
|
|
513
|
+
githubActivityEmpty: "No recent public GitHub activity returned right now.",
|
|
514
|
+
githubEventPush: "Pushed commits",
|
|
515
|
+
githubEventPrClosed: "Completed a Pull Request",
|
|
516
|
+
githubEventPrOpen: "Opened a Pull Request",
|
|
517
|
+
githubEventIssueClosed: "Closed an Issue",
|
|
518
|
+
githubEventIssueOpen: "Participated in an Issue",
|
|
519
|
+
githubEventCreateRepo: "Created a repository",
|
|
520
|
+
githubEventCreateTag: "Created a tag",
|
|
521
|
+
githubEventCreateBranch: "Created a branch",
|
|
522
|
+
githubEventRelease: "Published a release",
|
|
523
|
+
// Comments & Webmentions
|
|
524
|
+
commentsSummaryReady: "Comments are stored and managed via GitHub Discussions.",
|
|
525
|
+
commentsSummaryUnconfigured: "Comments are not enabled yet. Configure the four variables below to show the comment box.",
|
|
526
|
+
commentsGiscusGuideUrl: "https://giscus.app",
|
|
527
|
+
commentsSetupBefore: "Open",
|
|
528
|
+
commentsSetupMiddle: ", install it on a public repository with Discussions enabled, and add the repository, repository ID, category, and category ID to ",
|
|
529
|
+
commentsSetupAnd: " and GitHub Actions Variables: ",
|
|
530
|
+
commentsSetupJoin: ", ",
|
|
531
|
+
commentsSetupEnd: ".",
|
|
532
|
+
giscusLang: "en",
|
|
533
|
+
webmentionIdleNote: "Webmention receiving and display are ready; responses will appear once a custom domain is connected.",
|
|
534
|
+
webmentionLoading: "Loading responses from the open web…",
|
|
535
|
+
webmentionUnavailable: "Responses are temporarily unavailable. Please try again later.",
|
|
536
|
+
webmentionEmpty: "No external responses yet. Any site linking to this post can send a Webmention.",
|
|
537
|
+
webmentionReactionCount: (n: number) => `${n} ${n === 1 ? "like, repost, or bookmark" : "likes, reposts, or bookmarks"}`,
|
|
538
|
+
webmentionRespondersLabel: "Responders",
|
|
539
|
+
webmentionViewResponse: "View external response",
|
|
540
|
+
webmentionAuthorFallback: "Open web reader",
|
|
541
|
+
webmentionViewOriginal: "View original",
|
|
542
|
+
// Empty states, embeds, widgets, UI blocks
|
|
543
|
+
emptyPostsEyebrow: "No posts yet",
|
|
544
|
+
emptyPostsTitle: "No posts yet.",
|
|
545
|
+
emptyPostsDesc: "Published posts will appear here in chronological order.",
|
|
546
|
+
emptyPostsCta: "Back to home",
|
|
547
|
+
emptyThoughtsEyebrow: "No thoughts yet",
|
|
548
|
+
emptyThoughtsTitle: "No thoughts yet.",
|
|
549
|
+
emptyThoughtsDesc: "Short notes and links will appear here.",
|
|
550
|
+
emptyThoughtsCta: "Browse posts",
|
|
551
|
+
emptyColumnsEyebrow: "No columns yet",
|
|
552
|
+
emptyColumnsTitle: "No columns yet.",
|
|
553
|
+
emptyColumnsDesc: "Curated post series will appear here in reading order.",
|
|
554
|
+
emptyColumnsCta: "Browse all posts",
|
|
555
|
+
embedProviderWechat: "WeChat Official Account",
|
|
556
|
+
embedProviderWeb: "Web excerpt",
|
|
557
|
+
embedParseError: "Could not parse this embed block. Please check the URL and YAML syntax.",
|
|
558
|
+
embedFrameTitle: (label: string) => `${label} embedded content`,
|
|
559
|
+
embedOpenOriginal: "Open original",
|
|
560
|
+
embedFallbackTitle: "Open external content",
|
|
561
|
+
embedFallbackNoScript: "This platform does not provide a stable script-free embed; Garden keeps a clean link to the original post.",
|
|
562
|
+
embedFallbackWeb: "Read the full page and context on the original site.",
|
|
563
|
+
widgetParseError: "Could not parse this widget block. Place a .html or .pdf file in the folder matching the Markdown file name, and specify src and caption.",
|
|
564
|
+
widgetOpenPage: "Open page",
|
|
565
|
+
widgetOpenPdf: "Open PDF",
|
|
566
|
+
widgetPageKind: "Page",
|
|
567
|
+
widgetCollapse: "Collapse",
|
|
568
|
+
widgetExpand: "Expand",
|
|
569
|
+
uiBlockCorrupt: "This interactive block's payload is corrupted. Please re-run garden sync.",
|
|
570
|
+
// Admin static notice & metadata
|
|
571
|
+
staticAdminHeading: "Admin lives at a separate address.",
|
|
572
|
+
staticAdminBody: "This page is not available on the public blog. Use your separately deployed admin instance to post thoughts or edit interactive blocks.",
|
|
573
|
+
staticAdminRootBody: "This is the public blog. Use the admin site to post thoughts, upload media, and share.",
|
|
574
|
+
staticAdminOpen: "Open admin",
|
|
575
|
+
staticAdminNoUrl: "No admin URL configured yet.",
|
|
576
|
+
staticAdminBack: "Back to public blog",
|
|
577
|
+
adminTitleMain: "Content Studio",
|
|
578
|
+
adminTitleUi: "Writing Studio",
|
|
579
|
+
adminTitleBlocks: "Interactive Blocks",
|
|
580
|
+
adminTitleCatalog: "Component Catalog",
|
|
581
|
+
adminTitleEvals: "Model Evals",
|
|
582
|
+
adminTitlePreview: "Draft Preview",
|
|
583
|
+
a2IndexTitle: "A2 Parity",
|
|
584
|
+
a2IndexDeck: "Layout comparison against reference articles. Body text is unchanged.",
|
|
207
585
|
};
|
|
208
586
|
|
|
209
587
|
export const UI_STRINGS = { zh, en } as const;
|
|
210
588
|
export const ui = (lang: UiLang = siteLang) => UI_STRINGS[lang];
|
|
211
589
|
|
|
590
|
+
/**
|
|
591
|
+
* Normalize a browser or router pathname to the in-app path (stripping a
|
|
592
|
+
* configured basePath or a leading deployment prefix before /garden).
|
|
593
|
+
*/
|
|
594
|
+
export function inAppPath(pathname: string | null | undefined): string {
|
|
595
|
+
return resolveInAppPath(pathname, siteLinks?.basePath);
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
export function isHandbookPath(pathname: string | null | undefined): boolean {
|
|
599
|
+
return resolveIsHandbookPath(pathname, siteLinks?.basePath);
|
|
600
|
+
}
|
|
601
|
+
|
|
212
602
|
/**
|
|
213
603
|
* Chrome language for a route. The handbook follows its own URL (/garden/en/
|
|
214
604
|
* is English, the rest of /garden/ is Chinese); every other page uses the site
|
|
215
605
|
* language. `pathname` is the in-app path (no basePath), as usePathname gives.
|
|
216
606
|
*/
|
|
217
607
|
export function routeLang(pathname: string | null | undefined): UiLang {
|
|
218
|
-
const path = pathname
|
|
608
|
+
const path = inAppPath(pathname);
|
|
219
609
|
if (/^\/garden\/en(\/|$)/u.test(path)) return "en";
|
|
220
|
-
if (/^\/garden(\/|$)/u.test(path)) return "zh";
|
|
610
|
+
if (/^\/garden(\/|$)/u.test(path)) return siteLang === "en" ? "en" : "zh";
|
|
221
611
|
return siteLang;
|
|
222
612
|
}
|
|
613
|
+
|
|
614
|
+
/** Label for a column status: the enum values are localized, any other author text is shown as written. */
|
|
615
|
+
export const columnStatusLabel = (status: string | undefined, lang: UiLang = siteLang) => {
|
|
616
|
+
const t = ui(lang);
|
|
617
|
+
if (!status || status === "default") return t.columnDefault;
|
|
618
|
+
if (status === "ongoing") return t.columnOngoing;
|
|
619
|
+
if (status === "completed") return t.columnCompleted;
|
|
620
|
+
return status;
|
|
621
|
+
};
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handbook field tables (first header "字段"/"Field" plus a "是否必填"/"Required"
|
|
3
|
+
* column): tag the table, mark the Required column, and give every body cell its
|
|
4
|
+
* column name, so narrow screens can show each row as a small card.
|
|
5
|
+
*/
|
|
6
|
+
import { toString } from "hast-util-to-string";
|
|
7
|
+
import { visit } from "unist-util-visit";
|
|
8
|
+
|
|
9
|
+
// "字段", "字段或标签", "Field", "Field or Tag"…
|
|
10
|
+
const FIELD = /^(字段|field\b)/iu;
|
|
11
|
+
const REQUIRED = new Set(["是否必填", "required"]);
|
|
12
|
+
|
|
13
|
+
const cells = (row) => (row?.children ?? []).filter((node) => node.type === "element" && (node.tagName === "th" || node.tagName === "td"));
|
|
14
|
+
const rows = (node) => {
|
|
15
|
+
const out = [];
|
|
16
|
+
visit(node, "element", (child) => { if (child.tagName === "tr") out.push(child); });
|
|
17
|
+
return out;
|
|
18
|
+
};
|
|
19
|
+
const addClass = (node, name) => {
|
|
20
|
+
const props = (node.properties ??= {});
|
|
21
|
+
const list = Array.isArray(props.className) ? props.className : props.className ? [String(props.className)] : [];
|
|
22
|
+
if (!list.includes(name)) props.className = [...list, name];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export function isFieldTableHeader(labels) {
|
|
26
|
+
return labels.length >= 3 && FIELD.test(labels[0].trim()) && labels.some((label) => REQUIRED.has(label.trim().toLowerCase()));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function rehypeFieldTables() {
|
|
30
|
+
return (tree) => {
|
|
31
|
+
visit(tree, "element", (table) => {
|
|
32
|
+
if (table.tagName !== "table") return;
|
|
33
|
+
const [head, ...body] = rows(table);
|
|
34
|
+
const labels = cells(head).map((cell) => toString(cell).trim());
|
|
35
|
+
if (!isFieldTableHeader(labels)) return;
|
|
36
|
+
addClass(table, "field-table");
|
|
37
|
+
const required = labels.findIndex((label) => REQUIRED.has(label.toLowerCase()));
|
|
38
|
+
cells(head).forEach((cell, i) => { if (i === required) addClass(cell, "ft-req"); });
|
|
39
|
+
for (const row of body) {
|
|
40
|
+
cells(row).forEach((cell, i) => {
|
|
41
|
+
cell.properties = { ...(cell.properties ?? {}), dataLabel: labels[i] ?? "" };
|
|
42
|
+
if (i === required) addClass(cell, "ft-req");
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
}
|