hd-garden 0.7.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 (220) hide show
  1. package/.agents/skills/garden-ui/SKILL.md +26 -19
  2. package/.env.en.example +67 -0
  3. package/README.en.md +57 -0
  4. package/README.md +31 -46
  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 +14 -5
  15. package/app/blog/page.tsx +4 -2
  16. package/app/columns/[column]/[...path]/page.tsx +17 -18
  17. package/app/columns/page.tsx +12 -8
  18. package/app/components/AdminUiShell.tsx +4 -3
  19. package/app/components/ArticlePager.tsx +32 -0
  20. package/app/components/ArticleReferences.tsx +4 -1
  21. package/app/components/ArticleTags.tsx +3 -2
  22. package/app/components/BlogArchive.tsx +20 -16
  23. package/app/components/Comments.tsx +17 -10
  24. package/app/components/ContentEmptyState.tsx +29 -28
  25. package/app/components/ExternalEmbed.tsx +15 -14
  26. package/app/components/FeedMore.tsx +70 -0
  27. package/app/components/GardenContentGuide.tsx +10 -2
  28. package/app/components/GardenDocsNav.tsx +29 -22
  29. package/app/components/GiscusComments.tsx +2 -1
  30. package/app/components/GithubLiveOverview.tsx +10 -8
  31. package/app/components/GithubProfile.tsx +31 -21
  32. package/app/components/HandbookLangRedirect.tsx +30 -0
  33. package/app/components/HandbookLangSwitch.tsx +53 -0
  34. package/app/components/HandbookView.tsx +270 -0
  35. package/app/components/MarkdownArticle.tsx +39 -9
  36. package/app/components/MarkdownImage.tsx +6 -4
  37. package/app/components/SiteChrome.tsx +11 -2
  38. package/app/components/SiteFooter.tsx +6 -4
  39. package/app/components/SiteHeader.tsx +38 -25
  40. package/app/components/TableOfContents.tsx +15 -9
  41. package/app/components/ThoughtCard.tsx +16 -14
  42. package/app/components/ThoughtFeed.tsx +41 -0
  43. package/app/components/UiBlock.tsx +3 -2
  44. package/app/components/UiLang.tsx +18 -0
  45. package/app/components/Webmentions.tsx +13 -12
  46. package/app/components/WidgetChart.tsx +8 -6
  47. package/app/components/garden/FilePreview.tsx +14 -12
  48. package/app/components/garden/GardenBlock.tsx +10 -1
  49. package/app/components/garden/GardenFile.tsx +10 -8
  50. package/app/components/garden/RefDemo.tsx +62 -0
  51. package/app/components/garden/blocks.tsx +90 -32
  52. package/app/components/garden/chrome.tsx +20 -13
  53. package/app/components/garden/patterns.tsx +497 -0
  54. package/app/garden/[...slug]/page.tsx +11 -130
  55. package/app/garden/en/[...slug]/page.tsx +28 -0
  56. package/app/garden/en/page.tsx +16 -0
  57. package/app/garden/page.tsx +8 -74
  58. package/app/garden-modules.d.ts +3 -0
  59. package/app/globals.css +10 -0
  60. package/app/layout.tsx +2 -4
  61. package/app/lib/a2-meta.ts +26 -3
  62. package/app/lib/content.ts +13 -3
  63. package/app/lib/garden-docs-meta.ts +84 -5
  64. package/app/lib/github-id.mjs +8 -0
  65. package/app/lib/handbook-path.ts +11 -0
  66. package/app/lib/handbook-switch.mjs +12 -0
  67. package/app/lib/i18n.ts +621 -0
  68. package/app/lib/page-alternates.ts +2 -2
  69. package/app/lib/rehype-field-tables.mjs +47 -0
  70. package/app/lib/remark-a2.mjs +18 -8
  71. package/app/lib/remark-link-card.mjs +22 -0
  72. package/app/lib/remark-ref-demos.mjs +42 -0
  73. package/app/lib/tag-filter.ts +9 -2
  74. package/app/not-found.tsx +34 -4
  75. package/app/page.tsx +17 -14
  76. package/app/rss.xml/route.ts +2 -1
  77. package/app/site.config.ts +1 -1
  78. package/app/styles/a2.css +439 -32
  79. package/app/styles/shell.css +21 -2
  80. package/app/tags/page.tsx +7 -5
  81. package/app/thoughts/[slug]/page.tsx +2 -1
  82. package/app/thoughts/page/[n]/page.tsx +44 -0
  83. package/app/thoughts/page.tsx +4 -7
  84. package/app/ui-parity/page.tsx +38 -22
  85. package/bin/garden.mjs +133 -19
  86. package/docs/handbook/ai-writing.md +99 -0
  87. package/docs/handbook/assets/reading-layout-en.svg +26 -0
  88. package/docs/handbook/components/card.md +43 -20
  89. package/docs/handbook/components/chart.md +10 -24
  90. package/docs/handbook/components/choice.md +42 -0
  91. package/docs/handbook/components/compare.md +34 -21
  92. package/docs/handbook/components/figure.md +33 -44
  93. package/docs/handbook/components/file.md +10 -13
  94. package/docs/handbook/components/html.md +10 -28
  95. package/docs/handbook/components/interactive.md +9 -33
  96. package/docs/handbook/components/list.md +65 -0
  97. package/docs/handbook/components/relations.md +95 -0
  98. package/docs/handbook/components/stat.md +10 -26
  99. package/docs/handbook/components/timeline.md +13 -26
  100. package/docs/handbook/components.md +210 -19
  101. package/docs/handbook/config.md +7 -5
  102. package/docs/handbook/deploy.md +2 -2
  103. package/docs/handbook/design.md +23 -15
  104. package/docs/handbook/en/ai-writing.md +99 -0
  105. package/docs/handbook/en/components/card.md +67 -0
  106. package/docs/handbook/en/components/chart.md +38 -0
  107. package/docs/handbook/en/components/choice.md +42 -0
  108. package/docs/handbook/en/components/compare.md +53 -0
  109. package/docs/handbook/en/components/figure.md +59 -0
  110. package/docs/handbook/en/components/file/checklist.md +17 -0
  111. package/docs/handbook/en/components/file.md +29 -0
  112. package/docs/handbook/en/components/html.md +49 -0
  113. package/docs/handbook/en/components/interactive.md +41 -0
  114. package/docs/handbook/en/components/list.md +65 -0
  115. package/docs/handbook/en/components/relations.md +95 -0
  116. package/docs/handbook/en/components/stat.md +39 -0
  117. package/docs/handbook/en/components/timeline.md +37 -0
  118. package/docs/handbook/en/components.md +222 -0
  119. package/docs/handbook/en/config.md +127 -0
  120. package/docs/handbook/en/deploy.md +273 -0
  121. package/docs/handbook/en/design.md +88 -0
  122. package/docs/handbook/en/index.md +192 -0
  123. package/docs/handbook/en/reference/card.md +85 -0
  124. package/docs/handbook/en/reference/chart.md +68 -0
  125. package/docs/handbook/en/reference/choice.md +69 -0
  126. package/docs/handbook/en/reference/compare.md +59 -0
  127. package/docs/handbook/en/reference/figure.md +84 -0
  128. package/docs/handbook/en/reference/file/checklist.md +17 -0
  129. package/docs/handbook/en/reference/file.md +50 -0
  130. package/docs/handbook/en/reference/html.md +62 -0
  131. package/docs/handbook/en/reference/interactive.md +71 -0
  132. package/docs/handbook/en/reference/list.md +72 -0
  133. package/docs/handbook/en/reference/relations.md +113 -0
  134. package/docs/handbook/en/reference/stat.md +60 -0
  135. package/docs/handbook/en/reference/timeline.md +56 -0
  136. package/docs/handbook/en/reference.md +26 -0
  137. package/docs/handbook/en/start.md +92 -0
  138. package/docs/handbook/en/workflow.md +82 -0
  139. package/docs/handbook/en/writing.md +157 -0
  140. package/docs/handbook/index.md +99 -4
  141. package/docs/handbook/reference/card.md +85 -0
  142. package/docs/handbook/reference/chart.md +68 -0
  143. package/docs/handbook/reference/choice.md +69 -0
  144. package/docs/handbook/reference/compare.md +59 -0
  145. package/docs/handbook/reference/figure.md +84 -0
  146. package/docs/handbook/reference/file/checklist.md +17 -0
  147. package/docs/handbook/reference/file.md +50 -0
  148. package/docs/handbook/reference/html.md +62 -0
  149. package/docs/handbook/reference/interactive.md +71 -0
  150. package/docs/handbook/reference/list.md +72 -0
  151. package/docs/handbook/reference/relations.md +113 -0
  152. package/docs/handbook/reference/stat.md +60 -0
  153. package/docs/handbook/reference/timeline.md +56 -0
  154. package/docs/handbook/reference.md +26 -0
  155. package/docs/handbook/start.md +1 -1
  156. package/docs/handbook/workflow.md +6 -5
  157. package/docs/handbook/writing.md +21 -12
  158. package/next.config.ts +6 -0
  159. package/package.json +2 -1
  160. package/public/images/garden/ref-card-book.svg +1 -0
  161. package/public/images/garden/ref-card-lamp.svg +1 -0
  162. package/scripts/build-content.mjs +57 -16
  163. package/scripts/favicon-ico.mjs +9 -6
  164. package/scripts/file-preview.mjs +10 -7
  165. package/scripts/handbook-allowlist.mjs +18 -0
  166. package/scripts/handbook-facts.mjs +339 -11
  167. package/scripts/html-lang.mjs +36 -0
  168. package/scripts/link-assets.mjs +7 -0
  169. package/scripts/new-post.mjs +18 -9
  170. package/scripts/prepare-pages-output.mjs +30 -18
  171. package/scripts/redirects.mjs +17 -6
  172. package/scripts/static-404.mjs +25 -0
  173. package/scripts/ui-cli.mjs +14 -8
  174. package/templates/garden-blocks.md +67 -0
  175. package/templates/garden-snippets.plist +61 -0
  176. package/templates/site/Makefile.en +45 -0
  177. package/templates/site/README.en.md +15 -0
  178. package/templates/site/site.config.en.yaml +55 -0
  179. package/templates/site/site.config.yaml +2 -0
  180. package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
  181. package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
  182. package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
  183. package/templates/skills-en/garden-post-review/SKILL.md +63 -0
  184. package/templates/skills-en/garden-ui/SKILL.md +118 -0
  185. package/templates/workflows/pages.en.yml +195 -0
  186. package/templates/workflows/pages.yml +195 -0
  187. package/ui/core/apply.mjs +22 -4
  188. package/ui/core/cli-locale.mjs +67 -0
  189. package/ui/core/fence.mjs +18 -3
  190. package/ui/core/fix-prompt.mjs +18 -15
  191. package/ui/core/garden-fence.mjs +64 -0
  192. package/ui/core/scan.mjs +19 -3
  193. package/ui/garden/adjacent.mjs +21 -0
  194. package/ui/garden/catalog.mjs +205 -4
  195. package/ui/garden/check.mjs +12 -0
  196. package/ui/garden/file-block.mjs +8 -7
  197. package/ui/garden/html-lang.mjs +88 -0
  198. package/ui/garden/markdown-links.mjs +7 -4
  199. package/ui/garden/paging.mjs +30 -0
  200. package/ui/garden/parse.mjs +45 -0
  201. package/ui/garden/relations.mjs +252 -0
  202. package/ui/garden/search-index.mjs +6 -4
  203. package/ui/garden/source-icon.mjs +54 -0
  204. package/ui/parity-fixtures.mjs +77 -0
  205. package/ui/preview/dist/garden-ui.css +9 -9
  206. package/ui/preview/dist/garden-ui.esm.js +206 -169
  207. package/ui/preview/dist/garden-ui.iife.js +206 -169
  208. package/ui/prompt/rules.md +28 -1
  209. package/ui/react/components/chart.tsx +8 -5
  210. package/ui/react/components/display.tsx +7 -4
  211. package/ui/react/components/inputs.tsx +6 -3
  212. package/ui/react/components/layout.tsx +6 -4
  213. package/ui/react/components/media.tsx +7 -4
  214. package/ui/react/components/sandbox.tsx +3 -1
  215. package/ui/react/index.tsx +11 -7
  216. package/ui/react/lang.tsx +89 -0
  217. package/ui/react/parity.tsx +8 -6
  218. package/ui/react/runtime.tsx +3 -1
  219. package/ui/react/ui.css +9 -9
  220. package/worker/index.ts +14 -1
@@ -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:
@@ -6,18 +6,28 @@ import { BookOpenText, CaretRight, List, X } from "@phosphor-icons/react";
6
6
  import {
7
7
  COMPONENT_BADGE,
8
8
  groupHandbookDocs,
9
+ HANDBOOK_SECTIONS,
10
+ HANDBOOK_UI,
9
11
  NAV_SHORT_TITLE,
12
+ REFERENCE_SECTION,
13
+ isReferencePath,
14
+ handbookPath,
15
+ sectionTitle,
10
16
  type GardenDocNavItem,
17
+ type HandbookLang,
11
18
  } from "../lib/garden-docs-meta";
12
19
  import { siteHref } from "../lib/site-href";
13
20
 
14
21
  export function GardenDocsNav({
15
22
  docs,
16
23
  currentPath = "",
24
+ lang = "zh",
17
25
  }: {
18
26
  docs: GardenDocNavItem[];
19
27
  currentPath?: string;
28
+ lang?: HandbookLang;
20
29
  }) {
30
+ const t = HANDBOOK_UI[lang];
21
31
  const groups = groupHandbookDocs(docs);
22
32
  const currentDoc = docs.find((doc) => doc.path === currentPath);
23
33
  const currentSection = groups.find((group) =>
@@ -69,7 +79,7 @@ export function GardenDocsNav({
69
79
 
70
80
  return (
71
81
  <>
72
- <div className="gdoc-mobile-bar" aria-label="说明书目录栏">
82
+ <div className="gdoc-mobile-bar" aria-label={t.mobileBar}>
73
83
  <button
74
84
  ref={triggerRef}
75
85
  type="button"
@@ -79,28 +89,28 @@ export function GardenDocsNav({
79
89
  onClick={() => setMobileOpen(true)}
80
90
  >
81
91
  <List size={16} weight="bold" aria-hidden="true" />
82
- <span>说明目录</span>
92
+ <span>{t.sidebarShort}</span>
83
93
  </button>
84
94
  <a
85
95
  className="gdoc-nojs-trigger"
86
96
  id={`${drawerId}-open`}
87
97
  href={`#${drawerId}`}
88
- aria-label="打开说明目录"
98
+ aria-label={t.openSidebar}
89
99
  >
90
100
  <List size={16} weight="bold" aria-hidden="true" />
91
- <span>说明目录</span>
101
+ <span>{t.sidebarShort}</span>
92
102
  </a>
93
103
  <div className="gdoc-mobile-crumb" aria-hidden="true">
94
- <span>{currentSection?.title ?? "Garden 使用说明"}</span>
104
+ <span>{sectionTitle(currentSection, lang) ?? t.name}</span>
95
105
  <CaretRight size={12} />
96
- <strong>{currentDoc ? (NAV_SHORT_TITLE[currentDoc.path] ?? currentDoc.title) : "总览与快速入口"}</strong>
106
+ <strong>{currentDoc ? (lang === "en" ? currentDoc.navTitle ?? currentDoc.title : NAV_SHORT_TITLE[currentDoc.path] ?? currentDoc.title) : t.overviewHint}</strong>
97
107
  </div>
98
108
  </div>
99
109
 
100
110
  <button
101
111
  type="button"
102
112
  className="gdoc-backdrop"
103
- aria-label="关闭说明目录"
113
+ aria-label={t.closeSidebar}
104
114
  tabIndex={mobileOpen ? 0 : -1}
105
115
  data-open={mobileOpen}
106
116
  onClick={() => closeMobile(true)}
@@ -109,27 +119,27 @@ export function GardenDocsNav({
109
119
  <aside
110
120
  id={drawerId}
111
121
  className="gdoc-sidebar"
112
- aria-label="Garden 使用说明目录"
122
+ aria-label={t.sidebar}
113
123
  data-open={mobileOpen}
114
124
  >
115
125
  <div className="gdoc-sidebar-head">
116
126
  <Link
117
- href={siteHref("/garden")}
127
+ href={siteHref(handbookPath(lang))}
118
128
  className="gdoc-overview-link"
119
129
  aria-current={isOverview ? "page" : undefined}
120
130
  onClick={() => closeMobile(false)}
121
131
  >
122
132
  <span className="gdoc-overview-mark">
123
133
  <BookOpenText size={16} weight="duotone" aria-hidden="true" />
124
- <span>Garden 使用说明</span>
134
+ <span>{t.name}</span>
125
135
  </span>
126
- <small>总览与快速入口</small>
136
+ <small>{t.overviewHint}</small>
127
137
  </Link>
128
138
  <button
129
139
  ref={closeRef}
130
140
  type="button"
131
141
  className="gdoc-mobile-close"
132
- aria-label="关闭说明目录"
142
+ aria-label={t.closeSidebar}
133
143
  onClick={() => closeMobile(true)}
134
144
  >
135
145
  <X size={17} aria-hidden="true" />
@@ -137,35 +147,32 @@ export function GardenDocsNav({
137
147
  <a
138
148
  className="gdoc-nojs-close"
139
149
  href={`#${drawerId}-open`}
140
- aria-label="关闭说明目录"
150
+ aria-label={t.closeSidebar}
141
151
  >
142
152
  <X size={17} aria-hidden="true" />
143
153
  </a>
144
154
  </div>
145
155
 
146
- <nav className="gdoc-nav" aria-label="章节导航">
147
- {groups.map((group) => (
156
+ <nav className="gdoc-nav" aria-label={t.chapters}>
157
+ {(isReferencePath(currentPath) ? [...groups].sort((a, b) => Number(b.id === REFERENCE_SECTION) - Number(a.id === REFERENCE_SECTION)) : groups).map((group) => (
148
158
  <div key={group.id} className="gdoc-nav-group">
149
- <div className="gdoc-nav-group-head">
150
- <h2>{group.title}</h2>
151
- {group.note ? <span>{group.note}</span> : null}
152
- </div>
159
+ <h2 className="gdoc-nav-group-head">{sectionTitle(HANDBOOK_SECTIONS.find((sec) => sec.id === group.id), lang) ?? group.title}</h2>
153
160
  <ol className="gdoc-nav-list">
154
161
  {group.items.map((item) => {
155
162
  const active = item.path === currentPath;
156
163
  const badge = COMPONENT_BADGE[item.path];
157
- const label = NAV_SHORT_TITLE[item.path] ?? item.title;
164
+ const label = lang === "en" ? item.navTitle ?? item.title : NAV_SHORT_TITLE[item.path] ?? item.title;
158
165
  return (
159
166
  <li key={item.path} className={active ? "is-active" : undefined}>
160
167
  <Link
161
- href={siteHref(`/garden/${item.path}`)}
168
+ href={siteHref(handbookPath(lang, item.path))}
162
169
  aria-current={active ? "page" : undefined}
163
170
  onClick={() => closeMobile(false)}
164
171
  >
165
172
  {badge ? (
166
173
  <>
167
174
  <code className="gdoc-comp-pill">{badge}</code>
168
- <span className="gdoc-comp-label">{label}</span>
175
+ {group.id === REFERENCE_SECTION ? null : <span className="gdoc-comp-label">{label}</span>}
169
176
  </>
170
177
  ) : (
171
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>
@@ -0,0 +1,30 @@
1
+ "use client";
2
+
3
+ import { useEffect } from "react";
4
+ import { HANDBOOK_LANG_KEY, type HandbookLang } from "../lib/garden-docs-meta";
5
+ import { swapHandbookLang } from "../lib/handbook-path";
6
+
7
+ /**
8
+ * First visit: follow the browser language. Afterwards: follow the reader's
9
+ * saved choice (set by the 中 / EN switch). Only moves to a page that exists,
10
+ * which the hreflang link in <head> tells us.
11
+ */
12
+ export function HandbookLangRedirect({ lang }: { lang: HandbookLang }) {
13
+ useEffect(() => {
14
+ let saved: string | null = null;
15
+ try {
16
+ saved = localStorage.getItem(HANDBOOK_LANG_KEY);
17
+ } catch {}
18
+ const wanted: HandbookLang = saved === "en" || saved === "zh"
19
+ ? saved
20
+ : /^en\b/i.test(navigator.language || "") ? "en" : "zh";
21
+ if (wanted === lang) return;
22
+ const link = document.querySelector<HTMLLinkElement>(`link[rel="alternate"][hreflang="${wanted === "en" ? "en" : "zh-CN"}"]`);
23
+ if (!link?.href) return;
24
+ // The hreflang link says the page exists; the address comes from this one,
25
+ // so the origin and any basePath stay as they are.
26
+ const pathname = swapHandbookLang(location.pathname, wanted);
27
+ if (pathname && pathname !== location.pathname) location.replace(pathname + location.search + location.hash);
28
+ }, [lang]);
29
+ return null;
30
+ }
@@ -0,0 +1,53 @@
1
+ "use client";
2
+
3
+ import { useSyncExternalStore } from "react";
4
+ import { usePathname } from "next/navigation";
5
+ import { HANDBOOK_LANG_KEY, handbookBase, type HandbookLang } from "../lib/garden-docs-meta";
6
+ import { siteHref } from "../lib/site-href";
7
+ import { swapHandbookLang } from "../lib/handbook-path";
8
+ import { siteLang } from "../lib/i18n";
9
+ import { handbookPageLang, handbookSwitchLabel, handbookSwitchTarget, handbookSwitchTitle } from "../lib/handbook-switch.mjs";
10
+
11
+ const noSubscribe = () => () => {};
12
+
13
+ /** The counterpart page from the hreflang link in <head>, on this origin. */
14
+ function counterpartHref(target: HandbookLang) {
15
+ const link = document.querySelector<HTMLLinkElement>(`link[rel="alternate"][hreflang="${target === "en" ? "en" : "zh-CN"}"]`);
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);
19
+ return target === "zh" ? siteHref(`${handbookBase("zh")}/`) : null;
20
+ }
21
+
22
+ /** 「中 / EN」 in the top bar on handbook pages. Goes to the counterpart page. */
23
+ export function HandbookLangSwitch() {
24
+ const pathname = usePathname() ?? "";
25
+ const lang: HandbookLang = handbookPageLang(pathname);
26
+ const target: HandbookLang = handbookSwitchTarget(lang);
27
+ const href = useSyncExternalStore(
28
+ noSubscribe,
29
+ () => counterpartHref(target),
30
+ () => null,
31
+ );
32
+
33
+ const remember = () => {
34
+ try {
35
+ localStorage.setItem(HANDBOOK_LANG_KEY, target);
36
+ } catch {}
37
+ };
38
+ const label = (
39
+ <>
40
+ <span aria-current={lang === "zh" ? "true" : undefined}>中</span>
41
+ <span aria-hidden="true"> / </span>
42
+ <span aria-current={lang === "en" ? "true" : undefined}>EN</span>
43
+ </>
44
+ );
45
+ if (!href) {
46
+ return <span className="sh-lang is-disabled" title={handbookSwitchTitle(lang, siteLang)}>{label}</span>;
47
+ }
48
+ return (
49
+ <a className="sh-lang" href={href} hrefLang={target === "en" ? "en" : "zh-CN"} lang={target === "en" ? "en" : undefined} onClick={remember} aria-label={handbookSwitchLabel(lang)}>
50
+ {label}
51
+ </a>
52
+ );
53
+ }