hd-garden 0.8.0 → 0.8.1

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.
Files changed (185) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +2 -2
  2. package/.env.en.example +67 -0
  3. package/README.en.md +2 -2
  4. package/README.md +2 -2
  5. package/app/a2/[slug]/page.tsx +4 -3
  6. package/app/a2/page.tsx +4 -3
  7. package/app/about/page.tsx +8 -9
  8. package/app/admin/page.tsx +7 -6
  9. package/app/admin/preview/[...path]/page.tsx +2 -1
  10. package/app/admin/ui/blocks/page.tsx +2 -1
  11. package/app/admin/ui/catalog/page.tsx +2 -1
  12. package/app/admin/ui/evals/page.tsx +2 -1
  13. package/app/admin/ui/page.tsx +2 -1
  14. package/app/blog/[...path]/page.tsx +7 -6
  15. package/app/blog/page.tsx +4 -2
  16. package/app/columns/[column]/[...path]/page.tsx +8 -7
  17. package/app/columns/page.tsx +12 -8
  18. package/app/components/AdminUiShell.tsx +4 -3
  19. package/app/components/ArticleReferences.tsx +4 -1
  20. package/app/components/ArticleTags.tsx +3 -2
  21. package/app/components/BlogArchive.tsx +20 -16
  22. package/app/components/Comments.tsx +16 -10
  23. package/app/components/ContentEmptyState.tsx +29 -28
  24. package/app/components/ExternalEmbed.tsx +15 -14
  25. package/app/components/GardenContentGuide.tsx +10 -2
  26. package/app/components/GardenDocsNav.tsx +4 -2
  27. package/app/components/GiscusComments.tsx +2 -1
  28. package/app/components/GithubLiveOverview.tsx +10 -8
  29. package/app/components/GithubProfile.tsx +31 -21
  30. package/app/components/HandbookLangSwitch.tsx +8 -4
  31. package/app/components/HandbookView.tsx +92 -9
  32. package/app/components/MarkdownArticle.tsx +31 -7
  33. package/app/components/SiteChrome.tsx +3 -3
  34. package/app/components/SiteFooter.tsx +1 -1
  35. package/app/components/SiteHeader.tsx +13 -4
  36. package/app/components/ThoughtCard.tsx +16 -14
  37. package/app/components/UiBlock.tsx +3 -2
  38. package/app/components/Webmentions.tsx +11 -11
  39. package/app/components/WidgetChart.tsx +8 -6
  40. package/app/components/garden/FilePreview.tsx +2 -2
  41. package/app/components/garden/GardenBlock.tsx +7 -0
  42. package/app/components/garden/GardenFile.tsx +2 -2
  43. package/app/components/garden/RefDemo.tsx +62 -0
  44. package/app/components/garden/blocks.tsx +56 -8
  45. package/app/components/garden/patterns.tsx +497 -0
  46. package/app/garden/[...slug]/page.tsx +3 -2
  47. package/app/garden/en/page.tsx +7 -7
  48. package/app/garden/page.tsx +3 -2
  49. package/app/globals.css +10 -0
  50. package/app/lib/a2-meta.ts +26 -3
  51. package/app/lib/content.ts +2 -2
  52. package/app/lib/garden-docs-meta.ts +24 -0
  53. package/app/lib/github-id.mjs +8 -0
  54. package/app/lib/handbook-switch.mjs +12 -0
  55. package/app/lib/i18n.ts +401 -2
  56. package/app/lib/rehype-field-tables.mjs +47 -0
  57. package/app/lib/remark-a2.mjs +18 -8
  58. package/app/lib/remark-link-card.mjs +22 -0
  59. package/app/lib/remark-ref-demos.mjs +42 -0
  60. package/app/lib/tag-filter.ts +9 -2
  61. package/app/not-found.tsx +32 -4
  62. package/app/page.tsx +17 -14
  63. package/app/rss.xml/route.ts +2 -1
  64. package/app/styles/a2.css +338 -0
  65. package/app/styles/shell.css +15 -2
  66. package/app/tags/page.tsx +7 -5
  67. package/app/thoughts/[slug]/page.tsx +2 -1
  68. package/app/thoughts/page/[n]/page.tsx +1 -1
  69. package/app/thoughts/page.tsx +2 -1
  70. package/app/ui-parity/page.tsx +38 -22
  71. package/bin/garden.mjs +129 -19
  72. package/docs/handbook/ai-writing.md +2 -2
  73. package/docs/handbook/assets/reading-layout-en.svg +26 -0
  74. package/docs/handbook/components/card.md +43 -22
  75. package/docs/handbook/components/chart.md +10 -26
  76. package/docs/handbook/components/choice.md +42 -0
  77. package/docs/handbook/components/compare.md +32 -17
  78. package/docs/handbook/components/figure.md +33 -46
  79. package/docs/handbook/components/file.md +10 -15
  80. package/docs/handbook/components/html.md +10 -30
  81. package/docs/handbook/components/interactive.md +9 -35
  82. package/docs/handbook/components/list.md +65 -0
  83. package/docs/handbook/components/relations.md +95 -0
  84. package/docs/handbook/components/stat.md +10 -28
  85. package/docs/handbook/components/timeline.md +13 -28
  86. package/docs/handbook/components.md +210 -19
  87. package/docs/handbook/config.md +5 -3
  88. package/docs/handbook/deploy.md +1 -1
  89. package/docs/handbook/design.md +20 -12
  90. package/docs/handbook/en/ai-writing.md +2 -2
  91. package/docs/handbook/en/components/card.md +42 -21
  92. package/docs/handbook/en/components/chart.md +10 -26
  93. package/docs/handbook/en/components/choice.md +42 -0
  94. package/docs/handbook/en/components/compare.md +30 -15
  95. package/docs/handbook/en/components/figure.md +29 -42
  96. package/docs/handbook/en/components/file.md +9 -14
  97. package/docs/handbook/en/components/html.md +9 -29
  98. package/docs/handbook/en/components/interactive.md +8 -34
  99. package/docs/handbook/en/components/list.md +65 -0
  100. package/docs/handbook/en/components/relations.md +95 -0
  101. package/docs/handbook/en/components/stat.md +9 -27
  102. package/docs/handbook/en/components/timeline.md +9 -24
  103. package/docs/handbook/en/components.md +211 -19
  104. package/docs/handbook/en/config.md +32 -30
  105. package/docs/handbook/en/deploy.md +12 -12
  106. package/docs/handbook/en/design.md +19 -12
  107. package/docs/handbook/en/index.md +2 -2
  108. package/docs/handbook/en/reference/card.md +85 -0
  109. package/docs/handbook/en/reference/chart.md +68 -0
  110. package/docs/handbook/en/reference/choice.md +69 -0
  111. package/docs/handbook/en/reference/compare.md +59 -0
  112. package/docs/handbook/en/reference/figure.md +84 -0
  113. package/docs/handbook/en/reference/file/checklist.md +17 -0
  114. package/docs/handbook/en/reference/file.md +50 -0
  115. package/docs/handbook/en/reference/html.md +62 -0
  116. package/docs/handbook/en/reference/interactive.md +71 -0
  117. package/docs/handbook/en/reference/list.md +72 -0
  118. package/docs/handbook/en/reference/relations.md +113 -0
  119. package/docs/handbook/en/reference/stat.md +60 -0
  120. package/docs/handbook/en/reference/timeline.md +56 -0
  121. package/docs/handbook/en/reference.md +26 -0
  122. package/docs/handbook/en/start.md +2 -2
  123. package/docs/handbook/en/workflow.md +6 -5
  124. package/docs/handbook/en/writing.md +3 -2
  125. package/docs/handbook/index.md +1 -1
  126. package/docs/handbook/reference/card.md +85 -0
  127. package/docs/handbook/reference/chart.md +68 -0
  128. package/docs/handbook/reference/choice.md +69 -0
  129. package/docs/handbook/reference/compare.md +59 -0
  130. package/docs/handbook/reference/figure.md +84 -0
  131. package/docs/handbook/reference/file/checklist.md +17 -0
  132. package/docs/handbook/reference/file.md +50 -0
  133. package/docs/handbook/reference/html.md +62 -0
  134. package/docs/handbook/reference/interactive.md +71 -0
  135. package/docs/handbook/reference/list.md +72 -0
  136. package/docs/handbook/reference/relations.md +113 -0
  137. package/docs/handbook/reference/stat.md +60 -0
  138. package/docs/handbook/reference/timeline.md +56 -0
  139. package/docs/handbook/reference.md +26 -0
  140. package/docs/handbook/start.md +1 -1
  141. package/docs/handbook/workflow.md +6 -5
  142. package/docs/handbook/writing.md +3 -2
  143. package/next.config.ts +6 -0
  144. package/package.json +2 -1
  145. package/public/images/garden/ref-card-book.svg +1 -0
  146. package/public/images/garden/ref-card-lamp.svg +1 -0
  147. package/scripts/build-content.mjs +37 -16
  148. package/scripts/favicon-ico.mjs +9 -6
  149. package/scripts/file-preview.mjs +10 -7
  150. package/scripts/handbook-allowlist.mjs +18 -0
  151. package/scripts/handbook-facts.mjs +305 -11
  152. package/scripts/link-assets.mjs +7 -0
  153. package/scripts/new-post.mjs +18 -9
  154. package/scripts/prepare-pages-output.mjs +30 -18
  155. package/scripts/redirects.mjs +17 -6
  156. package/templates/garden-blocks.md +67 -0
  157. package/templates/garden-snippets.plist +61 -0
  158. package/templates/site/Makefile.en +45 -0
  159. package/templates/site/README.en.md +15 -0
  160. package/templates/site/site.config.en.yaml +55 -0
  161. package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
  162. package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
  163. package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
  164. package/templates/skills-en/garden-post-review/SKILL.md +63 -0
  165. package/templates/skills-en/garden-ui/SKILL.md +118 -0
  166. package/templates/workflows/pages.en.yml +195 -0
  167. package/templates/workflows/pages.yml +195 -0
  168. package/ui/garden/catalog.mjs +205 -4
  169. package/ui/garden/check.mjs +6 -0
  170. package/ui/garden/file-block.mjs +8 -7
  171. package/ui/garden/html-lang.mjs +73 -1
  172. package/ui/garden/markdown-links.mjs +7 -4
  173. package/ui/garden/parse.mjs +45 -0
  174. package/ui/garden/relations.mjs +252 -0
  175. package/ui/garden/search-index.mjs +6 -4
  176. package/ui/garden/source-icon.mjs +54 -0
  177. package/ui/parity-fixtures.mjs +77 -0
  178. package/ui/preview/dist/garden-ui.esm.js +41 -4
  179. package/ui/preview/dist/garden-ui.iife.js +41 -4
  180. package/ui/prompt/rules.md +28 -1
  181. package/ui/react/components/chart.tsx +1 -1
  182. package/ui/react/components/display.tsx +1 -1
  183. package/ui/react/lang.tsx +4 -0
  184. package/ui/react/parity.tsx +8 -6
  185. package/worker/index.ts +8 -2
@@ -17,6 +17,7 @@ import {
17
17
  } from "../lib/tag-filter";
18
18
  import { ContentEmptyState } from "./ContentEmptyState";
19
19
  import { siteHref } from "../lib/site-href";
20
+ import { siteLang, ui } from "../lib/i18n";
20
21
 
21
22
  function day(value: string) {
22
23
  const date = new Date(value);
@@ -72,6 +73,7 @@ export function BlogArchiveFallback({ posts, series }: ArchiveData) {
72
73
  }
73
74
 
74
75
  function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected: string[]; go: (next: string[]) => void }) {
76
+ const t = ui();
75
77
  const [compact, setCompact] = useState(false);
76
78
  const [expanded, setExpanded] = useState(false);
77
79
  const options = useMemo(() => tagOptions(posts, selected), [posts, selected]);
@@ -81,7 +83,7 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
81
83
  .sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
82
84
  const years = new Map<string, typeof filtered>();
83
85
  for (const post of filtered) {
84
- const year = day(post.date).slice(0, 4) || "未知";
86
+ const year = day(post.date).slice(0, 4) || t.unknownYear;
85
87
  const list = years.get(year);
86
88
  if (list) list.push(post);
87
89
  else years.set(year, [post]);
@@ -92,7 +94,7 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
92
94
  <div>
93
95
  {options.length ? (
94
96
  <div className="sh-tagfilter">
95
- <ul className="sh-tagchips" aria-label="按标签筛选">
97
+ <ul className="sh-tagchips" aria-label={t.tagFilterLabel}>
96
98
  {shown.map((option) => (
97
99
  <li key={option.tag}>
98
100
  <button
@@ -111,7 +113,9 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
111
113
  {hidden > 0 || expanded ? (
112
114
  <li>
113
115
  <button type="button" className="sh-tagmore" aria-expanded={expanded} onClick={() => setExpanded(!expanded)}>
114
- {expanded ? "收起" : `展开全部 ${options.length} 个标签`}
116
+ {siteLang === "en"
117
+ ? (expanded ? t.tagCollapse : t.tagExpandAll(options.length))
118
+ : (expanded ? "收起" : `展开全部 ${options.length} 个标签`)}
115
119
  </button>
116
120
  </li>
117
121
  ) : null}
@@ -119,17 +123,17 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
119
123
  </div>
120
124
  ) : null}
121
125
  <div className="sh-status">
122
- <span aria-live="polite">{tagSummary(selected.length, filtered.length)}</span>
123
- {selected.length ? <button type="button" onClick={() => go([])}>清除</button> : null}
124
- <div className="sh-view" role="group" aria-label="列表密度">
125
- <button type="button" aria-pressed={!compact} onClick={() => setCompact(false)}>摘要</button>
126
- <button type="button" aria-pressed={compact} onClick={() => setCompact(true)}>紧凑</button>
126
+ <span aria-live="polite">{tagSummary(selected.length, filtered.length, siteLang)}</span>
127
+ {selected.length ? <button type="button" onClick={() => go([])}>{siteLang === "en" ? t.tagClear : "清除"}</button> : null}
128
+ <div className="sh-view" role="group" aria-label={t.densityLabel}>
129
+ <button type="button" aria-pressed={!compact} onClick={() => setCompact(false)}>{t.densityDetailed}</button>
130
+ <button type="button" aria-pressed={compact} onClick={() => setCompact(true)}>{t.densityCompact}</button>
127
131
  </div>
128
132
  </div>
129
133
  <div id="sh-list" className={compact ? "is-compact" : undefined}>
130
134
  {filtered.length ? [...years.entries()].map(([year, entries]) => (
131
135
  <section className="sh-year" id={`y${year}`} key={year} aria-labelledby={`yh${year}`}>
132
- <h2 id={`yh${year}`}>{year}<span className="n">{entries.length} 篇</span></h2>
136
+ <h2 id={`yh${year}`}>{year}<span className="n">{siteLang === "en" ? t.yearPostCount(entries.length) : <>{entries.length} 篇</>}</span></h2>
133
137
  <ol className="sh-rows">
134
138
  {entries.map((post) => (
135
139
  <li className="sh-row" key={post.path}>
@@ -137,7 +141,7 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
137
141
  <div className="archive-copy">
138
142
  <h3>
139
143
  <Link href={postHref(post)}>{post.title}</Link>
140
- {post.pinned ? <span className="pin">置顶</span> : null}
144
+ {post.pinned ? <span className="pin">{t.pinnedBadge}</span> : null}
141
145
  </h3>
142
146
  {post.description ? <p>{post.description}</p> : null}
143
147
  </div>
@@ -148,19 +152,19 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
148
152
  </section>
149
153
  )) : selected.length ? (
150
154
  <div className="sh-tagempty">
151
- <p>没有同时带这些标签的文章。</p>
155
+ <p>{siteLang === "en" ? t.tagEmptyMatch : "没有同时带这些标签的文章。"}</p>
152
156
  <div className="acts">
153
157
  {selected.length > 1 ? (
154
- <button type="button" onClick={() => go(selected.slice(0, -1))}>去掉「{selected[selected.length - 1]}」</button>
158
+ <button type="button" onClick={() => go(selected.slice(0, -1))}>{t.tagDropLast(selected[selected.length - 1])}</button>
155
159
  ) : null}
156
- <button type="button" onClick={() => go([])}>看全部文章</button>
160
+ <button type="button" onClick={() => go([])}>{t.browseAllPosts}</button>
157
161
  </div>
158
162
  </div>
159
163
  ) : <ContentEmptyState kind="posts" />}
160
164
  </div>
161
165
  </div>
162
- <aside className="sh-rail" aria-label="按年份跳转">
163
- {years.size ? <h2>按年份</h2> : null}
166
+ <aside className="sh-rail" aria-label={t.railJumpByYear}>
167
+ {years.size ? <h2>{t.railByYear}</h2> : null}
164
168
  <ol>
165
169
  {[...years.keys()].map((year) => (
166
170
  <li key={year}>
@@ -170,7 +174,7 @@ function ArchiveView({ posts, series, selected, go }: ArchiveData & { selected:
170
174
  </ol>
171
175
  {series.length ? (
172
176
  <>
173
- <h2>专栏</h2>
177
+ <h2>{t.railColumns}</h2>
174
178
  <ol>
175
179
  {series.map((column) => (
176
180
  <li key={column.slug}>
@@ -5,17 +5,16 @@ import { ui } from "../lib/i18n";
5
5
 
6
6
  export function Comments({ slug }: { slug: string }) {
7
7
  const config = readGiscusConfig();
8
+ const t = ui();
8
9
 
9
10
  return (
10
11
  <section className="comments" aria-labelledby={`comments-heading-${slug}`}>
11
12
  <div className="comments-heading">
12
13
  <ChatCircle size={23} />
13
14
  <div>
14
- <h2 id={`comments-heading-${slug}`}>{ui().comments}</h2>
15
+ <h2 id={`comments-heading-${slug}`}>{t.comments}</h2>
15
16
  <p>
16
- {config
17
- ? "评论由 GitHub Discussions 保存和管理。"
18
- : "评论还没开放。配好下面四项后,这里才会出现评论框。"}
17
+ {config ? t.commentsSummaryReady : t.commentsSummaryUnconfigured}
19
18
  </p>
20
19
  </div>
21
20
  </div>
@@ -26,14 +25,21 @@ export function Comments({ slug }: { slug: string }) {
26
25
  <div className="integration-notice">
27
26
  <GithubLogo size={20} weight="fill" />
28
27
  <p>
29
- 打开{" "}
30
- <a href="https://giscus.app/zh-CN" target="_blank" rel="noreferrer">
28
+ {t.commentsSetupBefore}{" "}
29
+ <a href={t.commentsGiscusGuideUrl} target="_blank" rel="noreferrer">
31
30
  Giscus
32
31
  </a>
33
- ,安装到一个已开启 Discussions 的公开仓库,把仓库、仓库 ID、分类、分类 ID
34
- 写入 <code>.env.local</code> 和 GitHub Actions Variables:
35
- <code>GISCUS_REPO</code>、<code>GISCUS_REPO_ID</code>、
36
- <code>GISCUS_CATEGORY</code>、<code>GISCUS_CATEGORY_ID</code>。
32
+ {t.commentsSetupMiddle}
33
+ <code>.env.local</code>
34
+ {t.commentsSetupAnd}
35
+ <code>GISCUS_REPO</code>
36
+ {t.commentsSetupJoin}
37
+ <code>GISCUS_REPO_ID</code>
38
+ {t.commentsSetupJoin}
39
+ <code>GISCUS_CATEGORY</code>
40
+ {t.commentsSetupJoin}
41
+ <code>GISCUS_CATEGORY_ID</code>
42
+ {t.commentsSetupEnd}
37
43
  </p>
38
44
  </div>
39
45
  )}
@@ -3,35 +3,36 @@
3
3
  import Link from "next/link";
4
4
  import { ArrowRight, Article, Books, Quotes } from "@phosphor-icons/react/ssr";
5
5
  import { siteHref } from "../lib/site-href";
6
+ import { ui } from "../lib/i18n";
6
7
 
7
- const emptyContent = {
8
- posts: {
9
- icon: Article,
10
- eyebrow: "暂无博文",
11
- title: "暂无博文。",
12
- description: "文章会按时间排在这里。",
13
- actionHref: "/",
14
- actionLabel: "返回首页",
15
- },
16
- thoughts: {
17
- icon: Quotes,
18
- eyebrow: "暂无随想",
19
- title: "暂无随想。",
20
- description: "短笔记和链接会在这里。",
21
- actionHref: "/blog",
22
- actionLabel: "先读博文",
23
- },
24
- columns: {
25
- icon: Books,
26
- eyebrow: "暂无专栏",
27
- title: "暂无专栏。",
28
- description: "一组文章会按顺序列在这里。",
29
- actionHref: "/blog",
30
- actionLabel: "浏览全部博文",
31
- },
32
- } as const;
33
-
34
- export function ContentEmptyState({ kind }: { kind: keyof typeof emptyContent }) {
8
+ export function ContentEmptyState({ kind }: { kind: "posts" | "thoughts" | "columns" }) {
9
+ const s = ui();
10
+ const emptyContent = {
11
+ posts: {
12
+ icon: Article,
13
+ eyebrow: s.emptyPostsEyebrow,
14
+ title: s.emptyPostsTitle,
15
+ description: s.emptyPostsDesc,
16
+ actionHref: "/",
17
+ actionLabel: s.emptyPostsCta,
18
+ },
19
+ thoughts: {
20
+ icon: Quotes,
21
+ eyebrow: s.emptyThoughtsEyebrow,
22
+ title: s.emptyThoughtsTitle,
23
+ description: s.emptyThoughtsDesc,
24
+ actionHref: "/blog",
25
+ actionLabel: s.emptyThoughtsCta,
26
+ },
27
+ columns: {
28
+ icon: Books,
29
+ eyebrow: s.emptyColumnsEyebrow,
30
+ title: s.emptyColumnsTitle,
31
+ description: s.emptyColumnsDesc,
32
+ actionHref: "/blog",
33
+ actionLabel: s.emptyColumnsCta,
34
+ },
35
+ } as const;
35
36
  const content = emptyContent[kind];
36
37
  const Icon = content.icon;
37
38
 
@@ -7,6 +7,7 @@ import {
7
7
  YoutubeLogo,
8
8
  } from "@phosphor-icons/react/ssr";
9
9
  import { parse } from "yaml";
10
+ import { ui, type UiLang } from "../lib/i18n";
10
11
 
11
12
  type EmbedData = {
12
13
  url: string;
@@ -70,23 +71,23 @@ function playableUrl(provider: EmbedProvider, url: URL) {
70
71
  return null;
71
72
  }
72
73
 
73
- const providerCopy = {
74
- youtube: { label: "YouTube", Icon: YoutubeLogo },
75
- bilibili: { label: "Bilibili", Icon: VideoCamera },
76
- x: { label: "X", Icon: XLogo },
77
- wechat: { label: "微信公众号", Icon: WechatLogo },
78
- website: { label: "网页摘要", Icon: LinkSimple },
79
- };
80
-
81
- export function ExternalEmbed({ source }: { source: string }) {
74
+ export function ExternalEmbed({ source, lang }: { source: string; lang?: UiLang }) {
75
+ const s = ui(lang);
82
76
  const data = readEmbedData(source);
83
77
  if (!data) {
84
- return <p className="external-embed-error">无法解析这个嵌入块,请检查 URL 与 YAML 格式。</p>;
78
+ return <p className="external-embed-error">{s.embedParseError}</p>;
85
79
  }
86
80
 
87
81
  const url = new URL(data.url);
88
82
  const provider = providerFor(url);
89
83
  const playerUrl = playableUrl(provider, url);
84
+ const providerCopy = {
85
+ youtube: { label: "YouTube", Icon: YoutubeLogo },
86
+ bilibili: { label: "Bilibili", Icon: VideoCamera },
87
+ x: { label: "X", Icon: XLogo },
88
+ wechat: { label: s.embedProviderWechat, Icon: WechatLogo },
89
+ website: { label: s.embedProviderWeb, Icon: LinkSimple },
90
+ };
90
91
  const { Icon, label } = providerCopy[provider];
91
92
 
92
93
  if (playerUrl) {
@@ -95,7 +96,7 @@ export function ExternalEmbed({ source }: { source: string }) {
95
96
  <div className="external-embed-frame">
96
97
  <iframe
97
98
  src={playerUrl}
98
- title={data.title ?? `${label} 嵌入内容`}
99
+ title={data.title ?? s.embedFrameTitle(label)}
99
100
  loading="lazy"
100
101
  allow="accelerometer; autoplay; encrypted-media; fullscreen; picture-in-picture"
101
102
  referrerPolicy="strict-origin-when-cross-origin"
@@ -104,7 +105,7 @@ export function ExternalEmbed({ source }: { source: string }) {
104
105
  </div>
105
106
  <figcaption>
106
107
  <span><Icon size={17} />{label}</span>
107
- <a href={data.url} target="_blank" rel="noopener noreferrer">在原网站打开<ArrowUpRight size={14} /></a>
108
+ <a href={data.url} target="_blank" rel="noopener noreferrer">{s.embedOpenOriginal}<ArrowUpRight size={14} /></a>
108
109
  </figcaption>
109
110
  </figure>
110
111
  );
@@ -118,8 +119,8 @@ export function ExternalEmbed({ source }: { source: string }) {
118
119
  )}
119
120
  <span className="external-embed-copy">
120
121
  <small><Icon size={16} />{label} · {url.hostname}</small>
121
- <strong>{data.title ?? "打开这条外部内容"}</strong>
122
- <p>{data.description ?? (provider === "x" || provider === "wechat" ? "平台不提供稳定的无脚本嵌入,Garden 会保留清晰、可访问的原文入口。" : "查看原网页中的完整内容与上下文。")}</p>
122
+ <strong>{data.title ?? s.embedFallbackTitle}</strong>
123
+ <p>{data.description ?? (provider === "x" || provider === "wechat" ? s.embedFallbackNoScript : s.embedFallbackWeb)}</p>
123
124
  </span>
124
125
  <ArrowUpRight size={21} />
125
126
  </a>
@@ -32,7 +32,15 @@ cover: writing/first-post.jpg
32
32
 
33
33
  ## 一个小标题
34
34
 
35
- 可以继续插入图片、代码、公式和 Mermaid 图表。`;
35
+ 可以继续插入图片、代码、公式、Mermaid 图表,或用 \`garden\` 围栏写组件:
36
+
37
+ \`\`\`garden list
38
+ items:
39
+ - icon: idea
40
+ text: 记下为什么重新开始写
41
+ - icon: book
42
+ text: 先把旧笔记整理成目录
43
+ \`\`\``;
36
44
 
37
45
  const thoughtSource = `---
38
46
  title: 雨后的散步
@@ -53,7 +61,7 @@ const columnSource = `columns:
53
61
  - slug: writing-system
54
62
  title: 写作系统
55
63
  description: 记录从选题到发布的完整写作过程。
56
- status: 持续更新
64
+ status: ongoing
57
65
  cover: /images/columns/writing-system.webp
58
66
  coverAlt: 桌面上的笔记本与钢笔
59
67
  posts:
@@ -9,6 +9,8 @@ import {
9
9
  HANDBOOK_SECTIONS,
10
10
  HANDBOOK_UI,
11
11
  NAV_SHORT_TITLE,
12
+ REFERENCE_SECTION,
13
+ isReferencePath,
12
14
  handbookPath,
13
15
  sectionTitle,
14
16
  type GardenDocNavItem,
@@ -152,7 +154,7 @@ export function GardenDocsNav({
152
154
  </div>
153
155
 
154
156
  <nav className="gdoc-nav" aria-label={t.chapters}>
155
- {groups.map((group) => (
157
+ {(isReferencePath(currentPath) ? [...groups].sort((a, b) => Number(b.id === REFERENCE_SECTION) - Number(a.id === REFERENCE_SECTION)) : groups).map((group) => (
156
158
  <div key={group.id} className="gdoc-nav-group">
157
159
  <h2 className="gdoc-nav-group-head">{sectionTitle(HANDBOOK_SECTIONS.find((sec) => sec.id === group.id), lang) ?? group.title}</h2>
158
160
  <ol className="gdoc-nav-list">
@@ -170,7 +172,7 @@ export function GardenDocsNav({
170
172
  {badge ? (
171
173
  <>
172
174
  <code className="gdoc-comp-pill">{badge}</code>
173
- <span className="gdoc-comp-label">{label}</span>
175
+ {group.id === REFERENCE_SECTION ? null : <span className="gdoc-comp-label">{label}</span>}
174
176
  </>
175
177
  ) : (
176
178
  <span>{label}</span>
@@ -3,6 +3,7 @@
3
3
  import { useEffect, useRef } from "react";
4
4
 
5
5
  import type { GiscusConfig } from "../lib/giscus-config";
6
+ import { ui } from "../lib/i18n";
6
7
 
7
8
  export type { GiscusConfig };
8
9
 
@@ -38,7 +39,7 @@ export function GiscusComments({
38
39
  script.dataset.emitMetadata = "0";
39
40
  script.dataset.inputPosition = "bottom";
40
41
  script.dataset.theme = currentGiscusTheme();
41
- script.dataset.lang = "zh-CN";
42
+ script.dataset.lang = ui().giscusLang;
42
43
  script.dataset.loading = "lazy";
43
44
  container.append(script);
44
45
 
@@ -8,6 +8,7 @@ import {
8
8
  MapPin,
9
9
  Users,
10
10
  } from "@phosphor-icons/react";
11
+ import { siteLang, ui } from "../lib/i18n";
11
12
 
12
13
  export type GithubPublicProfile = {
13
14
  avatar_url: string;
@@ -70,28 +71,29 @@ export function GithubLiveOverview({
70
71
  return () => controller.abort();
71
72
  }, [username]);
72
73
 
74
+ const t = ui();
73
75
  const displayName = visibleGithubText(profile.name ?? profile.login) || profile.login;
74
76
  const profileBio = profile.bio?.trim();
75
77
  const signature = statusMessage?.trim() || profileBio;
76
78
  const syncLabel = syncState === "synced"
77
- ? "本次访问已同步"
79
+ ? (siteLang === "en" ? t.githubSyncSynced : "本次访问已同步")
78
80
  : syncState === "snapshot"
79
- ? "暂用发布时快照"
80
- : "正在同步当前资料";
81
+ ? t.githubSyncSnapshot
82
+ : t.githubSyncSyncing;
81
83
 
82
84
  return (
83
85
  <>
84
86
  <header className="github-overview">
85
87
  {/* eslint-disable-next-line @next/next/no-img-element */}
86
- <img src={profile.avatar_url} alt={`${profile.login} 的 GitHub 头像`} />
88
+ <img src={profile.avatar_url} alt={t.githubAvatarAlt(profile.login)} />
87
89
  <div className="github-identity">
88
- <span className="eyebrow" aria-live="polite"><GithubLogo size={17} /> GitHub 公开资料 · {syncLabel}</span>
90
+ <span className="eyebrow" aria-live="polite"><GithubLogo size={17} />{` ${t.githubOverviewPrefix} · `}{syncLabel}</span>
89
91
  <h2>{displayName}</h2>
90
92
  {signature && <p>{signature}</p>}
91
93
  {profile.location && <span className="github-location"><MapPin size={14} />{profile.location}</span>}
92
94
  </div>
93
95
  <a href={profile.html_url} target="_blank" rel="noopener noreferrer">
94
- 查看 GitHub
96
+ {t.githubViewProfile}
95
97
  <ArrowUpRight size={16} />
96
98
  </a>
97
99
  </header>
@@ -99,7 +101,7 @@ export function GithubLiveOverview({
99
101
  <dl className="github-metrics">
100
102
  <div>
101
103
  <GithubLogo size={21} aria-hidden="true" />
102
- <dt>公开仓库</dt>
104
+ <dt>{t.githubMetricRepos}</dt>
103
105
  <dd>{syncState === "synced" || initialLive ? profile.public_repos : "—"}</dd>
104
106
  </div>
105
107
  <div>
@@ -109,7 +111,7 @@ export function GithubLiveOverview({
109
111
  </div>
110
112
  <div>
111
113
  <GitCommit size={21} aria-hidden="true" />
112
- <dt>近 30 天 commits</dt>
114
+ <dt>{t.githubMetricCommits}</dt>
113
115
  <dd>{recentCommits ?? "—"}</dd>
114
116
  </div>
115
117
  </dl>
@@ -4,7 +4,9 @@ import {
4
4
  Star,
5
5
  } from "@phosphor-icons/react/ssr";
6
6
  import { githubUsername } from "../lib/site";
7
+ import { hasRealGithubId } from "../lib/github-id.mjs";
7
8
  import { siteConfig } from "../site.config";
9
+ import { ui } from "../lib/i18n";
8
10
  import { GithubLiveOverview, type GithubPublicProfile } from "./GithubLiveOverview";
9
11
 
10
12
  type GithubRepository = {
@@ -93,26 +95,31 @@ async function loadGithubUser(token?: string) {
93
95
  }
94
96
 
95
97
  function activityFromEvent(event: GithubEvent): GithubActivity | null {
98
+ const t = ui();
96
99
  const repository = event.repo.name;
97
100
  const repositoryUrl = `https://github.com/${repository}`;
98
101
 
99
102
  if (event.type === "PushEvent") {
100
- return { id: event.id, label: "推送了代码", repository, date: event.created_at, url: `${repositoryUrl}/commits` };
103
+ return { id: event.id, label: t.githubEventPush, repository, date: event.created_at, url: `${repositoryUrl}/commits` };
101
104
  }
102
105
  if (event.type === "PullRequestEvent") {
103
- const action = event.payload.action === "closed" ? "完成了 Pull Request" : "提交了 Pull Request";
106
+ const action = event.payload.action === "closed" ? t.githubEventPrClosed : t.githubEventPrOpen;
104
107
  return { id: event.id, label: action, repository, date: event.created_at, url: event.payload.pull_request?.html_url ?? repositoryUrl };
105
108
  }
106
109
  if (event.type === "IssuesEvent") {
107
- const action = event.payload.action === "closed" ? "关闭了 Issue" : "参与了 Issue";
110
+ const action = event.payload.action === "closed" ? t.githubEventIssueClosed : t.githubEventIssueOpen;
108
111
  return { id: event.id, label: action, repository, date: event.created_at, url: event.payload.issue?.html_url ?? repositoryUrl };
109
112
  }
110
113
  if (event.type === "CreateEvent") {
111
- const target = event.payload.ref_type === "repository" ? "创建了仓库" : `创建了${event.payload.ref_type === "tag" ? "标签" : "分支"}`;
114
+ const target = event.payload.ref_type === "repository"
115
+ ? t.githubEventCreateRepo
116
+ : event.payload.ref_type === "tag"
117
+ ? t.githubEventCreateTag
118
+ : t.githubEventCreateBranch;
112
119
  return { id: event.id, label: target, repository, date: event.created_at, url: repositoryUrl };
113
120
  }
114
121
  if (event.type === "ReleaseEvent") {
115
- return { id: event.id, label: "发布了新版本", repository, date: event.created_at, url: event.payload.release?.html_url ?? `${repositoryUrl}/releases` };
122
+ return { id: event.id, label: t.githubEventRelease, repository, date: event.created_at, url: event.payload.release?.html_url ?? `${repositoryUrl}/releases` };
116
123
  }
117
124
  return null;
118
125
  }
@@ -220,7 +227,7 @@ async function loadGithubGraph(token?: string) {
220
227
  }
221
228
 
222
229
  function formatActivityDate(value: string) {
223
- return new Intl.DateTimeFormat("zh-CN", { month: "2-digit", day: "2-digit" }).format(new Date(value));
230
+ return new Intl.DateTimeFormat(ui().dateLocale, { month: "2-digit", day: "2-digit" }).format(new Date(value));
224
231
  }
225
232
 
226
233
  function contributionLevelClass(level: string) {
@@ -228,6 +235,9 @@ function contributionLevelClass(level: string) {
228
235
  }
229
236
 
230
237
  export async function GithubProfile() {
238
+ // A new site still has the template id (or none): render the page without a profile and never call the API.
239
+ if (!hasRealGithubId(githubUsername)) return null;
240
+ const t = ui();
231
241
  const token = process.env.GITHUB_TOKEN;
232
242
  const [{ profile, live }, graph, activity] = await Promise.all([
233
243
  loadGithubUser(token),
@@ -240,7 +250,7 @@ export async function GithubProfile() {
240
250
  const calendarDays = graph?.year.contributionCalendar.weeks.flatMap((week) => week.contributionDays) ?? [];
241
251
 
242
252
  return (
243
- <section className="github-profile" aria-label="GitHub 资料">
253
+ <section className="github-profile" aria-label={t.githubProfileAria}>
244
254
  <GithubLiveOverview
245
255
  username={githubUsername}
246
256
  initialProfile={profile}
@@ -252,8 +262,8 @@ export async function GithubProfile() {
252
262
  <section className="github-pinned" aria-labelledby="github-pinned-title">
253
263
  <header>
254
264
  <div>
255
- <p className="eyebrow">PINNED / 置顶仓库</p>
256
- <h3 id="github-pinned-title">正在做的项目</h3>
265
+ <p className="eyebrow">{t.githubPinnedEyebrow}</p>
266
+ <h3 id="github-pinned-title">{t.githubPinnedHeading}</h3>
257
267
  </div>
258
268
  <PushPin size={20} aria-hidden="true" />
259
269
  </header>
@@ -262,7 +272,7 @@ export async function GithubProfile() {
262
272
  {pinned.map((repository) => (
263
273
  <a href={repository.url} target="_blank" rel="noopener noreferrer" key={repository.nameWithOwner}>
264
274
  <strong>{repository.nameWithOwner.split("/").at(-1)}</strong>
265
- <p>{repository.description ?? "查看这个公开仓库的代码与最近更新。"}</p>
275
+ <p>{repository.description ?? t.githubPinnedFallbackDesc}</p>
266
276
  <span>
267
277
  {repository.primaryLanguage && <><i style={{ background: repository.primaryLanguage.color ?? undefined }} />{repository.primaryLanguage.name}</>}
268
278
  <small><Star size={13} weight="fill" />{repository.stargazerCount}</small>
@@ -271,38 +281,38 @@ export async function GithubProfile() {
271
281
  ))}
272
282
  </div>
273
283
  ) : (
274
- <p className="github-data-note">配置 GitHub Token 或 author.githubPinned 后,这里会展示置顶仓库。</p>
284
+ <p className="github-data-note">{t.githubPinnedEmpty}</p>
275
285
  )}
276
286
  </section>
277
287
 
278
288
  <section className="github-contributions" aria-labelledby="github-contributions-title">
279
289
  <header>
280
290
  <div>
281
- <p className="eyebrow">CONTRIBUTIONS / 贡献</p>
282
- <h3 id="github-contributions-title">过去一年的公开贡献</h3>
291
+ <p className="eyebrow">{t.githubContribEyebrow}</p>
292
+ <h3 id="github-contributions-title">{t.githubContribHeading}</h3>
283
293
  </div>
284
- <strong>{graph ? `${graph.year.contributionCalendar.totalContributions} 次贡献` : "需要 GitHub Token"}</strong>
294
+ <strong>{graph ? t.githubContribCount(graph.year.contributionCalendar.totalContributions) : t.githubContribNeedToken}</strong>
285
295
  </header>
286
296
  {calendarDays.length > 0 ? (
287
- <div className="github-calendar" role="img" aria-label={`过去一年 ${graph?.year.contributionCalendar.totalContributions ?? 0} 次公开贡献`}>
297
+ <div className="github-calendar" role="img" aria-label={t.githubContribAria(graph?.year.contributionCalendar.totalContributions ?? 0)}>
288
298
  {calendarDays.map((day) => (
289
299
  <span
290
300
  className={`github-contribution-day level-${contributionLevelClass(day.contributionLevel)}`}
291
301
  key={day.date}
292
- title={`${day.date} · ${day.contributionCount} 次贡献`}
293
- aria-label={`${day.date},${day.contributionCount} 次贡献`}
302
+ title={t.githubContribDayTitle(day.date, day.contributionCount)}
303
+ aria-label={t.githubContribDayAria(day.date, day.contributionCount)}
294
304
  />
295
305
  ))}
296
306
  </div>
297
307
  ) : (
298
- <p className="github-data-note">正式发布时会使用工作流的只读令牌生成贡献热点图。</p>
308
+ <p className="github-data-note">{t.githubContribEmpty}</p>
299
309
  )}
300
310
  </section>
301
311
 
302
312
  <section className="github-activity" aria-labelledby="github-activity-title">
303
313
  <header>
304
- <p className="eyebrow">CONTRIBUTION ACTIVITY / 最近活动</p>
305
- <h3 id="github-activity-title">最近在 GitHub 上做了什么</h3>
314
+ <p className="eyebrow">{t.githubActivityEyebrow}</p>
315
+ <h3 id="github-activity-title">{t.githubActivityHeading}</h3>
306
316
  </header>
307
317
  {activity.length > 0 ? (
308
318
  <ol>
@@ -318,7 +328,7 @@ export async function GithubProfile() {
318
328
  ))}
319
329
  </ol>
320
330
  ) : (
321
- <p className="github-data-note">GitHub 暂时没有返回最近的公开贡献活动。</p>
331
+ <p className="github-data-note">{t.githubActivityEmpty}</p>
322
332
  )}
323
333
  </section>
324
334
  </section>
@@ -5,6 +5,8 @@ import { usePathname } from "next/navigation";
5
5
  import { HANDBOOK_LANG_KEY, handbookBase, type HandbookLang } from "../lib/garden-docs-meta";
6
6
  import { siteHref } from "../lib/site-href";
7
7
  import { swapHandbookLang } from "../lib/handbook-path";
8
+ import { siteLang } from "../lib/i18n";
9
+ import { handbookPageLang, handbookSwitchLabel, handbookSwitchTarget, handbookSwitchTitle } from "../lib/handbook-switch.mjs";
8
10
 
9
11
  const noSubscribe = () => () => {};
10
12
 
@@ -12,14 +14,16 @@ const noSubscribe = () => () => {};
12
14
  function counterpartHref(target: HandbookLang) {
13
15
  const link = document.querySelector<HTMLLinkElement>(`link[rel="alternate"][hreflang="${target === "en" ? "en" : "zh-CN"}"]`);
14
16
  if (link?.href) return swapHandbookLang(location.pathname, target);
17
+ // English sites leave hreflang out of <head>; the handbook ships every page in both languages there.
18
+ if (siteLang === "en") return swapHandbookLang(location.pathname, target);
15
19
  return target === "zh" ? siteHref(`${handbookBase("zh")}/`) : null;
16
20
  }
17
21
 
18
22
  /** 「中 / EN」 in the top bar on handbook pages. Goes to the counterpart page. */
19
23
  export function HandbookLangSwitch() {
20
24
  const pathname = usePathname() ?? "";
21
- const lang: HandbookLang = /\/garden\/en(\/|$)/.test(pathname) ? "en" : "zh";
22
- const target: HandbookLang = lang === "en" ? "zh" : "en";
25
+ const lang: HandbookLang = handbookPageLang(pathname);
26
+ const target: HandbookLang = handbookSwitchTarget(lang);
23
27
  const href = useSyncExternalStore(
24
28
  noSubscribe,
25
29
  () => counterpartHref(target),
@@ -39,10 +43,10 @@ export function HandbookLangSwitch() {
39
43
  </>
40
44
  );
41
45
  if (!href) {
42
- return <span className="sh-lang is-disabled" title="No English version of this page yet">{label}</span>;
46
+ return <span className="sh-lang is-disabled" title={handbookSwitchTitle(lang, siteLang)}>{label}</span>;
43
47
  }
44
48
  return (
45
- <a className="sh-lang" href={href} hrefLang={target === "en" ? "en" : "zh-CN"} lang={target === "en" ? "en" : undefined} onClick={remember} aria-label={target === "en" ? "Read in English" : "Switch to Chinese"}>
49
+ <a className="sh-lang" href={href} hrefLang={target === "en" ? "en" : "zh-CN"} lang={target === "en" ? "en" : undefined} onClick={remember} aria-label={handbookSwitchLabel(lang)}>
46
50
  {label}
47
51
  </a>
48
52
  );