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.
Files changed (187) 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/ArticleMeasure.tsx +6 -3
  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 +16 -10
  24. package/app/components/ContentEmptyState.tsx +29 -28
  25. package/app/components/ExternalEmbed.tsx +15 -14
  26. package/app/components/GardenContentGuide.tsx +10 -2
  27. package/app/components/GardenDocsNav.tsx +4 -2
  28. package/app/components/GiscusComments.tsx +2 -1
  29. package/app/components/GithubLiveOverview.tsx +10 -8
  30. package/app/components/GithubProfile.tsx +31 -21
  31. package/app/components/HandbookLangSwitch.tsx +8 -4
  32. package/app/components/HandbookView.tsx +92 -9
  33. package/app/components/MarkdownArticle.tsx +31 -7
  34. package/app/components/MermaidDiagram.tsx +4 -3
  35. package/app/components/SiteChrome.tsx +3 -3
  36. package/app/components/SiteFooter.tsx +1 -1
  37. package/app/components/SiteHeader.tsx +13 -4
  38. package/app/components/ThoughtCard.tsx +16 -14
  39. package/app/components/UiBlock.tsx +3 -2
  40. package/app/components/Webmentions.tsx +11 -11
  41. package/app/components/WidgetChart.tsx +8 -6
  42. package/app/components/garden/FilePreview.tsx +2 -2
  43. package/app/components/garden/GardenBlock.tsx +7 -0
  44. package/app/components/garden/GardenFile.tsx +2 -2
  45. package/app/components/garden/RefDemo.tsx +62 -0
  46. package/app/components/garden/blocks.tsx +56 -8
  47. package/app/components/garden/patterns.tsx +497 -0
  48. package/app/garden/[...slug]/page.tsx +3 -2
  49. package/app/garden/en/page.tsx +7 -7
  50. package/app/garden/page.tsx +3 -2
  51. package/app/globals.css +49 -1
  52. package/app/lib/a2-meta.ts +26 -3
  53. package/app/lib/content.ts +2 -2
  54. package/app/lib/garden-docs-meta.ts +24 -0
  55. package/app/lib/github-id.mjs +8 -0
  56. package/app/lib/handbook-switch.mjs +12 -0
  57. package/app/lib/i18n.ts +401 -2
  58. package/app/lib/rehype-field-tables.mjs +47 -0
  59. package/app/lib/remark-a2.mjs +18 -8
  60. package/app/lib/remark-link-card.mjs +22 -0
  61. package/app/lib/remark-ref-demos.mjs +42 -0
  62. package/app/lib/tag-filter.ts +9 -2
  63. package/app/not-found.tsx +32 -4
  64. package/app/page.tsx +17 -14
  65. package/app/rss.xml/route.ts +2 -1
  66. package/app/styles/a2.css +338 -0
  67. package/app/styles/shell.css +15 -2
  68. package/app/tags/page.tsx +7 -5
  69. package/app/thoughts/[slug]/page.tsx +2 -1
  70. package/app/thoughts/page/[n]/page.tsx +1 -1
  71. package/app/thoughts/page.tsx +2 -1
  72. package/app/ui-parity/page.tsx +38 -22
  73. package/bin/garden.mjs +129 -19
  74. package/docs/handbook/ai-writing.md +2 -2
  75. package/docs/handbook/assets/reading-layout-en.svg +26 -0
  76. package/docs/handbook/components/card.md +43 -22
  77. package/docs/handbook/components/chart.md +10 -26
  78. package/docs/handbook/components/choice.md +42 -0
  79. package/docs/handbook/components/compare.md +32 -17
  80. package/docs/handbook/components/figure.md +33 -46
  81. package/docs/handbook/components/file.md +10 -15
  82. package/docs/handbook/components/html.md +10 -30
  83. package/docs/handbook/components/interactive.md +9 -35
  84. package/docs/handbook/components/list.md +65 -0
  85. package/docs/handbook/components/relations.md +95 -0
  86. package/docs/handbook/components/stat.md +10 -28
  87. package/docs/handbook/components/timeline.md +13 -28
  88. package/docs/handbook/components.md +210 -19
  89. package/docs/handbook/config.md +5 -3
  90. package/docs/handbook/deploy.md +1 -1
  91. package/docs/handbook/design.md +20 -12
  92. package/docs/handbook/en/ai-writing.md +2 -2
  93. package/docs/handbook/en/components/card.md +42 -21
  94. package/docs/handbook/en/components/chart.md +10 -26
  95. package/docs/handbook/en/components/choice.md +42 -0
  96. package/docs/handbook/en/components/compare.md +30 -15
  97. package/docs/handbook/en/components/figure.md +29 -42
  98. package/docs/handbook/en/components/file.md +9 -14
  99. package/docs/handbook/en/components/html.md +9 -29
  100. package/docs/handbook/en/components/interactive.md +8 -34
  101. package/docs/handbook/en/components/list.md +65 -0
  102. package/docs/handbook/en/components/relations.md +95 -0
  103. package/docs/handbook/en/components/stat.md +9 -27
  104. package/docs/handbook/en/components/timeline.md +9 -24
  105. package/docs/handbook/en/components.md +211 -19
  106. package/docs/handbook/en/config.md +32 -30
  107. package/docs/handbook/en/deploy.md +12 -12
  108. package/docs/handbook/en/design.md +19 -12
  109. package/docs/handbook/en/index.md +2 -2
  110. package/docs/handbook/en/reference/card.md +85 -0
  111. package/docs/handbook/en/reference/chart.md +68 -0
  112. package/docs/handbook/en/reference/choice.md +69 -0
  113. package/docs/handbook/en/reference/compare.md +59 -0
  114. package/docs/handbook/en/reference/figure.md +84 -0
  115. package/docs/handbook/en/reference/file/checklist.md +17 -0
  116. package/docs/handbook/en/reference/file.md +50 -0
  117. package/docs/handbook/en/reference/html.md +62 -0
  118. package/docs/handbook/en/reference/interactive.md +71 -0
  119. package/docs/handbook/en/reference/list.md +72 -0
  120. package/docs/handbook/en/reference/relations.md +113 -0
  121. package/docs/handbook/en/reference/stat.md +60 -0
  122. package/docs/handbook/en/reference/timeline.md +56 -0
  123. package/docs/handbook/en/reference.md +26 -0
  124. package/docs/handbook/en/start.md +2 -2
  125. package/docs/handbook/en/workflow.md +6 -5
  126. package/docs/handbook/en/writing.md +3 -2
  127. package/docs/handbook/index.md +1 -1
  128. package/docs/handbook/reference/card.md +85 -0
  129. package/docs/handbook/reference/chart.md +68 -0
  130. package/docs/handbook/reference/choice.md +69 -0
  131. package/docs/handbook/reference/compare.md +59 -0
  132. package/docs/handbook/reference/figure.md +84 -0
  133. package/docs/handbook/reference/file/checklist.md +17 -0
  134. package/docs/handbook/reference/file.md +50 -0
  135. package/docs/handbook/reference/html.md +62 -0
  136. package/docs/handbook/reference/interactive.md +71 -0
  137. package/docs/handbook/reference/list.md +72 -0
  138. package/docs/handbook/reference/relations.md +113 -0
  139. package/docs/handbook/reference/stat.md +60 -0
  140. package/docs/handbook/reference/timeline.md +56 -0
  141. package/docs/handbook/reference.md +26 -0
  142. package/docs/handbook/start.md +1 -1
  143. package/docs/handbook/workflow.md +6 -5
  144. package/docs/handbook/writing.md +3 -2
  145. package/next.config.ts +6 -0
  146. package/package.json +2 -1
  147. package/public/images/garden/ref-card-book.svg +1 -0
  148. package/public/images/garden/ref-card-lamp.svg +1 -0
  149. package/scripts/build-content.mjs +37 -16
  150. package/scripts/favicon-ico.mjs +9 -6
  151. package/scripts/file-preview.mjs +10 -7
  152. package/scripts/handbook-allowlist.mjs +18 -0
  153. package/scripts/handbook-facts.mjs +305 -11
  154. package/scripts/link-assets.mjs +7 -0
  155. package/scripts/new-post.mjs +18 -9
  156. package/scripts/prepare-pages-output.mjs +30 -18
  157. package/scripts/redirects.mjs +17 -6
  158. package/templates/garden-blocks.md +67 -0
  159. package/templates/garden-snippets.plist +61 -0
  160. package/templates/site/Makefile.en +45 -0
  161. package/templates/site/README.en.md +15 -0
  162. package/templates/site/site.config.en.yaml +55 -0
  163. package/templates/skills-en/garden-interactive-chart/SKILL.md +84 -0
  164. package/templates/skills-en/garden-interactive-chart/references/shell.html +49 -0
  165. package/templates/skills-en/garden-interactive-chart/references/style.md +61 -0
  166. package/templates/skills-en/garden-post-review/SKILL.md +63 -0
  167. package/templates/skills-en/garden-ui/SKILL.md +118 -0
  168. package/templates/workflows/pages.en.yml +195 -0
  169. package/templates/workflows/pages.yml +195 -0
  170. package/ui/garden/catalog.mjs +205 -4
  171. package/ui/garden/check.mjs +6 -0
  172. package/ui/garden/file-block.mjs +8 -7
  173. package/ui/garden/html-lang.mjs +73 -1
  174. package/ui/garden/markdown-links.mjs +7 -4
  175. package/ui/garden/parse.mjs +45 -0
  176. package/ui/garden/relations.mjs +252 -0
  177. package/ui/garden/search-index.mjs +6 -4
  178. package/ui/garden/source-icon.mjs +54 -0
  179. package/ui/parity-fixtures.mjs +77 -0
  180. package/ui/preview/dist/garden-ui.esm.js +41 -4
  181. package/ui/preview/dist/garden-ui.iife.js +41 -4
  182. package/ui/prompt/rules.md +28 -1
  183. package/ui/react/components/chart.tsx +1 -1
  184. package/ui/react/components/display.tsx +1 -1
  185. package/ui/react/lang.tsx +4 -0
  186. package/ui/react/parity.tsx +8 -6
  187. package/worker/index.ts +8 -2
@@ -11,7 +11,7 @@ description: >-
11
11
 
12
12
  # 编译 Garden 交互块
13
13
 
14
- 作者在稿里用不带名字的 `garden` 围栏写一句想法,这个 skill 把它变成 `garden interactive` 或别的 `garden` 组件。交互组件和设计规则在 `npx garden ui prompt` 的输出里,9 个 `garden` 组件的用途和示例在 `node_modules/hd-garden/templates/garden-blocks.md`(在引擎仓库里是 `templates/garden-blocks.md`),以这两份为准,不凭记忆写组件。
14
+ 作者在稿里用不带名字的 `garden` 围栏写一句想法,这个 skill 把它变成 `garden interactive` 或别的 `garden` 组件。交互组件和设计规则在 `npx garden ui prompt` 的输出里,12 个 `garden` 组件的用途和示例在 `node_modules/hd-garden/templates/garden-blocks.md`(在引擎仓库里是 `templates/garden-blocks.md`),以这两份为准,不凭记忆写组件。
15
15
 
16
16
  作者提交时,提交前钩子跑的是同一个 `npx garden ui check`。你用来找块、写回、自查的命令,和拦住作者提交的命令是同一套。
17
17
 
@@ -48,7 +48,7 @@ description: >-
48
48
  - **写回正文**:一句话、一张表或一张 Mermaid 就能说清时。
49
49
  - **写 `garden interactive`**:读者需要拖、点、翻才能看懂时。
50
50
  - **写一段 `diff`**:想法紧跟在一段完整代码后面,说的是「和上一版比改了什么」时。按原话写出 diff,用一段普通 Markdown 写回(只有这一个 `diff` 围栏)。写回后它紧挨着那段代码,页面会合成「代码 / 对比上一版」切换;diff 里的新行要和上面的完整代码一致。
51
- - **写别的 `garden` 组件**:一张资源卡、几个指标、两种做法并排、一条时间线、一张静态图表、一张结构图、一份下载文件。
51
+ - **写别的 `garden` 组件**:一张或多张资源卡(`card` / `cards`)、几个指标(`stat`)、两种做法并排或好坏对照(`compare` / `verdict: good|bad`)、一条时间线(`timeline`)、一张静态图表(`chart`)、一张分区结构图(`figure`)、一份下载文件(`file`)、3~6 个要素的关系图(`relations`)、紧凑图标列表或分步列表(`list`)、两三个选项的选择卡(`choice`)。字段与校验约束见 `npx garden ui prompt` 与 `templates/garden-blocks.md`。
52
52
  - **写 `garden html`**:目录表达不了的画布动画、音频、物理模拟等。按 [garden-interactive-chart](../garden-interactive-chart/SKILL.md) 在同名目录写 HTML,用 `title` 写下判断。
53
53
 
54
54
  4. 写 `garden interactive` 时:
@@ -0,0 +1,67 @@
1
+ # Public blog canonical URL without a trailing slash. Used by GitHub Pages builds and the standalone admin studio.
2
+ SITE_URL=https://your-name.github.io
3
+ NEXT_PUBLIC_SITE_URL=https://your-name.github.io
4
+
5
+ # Standalone admin studio URL; static exports redirect /admin here.
6
+ ADMIN_URL=https://admin.example.com
7
+
8
+ # Optional: explicit GitHub OAuth callback URL when behind a reverse proxy or multiple domains.
9
+ # GITHUB_OAUTH_REDIRECT_URI=https://admin.example.com/api/auth/github/callback
10
+
11
+ # Comments are disabled by default. Open https://giscus.app, install the app, uncomment below, and fill in real values.
12
+ # The repository must be public (owner/name) with Discussions enabled.
13
+ # Also add the same four variables under GitHub Actions Variables for production builds.
14
+ # To publish without comments, set the Actions Variable GISCUS_DISABLED to 1.
15
+ # GISCUS_REPO=
16
+ # GISCUS_REPO_ID=
17
+ # GISCUS_CATEGORY=
18
+ # GISCUS_CATEGORY_ID=
19
+
20
+ # Webmention.io: set your domain (without https://) after registering a custom domain.
21
+ WEBMENTION_IO_DOMAIN=example.com
22
+
23
+ # Optional: override the default Webmention.io endpoint when self-hosting.
24
+ # WEBMENTION_ENDPOINT=https://example.com/webmention
25
+
26
+ # Optional: display exact commit counts, pinned repositories, and contribution graph on /about locally. Server-only.
27
+ # GITHUB_TOKEN=github_pat_xxx
28
+
29
+ # Optional: cookie-free analytics, such as Plausible.
30
+ # NEXT_PUBLIC_ANALYTICS_DOMAIN=example.com
31
+ # NEXT_PUBLIC_ANALYTICS_SCRIPT=https://plausible.io/js/script.js
32
+
33
+ # Optional: include drafts and future-dated posts during local builds.
34
+ # CONTENT_INCLUDE_DRAFTS=1
35
+
36
+ # Admin studio: GitHub OAuth App, allowed logins, and fine-grained PAT for writing to the content repository.
37
+ # Callback URL: https://admin.example.com/api/auth/github/callback
38
+ # CONTENT_REPOSITORY=owner/repository
39
+ # CONTENT_BRANCH=main
40
+ # CONTENT_GITHUB_TOKEN=github_pat_xxxxxxxxxxxx
41
+ # GITHUB_OAUTH_CLIENT_ID=Ov23li_xxxxxxxxxxxx
42
+ # GITHUB_OAUTH_CLIENT_SECRET=xxxxxxxxxxxxxxxx
43
+ # GITHUB_SESSION_SECRET=use-a-random-string-at-least-32-characters-long
44
+ # ADMIN_GITHUB_LOGINS=your-github-login
45
+
46
+ # Admin media: Alibaba Cloud OSS. AccessKeys stay on the admin server and are never sent to the browser.
47
+ # OSS_ENDPOINT=https://oss-cn-hangzhou.aliyuncs.com
48
+ # OSS_BUCKET=your-public-media-bucket
49
+ # OSS_ACCESS_KEY_ID=LTAIxxxxxxxx
50
+ # OSS_ACCESS_KEY_SECRET=xxxxxxxx
51
+ # OSS_PREFIX=blog-media
52
+ # OSS_PUBLIC_BASE_URL=https://media.example.com
53
+ # OSS_MAX_UPLOAD_MB=200
54
+
55
+ # Admin UI studio: OpenAI-compatible endpoint. API keys stay on the admin server only.
56
+ # UI_MODEL_BASE_URL=https://api.openai.com/v1
57
+ # UI_MODEL_API_KEY=sk-xxxxxxxx
58
+ # UI_MODEL_NAME=gpt-5.6
59
+ # UI_DRAFT_RATE_LIMIT=60
60
+ # Optional: GitHub Enterprise or local mock API base URL.
61
+ # GITHUB_API_URL=https://api.github.com
62
+
63
+ # Content distribution: X uses a user access token with write permissions; other platforms use your webhook bridges.
64
+ # X_USER_ACCESS_TOKEN=xxxxxxxx
65
+ # CSDN_SYNC_WEBHOOK=https://publisher.example.com/csdn
66
+ # ZHIHU_SYNC_WEBHOOK=https://publisher.example.com/zhihu
67
+ # JUEJIN_SYNC_WEBHOOK=https://publisher.example.com/juejin
package/README.en.md CHANGED
@@ -29,7 +29,7 @@ Where you want a component or interactive calculation, write an unnamed `garden`
29
29
  ```
30
30
  ````
31
31
 
32
- Ask your Agent to compile the post using the bundled `garden-ui` skill: it runs `npx garden ui scan` to locate uncompiled or broken blocks, reads the component catalog from `npx garden ui prompt`, generates a `garden interactive` block (or `card`, `stat`, `compare`, `timeline`, `chart`, `figure`, `html`, `file`, or a `diff` block right below a code snippet), and writes only that fence back via `npx garden ui apply`. If you run `git commit` before compiling, the pre-commit hook runs `npx --no-install garden ui check --staged --quiet`, stops the commit, and prints a ready-to-paste fix prompt for your AI.
32
+ Ask your Agent to compile the post using the bundled `garden-ui` skill: it runs `npx garden ui scan` to locate uncompiled or broken blocks, reads the component catalog from `npx garden ui prompt`, generates a `garden interactive` block (or `card`, `stat`, `compare`, `timeline`, `chart`, `figure`, `html`, `file`, `relations`, `list`, `choice`, or a `diff` block right below a code snippet), and writes only that fence back via `npx garden ui apply`. If you run `git commit` before compiling, the pre-commit hook runs `npx --no-install garden ui check --staged --quiet`, stops the commit, and prints a ready-to-paste fix prompt for your AI.
33
33
 
34
34
  ## 3. Common commands
35
35
 
@@ -48,7 +48,7 @@ npx garden ui catalog # Generate an offline component
48
48
 
49
49
  Start `make dev` and open `/garden/en` (Chinese at `/garden`), or read the markdown sources directly:
50
50
 
51
- - **[Garden Handbook (English)](docs/handbook/en/index.md)** · **[Garden 使用说明(中文)](docs/handbook/index.md)**: [Quick Start](docs/handbook/en/start.md), [Writing with AI](docs/handbook/en/ai-writing.md), [Site & Column Configuration](docs/handbook/en/config.md), [Deploy to GitHub Pages](docs/handbook/en/deploy.md), [Writing Posts & Thoughts](docs/handbook/en/writing.md), [How Reading Layout Works](docs/handbook/en/design.md), [9 Built-in Components](docs/handbook/en/components.md), and [Checking & Shortcuts](docs/handbook/en/workflow.md).
51
+ - **[Garden Handbook (English)](docs/handbook/en/index.md)** · **[Garden 使用说明(中文)](docs/handbook/index.md)**: [Quick Start](docs/handbook/en/start.md), [Writing with AI](docs/handbook/en/ai-writing.md), [Site & Column Configuration](docs/handbook/en/config.md), [Deploy to GitHub Pages](docs/handbook/en/deploy.md), [Writing Posts & Thoughts](docs/handbook/en/writing.md), [How Reading Layout Works](docs/handbook/en/design.md), [12 Built-in Components](docs/handbook/en/components.md), and [Checking & Shortcuts](docs/handbook/en/workflow.md).
52
52
  - **[Quick Start Guide](docs/quick-start.md)** and **[GitHub Pages Deployment](docs/github-pages.md)**: single public repository or private source + public output repository setup, plus Giscus comments.
53
53
  - **[Admin Service](docs/admin-service.md)**: optional `/admin` server (local, Docker, or Cloudflare Workers) for posting thoughts from the web, uploading media to Alibaba Cloud OSS, and using the `/admin/ui` workbench.
54
54
 
package/README.md CHANGED
@@ -31,7 +31,7 @@ make dev
31
31
  ```
32
32
  ````
33
33
 
34
- 让 Agent 按站点里的 `garden-ui` skill 处理这篇稿件:Agent 先用 `npx garden ui scan` 读取待编译块的哈希,再读 `npx garden ui prompt` 的组件规则,生成 `garden interactive`(或 `card`、`stat`、`compare`、`timeline`、`chart`、`figure`、`html`、`file` 等内置组件,或在完整代码下方生成 `diff`),最后用 `npx garden ui apply` 只替换该围栏。如果你在编译前直接 `git commit`,pre-commit 钩子会运行同一条 `npx --no-install garden ui check --staged --quiet` 拦下提交,并打印一段可直接粘贴给 AI 的修复提示词。
34
+ 让 Agent 按站点里的 `garden-ui` skill 处理这篇稿件:Agent 先用 `npx garden ui scan` 读取待编译块的哈希,再读 `npx garden ui prompt` 的组件规则,生成 `garden interactive`(或 `card`、`stat`、`compare`、`timeline`、`chart`、`figure`、`html`、`file`、`relations`、`list`、`choice` 等内置组件,或在完整代码下方生成 `diff`),最后用 `npx garden ui apply` 只替换该围栏。如果你在编译前直接 `git commit`,pre-commit 钩子会运行同一条 `npx --no-install garden ui check --staged --quiet` 拦下提交,并打印一段可直接粘贴给 AI 的修复提示词。
35
35
 
36
36
  ## 3. 常用命令
37
37
 
@@ -50,7 +50,7 @@ npx garden ui catalog # 在 .garden/ui-catalog.html 生
50
50
 
51
51
  本地运行 `make dev` 后访问 `/garden`(英文版在 `/garden/en`),或直接阅读仓库内的文档:
52
52
 
53
- - **[Garden 使用说明(中文)](docs/handbook/index.md)** · **[Garden Handbook (English)](docs/handbook/en/index.md)**:快速开始、[用 AI 写作](docs/handbook/ai-writing.md)、[站点与专栏配置](docs/handbook/config.md)、[发布到 GitHub Pages](docs/handbook/deploy.md)、[写文章与随想](docs/handbook/writing.md)、[阅读排版](docs/handbook/design.md)、[9 个内置组件](docs/handbook/components.md) 与 [语法检查](docs/handbook/workflow.md)。
53
+ - **[Garden 使用说明(中文)](docs/handbook/index.md)** · **[Garden Handbook (English)](docs/handbook/en/index.md)**:快速开始、[用 AI 写作](docs/handbook/ai-writing.md)、[站点与专栏配置](docs/handbook/config.md)、[发布到 GitHub Pages](docs/handbook/deploy.md)、[写文章与随想](docs/handbook/writing.md)、[阅读排版](docs/handbook/design.md)、[12 个内置组件](docs/handbook/components.md) 与 [语法检查](docs/handbook/workflow.md)。
54
54
  - **[从零开始](docs/quick-start.md)** 与 **[GitHub Pages 部署](docs/github-pages.md)**:单公开仓库或「私有源码仓库 + 公开页面仓库」的 Actions 配置与 Giscus 评论接入。
55
55
  - **[独立管理后台](docs/admin-service.md)**:可选的 `/admin` 服务(本地、Docker 或 Cloudflare Workers),支持网页发随想、阿里云 OSS 媒体直传与 `/admin/ui` 组件工作台。
56
56
 
@@ -7,6 +7,7 @@ import { TableOfContents } from "../../components/TableOfContents";
7
7
  import { a2WordCounts } from "@garden/generated-content";
8
8
  import { demoDateLabel, demoDateTime } from "../../lib/a2-meta";
9
9
  import { extractHeadings } from "../../lib/content";
10
+ import { siteLang, ui } from "../../lib/i18n";
10
11
  import { a2Slugs, readA2Article } from "../../lib/a2-preview";
11
12
  import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../ui/garden/static-params.mjs";
12
13
 
@@ -37,9 +38,9 @@ export default async function A2ArticlePage({ params }: { params: Promise<{ slug
37
38
  <ArticleTitle title={article.title.replace(/(互动版)$/u, "")} />
38
39
  <ul className="meta article-meta">
39
40
  <li>Hedon</li>
40
- {article.date ? <li><time dateTime={demoDateTime(article.date)}>{demoDateLabel(article.date)}</time></li> : null}
41
- <li>约 {wordCount.toLocaleString("en-US")} 字</li>
42
- {article.original ? <li><a className="orig ext" href={article.original} target="_blank" rel="noopener noreferrer">原文 ↗</a></li> : null}
41
+ {article.date ? <li><time dateTime={demoDateTime(article.date)}>{demoDateLabel(article.date, siteLang)}</time></li> : null}
42
+ <li>{siteLang === "en" ? ui().wordCount(wordCount) : <>约 {wordCount.toLocaleString("en-US")} 字</>}</li>
43
+ {article.original ? <li><a className="orig ext" href={article.original} target="_blank" rel="noopener noreferrer">{siteLang === "en" ? "Original ↗" : "原文 ↗"}</a></li> : null}
43
44
  </ul>
44
45
  {article.cover ? (
45
46
  <figure className="article-hero article-cover cover-card cover-img">
package/app/a2/page.tsx CHANGED
@@ -1,9 +1,10 @@
1
1
  import Link from "next/link";
2
2
  import { siteHref } from "../lib/site-href";
3
3
  import { a2Slugs, readA2Article } from "../lib/a2-preview";
4
+ import { ui } from "../lib/i18n";
4
5
 
5
6
  export const metadata = {
6
- title: "A2 对照",
7
+ title: ui().a2IndexTitle,
7
8
  robots: { index: false, follow: false },
8
9
  };
9
10
 
@@ -12,8 +13,8 @@ export default function A2Index() {
12
13
  return (
13
14
  <main className="page-shell index-page">
14
15
  <header className="sh-head">
15
- <h1>A2 对照</h1>
16
- <p className="sh-deck">对照参考文章的版式。正文没有改字。</p>
16
+ <h1>{ui().a2IndexTitle}</h1>
17
+ <p className="sh-deck">{ui().a2IndexDeck}</p>
17
18
  </header>
18
19
  <ul className="sh-rows">
19
20
  {articles.map((article) => (
@@ -5,16 +5,15 @@ import { PageIntro } from "../components/PageIntro";
5
5
  import { siteConfig } from "../site.config";
6
6
  import { siteHref } from "../lib/site-href";
7
7
  import { pageAlternates } from "../lib/page-alternates";
8
+ import { ui } from "../lib/i18n";
8
9
 
10
+ const t = ui();
9
11
  const about = siteConfig.pages.about;
10
- const heading = about.heading ?? "关于";
11
- const paragraphs = about.paragraphs ?? [
12
- "一名工程师。平时写代码,也把想清楚的问题记下来。",
13
- "这里放文章、随想和专栏。",
14
- ];
12
+ const heading = about.heading ?? t.aboutFallbackHeading;
13
+ const paragraphs = about.paragraphs ?? t.aboutFallbackParagraphs;
15
14
  const quote = about.quote ?? "";
16
- const focus = about.focus ?? ["软件", "笔记", "日常"];
17
- const cta = about.cta ?? { href: "/blog", label: "浏览博文" };
15
+ const focus = about.focus ?? t.aboutFallbackFocus;
16
+ const cta = about.cta ?? { href: "/blog", label: t.aboutFallbackCta };
18
17
 
19
18
  export const metadata = {
20
19
  title: { absolute: siteConfig.name },
@@ -26,7 +25,7 @@ export default function AboutPage() {
26
25
  return (
27
26
  <main className="page-shell about-page">
28
27
  <PageIntro
29
- eyebrow="关于"
28
+ eyebrow={t.aboutEyebrow}
30
29
  title={about.title}
31
30
  subtitle={about.subtitle}
32
31
  />
@@ -40,7 +39,7 @@ export default function AboutPage() {
40
39
  {quote ? <blockquote>{quote}</blockquote> : null}
41
40
  </section>
42
41
  <aside className="about-facts">
43
- <span>现在关注</span>
42
+ <span>{t.aboutFocusHeading}</span>
44
43
  <ul>
45
44
  {focus.map((item) => (
46
45
  <li key={item}>{item}</li>
@@ -6,6 +6,7 @@ import { columns, posts, thoughts } from "../lib/content";
6
6
  import { isContentRepositoryConfigured, listRepositoryThoughts } from "../lib/github-content";
7
7
  import { getSiteUrl } from "../lib/site";
8
8
  import { siteConfig } from "../site.config";
9
+ import { ui } from "../lib/i18n";
9
10
  import { modelConfig, remainingDrafts } from "../lib/ui-model";
10
11
  import { CATALOG_VERSION } from "../../ui/core/catalog.mjs";
11
12
  import packageJson from "../../package.json";
@@ -13,7 +14,7 @@ import packageJson from "../../package.json";
13
14
  export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
14
15
 
15
16
  export const metadata = {
16
- title: "内容管理",
17
+ title: ui().adminTitleMain,
17
18
  robots: { index: false, follow: false },
18
19
  };
19
20
 
@@ -27,17 +28,17 @@ export default async function AdminPage({
27
28
  return (
28
29
  <main className="admin-access-page">
29
30
  <div className="admin-access-card">
30
- <h1>后台在另一个地址。</h1>
31
- <p>这个页面是公开博客。写随想、上传和转发,去后台。</p>
31
+ <h1>{ui().staticAdminHeading}</h1>
32
+ <p>{ui().staticAdminRootBody}</p>
32
33
  <div className="admin-access-actions">
33
34
  {adminUrl ? (
34
35
  <a className="admin-github-login" href={adminUrl}>
35
- 打开后台 <ArrowSquareOut size={15} />
36
+ {`${ui().staticAdminOpen} `}<ArrowSquareOut size={15} />
36
37
  </a>
37
38
  ) : (
38
- <p>还没有填写后台地址。</p>
39
+ <p>{ui().staticAdminNoUrl}</p>
39
40
  )}
40
- <Link href="/">返回公开博客</Link>
41
+ <Link href="/">{ui().staticAdminBack}</Link>
41
42
  </div>
42
43
  </div>
43
44
  </main>
@@ -7,12 +7,13 @@ import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } fro
7
7
  import { MarkdownArticle } from "../../../components/MarkdownArticle";
8
8
  import { repositoryContentPath } from "../../../lib/repository-path.mjs";
9
9
  import { readMarkdown, readText } from "../../../lib/ui-content";
10
+ import { ui } from "../../../lib/i18n";
10
11
  import { formatInlineLink, replaceInlineLinks } from "../../../../ui/garden/markdown-destination.mjs";
11
12
  import { STATIC_PLACEHOLDER, staticParams } from "../../../../ui/garden/static-params.mjs";
12
13
 
13
14
  export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
14
15
  export const dynamicParams = false;
15
- export const metadata = { title: "草稿预览", robots: { index: false, follow: false } };
16
+ export const metadata = { title: ui().adminTitlePreview, robots: { index: false, follow: false } };
16
17
 
17
18
  /** Draft preview is served by the admin worker. The public static export has no paths here. */
18
19
  export function generateStaticParams() {
@@ -1,9 +1,10 @@
1
1
  import { AdminBlocks } from "../../../components/AdminUiPanels";
2
2
  import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } from "../../../components/AdminUiShell";
3
+ import { ui } from "../../../lib/i18n";
3
4
  import { getSiteUrl } from "../../../lib/site";
4
5
 
5
6
  export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
6
- export const metadata = { title: "交互块", robots: { index: false, follow: false } };
7
+ export const metadata = { title: ui().adminTitleBlocks, robots: { index: false, follow: false } };
7
8
 
8
9
  export default async function UiBlocksPage() {
9
10
  if (isStaticExport()) return <StaticAdminNotice />;
@@ -1,9 +1,10 @@
1
1
  import { AdminCatalog } from "../../../components/AdminUiPanels";
2
2
  import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } from "../../../components/AdminUiShell";
3
+ import { ui } from "../../../lib/i18n";
3
4
  import { recipeSources } from "../../../lib/ui-model";
4
5
 
5
6
  export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
6
- export const metadata = { title: "组件清单", robots: { index: false, follow: false } };
7
+ export const metadata = { title: ui().adminTitleCatalog, robots: { index: false, follow: false } };
7
8
 
8
9
  export default async function UiCatalogPage() {
9
10
  if (isStaticExport()) return <StaticAdminNotice />;
@@ -2,10 +2,11 @@ import { parse as parseYaml } from "yaml";
2
2
  import casesSource from "../../../../ui/evals/cases.yaml?raw";
3
3
  import { AdminEvals } from "../../../components/AdminUiPanels";
4
4
  import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } from "../../../components/AdminUiShell";
5
+ import { ui } from "../../../lib/i18n";
5
6
  import { modelConfig } from "../../../lib/ui-model";
6
7
 
7
8
  export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
8
- export const metadata = { title: "模型自检", robots: { index: false, follow: false } };
9
+ export const metadata = { title: ui().adminTitleEvals, robots: { index: false, follow: false } };
9
10
 
10
11
  export default async function UiEvalsPage() {
11
12
  if (isStaticExport()) return <StaticAdminNotice />;
@@ -1,9 +1,10 @@
1
1
  import { AdminUiShell, isStaticExport, requireAdminPage, StaticAdminNotice } from "../../components/AdminUiShell";
2
2
  import { UiStudio } from "../../components/UiStudio";
3
+ import { ui } from "../../lib/i18n";
3
4
  import { modelConfig, recipeSources, remainingDrafts } from "../../lib/ui-model";
4
5
 
5
6
  export const dynamic = process.env.STATIC_EXPORT === "1" ? "force-static" : "force-dynamic";
6
- export const metadata = { title: "写作台", robots: { index: false, follow: false } };
7
+ export const metadata = { title: ui().adminTitleUi, robots: { index: false, follow: false } };
7
8
 
8
9
  export default async function UiStudioPage() {
9
10
  if (isStaticExport()) return <StaticAdminNotice />;
@@ -28,7 +28,7 @@ import {
28
28
  } from "../../lib/site";
29
29
  import { githubUrl, siteConfig } from "../../site.config";
30
30
  import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../ui/garden/static-params.mjs";
31
- import { ui } from "../../lib/i18n";
31
+ import { siteLang, ui } from "../../lib/i18n";
32
32
 
33
33
  export function generateStaticParams() {
34
34
  return staticParams(posts.map((post) => ({ path: post.path.split("/") })), { path: [STATIC_PLACEHOLDER] });
@@ -86,12 +86,13 @@ export default async function PostPage({
86
86
  const canonicalUrl = absoluteUrl(pathname, baseUrl);
87
87
  const wordCount = estimateWordCount(post.content);
88
88
  const { prev, next } = adjacentByDate(posts, (item: { path: string }) => item.path === post.path);
89
+ const t = ui();
89
90
 
90
91
  return (
91
92
  <main className="reading-page post-reading-page">
92
93
  <article className="post-article h-entry">
93
94
  <Link className="u-url sr-only" href={pathname}>
94
- {post.title} 永久链接
95
+ {siteLang === "en" ? t.permalinkLabel(post.title) : <>{post.title} 永久链接</>}
95
96
  </Link>
96
97
  <a
97
98
  className="p-author h-card sr-only"
@@ -109,18 +110,18 @@ export default async function PostPage({
109
110
  {formatDate(post.date)}
110
111
  </time>
111
112
  </li>
112
- {post.updated ? <li>更新于 {formatDate(post.updated)}</li> : null}
113
- <li>{ui().wordCount(wordCount)}</li>
113
+ {post.updated ? <li>{siteLang === "en" ? t.updatedOn(formatDate(post.updated)) : <>更新于 {formatDate(post.updated)}</>}</li> : null}
114
+ <li>{t.wordCount(wordCount)}</li>
114
115
  {originalUrl(post) ? (
115
116
  <li>
116
- <a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">原文 ↗</a>
117
+ <a className="orig ext" href={originalUrl(post)} target="_blank" rel="noopener noreferrer">{t.originalLink}</a>
117
118
  </li>
118
119
  ) : null}
119
120
  </ul>
120
121
  <ArticleTags tags={post.tags} />
121
122
  {post.cover && (
122
123
  <figure className="article-hero article-cover cover-card cover-img">
123
- <CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={`${post.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
124
+ <CoverImage className="u-photo" src={post.cover} image={post.coverImage} alt={t.coverLabel(post.title)} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
124
125
  </figure>
125
126
  )}
126
127
  </header>
package/app/blog/page.tsx CHANGED
@@ -4,6 +4,7 @@ import { BlogArchive, BlogArchiveFallback } from "../components/BlogArchive";
4
4
  import { posts } from "../lib/content";
5
5
  import { archivePosts, archiveSeries } from "../lib/archive";
6
6
  import { pageAlternates } from "../lib/page-alternates";
7
+ import { siteLang, ui } from "../lib/i18n";
7
8
 
8
9
  export const metadata = {
9
10
  title: siteDocumentTitle,
@@ -12,13 +13,14 @@ export const metadata = {
12
13
  };
13
14
 
14
15
  export default function BlogIndex() {
16
+ const t = ui();
15
17
  const list = archivePosts();
16
18
  const series = archiveSeries();
17
19
  return (
18
20
  <main className="sh-main" id="content">
19
21
  <div className="sh-head">
20
- <h1 id="sh-title">文章</h1>
21
- <p className="sh-deck">阶段性的完整沉淀,关于工程、架构与技术思考。共 {posts.length} 篇,按年份归档。</p>
22
+ <h1 id="sh-title">{t.blogHeading}</h1>
23
+ <p className="sh-deck">{siteLang === "en" ? t.blogDeck(posts.length) : <>阶段性的完整沉淀,关于工程、架构与技术思考。共 {posts.length} 篇,按年份归档。</>}</p>
22
24
  </div>
23
25
  <Suspense fallback={<BlogArchiveFallback posts={list} series={series} />}>
24
26
  <BlogArchive posts={list} series={series} />
@@ -29,7 +29,7 @@ import {
29
29
  import { githubUrl, siteConfig } from "../../../site.config";
30
30
  import { siteHref } from "../../../lib/site-href";
31
31
  import { isStaticPlaceholder, STATIC_PLACEHOLDER, staticParams } from "../../../../ui/garden/static-params.mjs";
32
- import { ui } from "../../../lib/i18n";
32
+ import { columnStatusLabel, siteLang, ui } from "../../../lib/i18n";
33
33
 
34
34
  export function generateStaticParams() {
35
35
  return staticParams(
@@ -96,15 +96,16 @@ export default async function ColumnEntryPage({
96
96
  const baseUrl = await getSiteUrl();
97
97
  const pathname = columnHref(entry);
98
98
  const canonicalUrl = absoluteUrl(pathname, baseUrl);
99
+ const t = ui();
99
100
 
100
101
  return (
101
102
  <main className="column-reading-page">
102
103
  <aside className="series-nav">
103
104
  <Link href={siteHref("/columns")} className="back-link">
104
105
  <ArrowLeft size={15} />
105
- 所有专栏
106
+ {t.allColumnsBack}
106
107
  </Link>
107
- <p>主题专栏</p>
108
+ <p>{t.seriesEyebrow}</p>
108
109
  <h2>{entry.columnTitle}</h2>
109
110
  <ol>
110
111
  {listed.map((item) => (
@@ -113,12 +114,12 @@ export default async function ColumnEntryPage({
113
114
  </li>
114
115
  ))}
115
116
  </ol>
116
- <small>{series.length} 篇 · 持续更新</small>
117
+ <small>{ui().columnSideCount(series.length, columnStatusLabel(entry.columnStatus))}</small>
117
118
  </aside>
118
119
 
119
120
  <article className="article-column series-article h-entry">
120
121
  <Link className="u-url sr-only" href={pathname}>
121
- {entry.title} 永久链接
122
+ {siteLang === "en" ? t.permalinkLabel(entry.title) : <>{entry.title} 永久链接</>}
122
123
  </Link>
123
124
  <a
124
125
  className="p-author h-card sr-only"
@@ -137,13 +138,13 @@ export default async function ColumnEntryPage({
137
138
  </time>
138
139
  </li>
139
140
  {originalUrl(entry) ? (
140
- <li><a className="orig ext" href={originalUrl(entry)} target="_blank" rel="noopener noreferrer">原文 ↗</a></li>
141
+ <li><a className="orig ext" href={originalUrl(entry)} target="_blank" rel="noopener noreferrer">{t.originalLink}</a></li>
141
142
  ) : null}
142
143
  </ul>
143
144
  <ArticleTags tags={entry.tags} />
144
145
  {entry.cover && (
145
146
  <figure className="article-hero article-cover cover-img">
146
- <CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={`${entry.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
147
+ <CoverImage className="u-photo" src={entry.cover} image={entry.coverImage} alt={siteLang === "en" ? t.coverLabel(entry.title) : `${entry.title} 封面`} sizes="(max-width: 852px) calc(100vw - 40px), 812px" priority />
147
148
  </figure>
148
149
  )}
149
150
  </header>
@@ -4,6 +4,7 @@ import { columnHref, columns } from "../lib/content";
4
4
  import { ContentEmptyState } from "../components/ContentEmptyState";
5
5
  import { siteConfig, siteDocumentTitle } from "../site.config";
6
6
  import { pageAlternates } from "../lib/page-alternates";
7
+ import { columnStatusLabel, ui } from "../lib/i18n";
7
8
 
8
9
  export const metadata = {
9
10
  title: siteDocumentTitle,
@@ -23,6 +24,7 @@ function day(value: string) {
23
24
  }
24
25
 
25
26
  export default function ColumnsPage() {
27
+ const t = ui();
26
28
  const grouped = Array.from(
27
29
  columns.reduce((result, entry) => {
28
30
  const existing = result.get(entry.column ?? "");
@@ -39,19 +41,21 @@ export default function ColumnsPage() {
39
41
  return (
40
42
  <main className="sh-main" id="content">
41
43
  <div className="sh-head">
42
- <h1>{siteConfig.pages.columns.title || "专栏"}</h1>
44
+ <h1>{siteConfig.pages.columns.title || t.homeColumns}</h1>
43
45
  {siteConfig.pages.columns.subtitle ? <p className="sh-deck">{siteConfig.pages.columns.subtitle}</p> : null}
44
46
  </div>
45
47
  {grouped.length ? grouped.map(({ slug, first, entries }) => (
46
- <section className="sh-series" aria-labelledby={slug} key={slug}>
47
- <div className="cover" style={{ background: "var(--well)" }}>
48
- {first.columnCover ? <img className="column-feature-cover" src={first.columnCover} alt={first.columnCoverAlt ?? `${first.columnTitle} 封面`} /> : null}
49
- </div>
48
+ <section className={first.columnCover ? "sh-series" : "sh-series no-cover"} aria-labelledby={slug} key={slug}>
49
+ {first.columnCover ? (
50
+ <div className="cover" style={{ background: "var(--well)" }}>
51
+ <img className="column-feature-cover" src={first.columnCover} alt={first.columnCoverAlt ?? t.columnCoverAlt(first.columnTitle)} />
52
+ </div>
53
+ ) : null}
50
54
  <div className="body">
51
55
  <h2 id={slug}>{first.columnTitle}</h2>
52
- <p className="count">阅读清单,{entries.length} 篇,{first.columnStatus ?? "持续更新"}</p>
53
- <p className="desc">{first.columnDescription ?? first.description ?? "按顺序组织的专题文章系列。"}</p>
54
- <ol aria-label={`${first.columnTitle},从新到旧`}>
56
+ <p className="count">{t.columnReadingList(entries.length, columnStatusLabel(first.columnStatus))}</p>
57
+ <p className="desc">{first.columnDescription ?? first.description ?? t.columnFallbackDesc}</p>
58
+ <ol aria-label={t.columnNewestFirst(first.columnTitle)}>
55
59
  {entries.map((entry) => (
56
60
  <li key={entry.path}>
57
61
  <Link href={columnHref(entry)}>{entry.title}</Link>
@@ -2,6 +2,7 @@ import Link from "next/link";
2
2
  import { redirect } from "next/navigation";
3
3
  import type { ReactNode } from "react";
4
4
  import { getAdminUser } from "../lib/admin-auth";
5
+ import { ui } from "../lib/i18n";
5
6
 
6
7
  const SECTIONS = [
7
8
  { href: "/admin", label: "总览" },
@@ -23,9 +24,9 @@ export function StaticAdminNotice() {
23
24
  return (
24
25
  <main className="admin-access-page">
25
26
  <div className="admin-access-card">
26
- <h1>后台在另一个地址。</h1>
27
- <p>公开博客上看不到这一页。写随想和改交互,去单独部署的后台。</p>
28
- <Link href="/">返回公开博客</Link>
27
+ <h1>{ui().staticAdminHeading}</h1>
28
+ <p>{ui().staticAdminBody}</p>
29
+ <Link href="/">{ui().staticAdminBack}</Link>
29
30
  </div>
30
31
  </main>
31
32
  );
@@ -92,8 +92,11 @@ export function ArticleMeasure() {
92
92
  const articleBox = (article.querySelector(".markdown-body") ?? article).getBoundingClientRect();
93
93
  const viewport = root.clientWidth;
94
94
  let docMaxRoom: number | null = null;
95
- if (docPage) {
96
- const sidebar = docPage.querySelector<HTMLElement>(".gdoc-sidebar");
95
+ // A left rail (the doc sidebar, or a column's article list) bounds the
96
+ // breakout on both sides, so wide content never runs under it.
97
+ const columnNav = docPage ? null : document.querySelector<HTMLElement>(".series-nav");
98
+ if (docPage || columnNav) {
99
+ const sidebar = docPage ? docPage.querySelector<HTMLElement>(".gdoc-sidebar") : columnNav;
97
100
  let leftLimit = 24;
98
101
  if (sidebar && getComputedStyle(sidebar).position === "sticky") {
99
102
  const sbBox = sidebar.getBoundingClientRect();
@@ -102,7 +105,7 @@ export function ArticleMeasure() {
102
105
  }
103
106
  }
104
107
  let rightLimit = viewport - 24;
105
- if (toc && getComputedStyle(toc).display !== "none") {
108
+ if (docPage && toc && getComputedStyle(toc).display !== "none") {
106
109
  const tocBox = toc.getBoundingClientRect();
107
110
  if (tocBox.width > 0 && tocBox.left >= articleBox.right - 1) {
108
111
  rightLimit = tocBox.left - translatedX(toc) - 16;
@@ -1,4 +1,5 @@
1
1
  import { referenceColumns, referenceDomain } from "../../ui/garden/references.mjs";
2
+ import { ui, type UiLang } from "../lib/i18n";
2
3
 
3
4
  type ReferenceItem = {
4
5
  title: string;
@@ -8,8 +9,10 @@ type ReferenceItem = {
8
9
 
9
10
  export function ArticleReferences({
10
11
  references,
12
+ lang,
11
13
  }: {
12
14
  references?: ReferenceItem[];
15
+ lang?: UiLang;
13
16
  }) {
14
17
  if (!references?.length) return null;
15
18
  const split = referenceColumns(references.length) === 2;
@@ -19,7 +22,7 @@ export function ArticleReferences({
19
22
  className={split ? "article-references is-split" : "article-references"}
20
23
  aria-labelledby="article-references-heading"
21
24
  >
22
- <h2 id="article-references-heading">参考</h2>
25
+ <h2 id="article-references-heading">{ui(lang).referencesHeading}</h2>
23
26
  <ol>
24
27
  {references.map((item, index) => {
25
28
  const domain = item.domain ?? referenceDomain(item.url);
@@ -1,13 +1,14 @@
1
1
  import Link from "next/link";
2
2
  import { articleTagHref } from "../lib/tag-filter";
3
3
  import { siteHref } from "../lib/site-href";
4
+ import { ui, type UiLang } from "../lib/i18n";
4
5
 
5
6
  /** Tags under an article title. Link, so following one is a client navigation like the rest of the site. */
6
- export function ArticleTags({ tags }: { tags?: string[] }) {
7
+ export function ArticleTags({ tags, lang }: { tags?: string[]; lang?: UiLang }) {
7
8
  const list = (tags ?? []).filter(Boolean);
8
9
  if (!list.length) return null;
9
10
  return (
10
- <ul className="article-tagrow" aria-label="标签">
11
+ <ul className="article-tagrow" aria-label={ui(lang).articleTagsLabel}>
11
12
  {list.map((tag) => (
12
13
  <li key={tag}>
13
14
  <Link className="tag-chip p-category" href={siteHref(articleTagHref(tag))}>