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
@@ -9,6 +9,14 @@ const ALERT_TITLE = {
9
9
  caution: "注意",
10
10
  };
11
11
 
12
+ const ALERT_TITLE_EN = {
13
+ note: "Note",
14
+ tip: "Tip",
15
+ important: "Important",
16
+ warning: "Warning",
17
+ caution: "Caution",
18
+ };
19
+
12
20
  const LEGACY_KIND = {
13
21
  info: "note",
14
22
  success: "tip",
@@ -47,13 +55,14 @@ function isBlankParagraph(node) {
47
55
  return node?.type === "paragraph" && toString(node).trim() === "" && meaningfulChildren(node).length === 0;
48
56
  }
49
57
 
50
- function alertTitle(kind, label) {
58
+ function alertTitle(kind, label, lang = "zh") {
59
+ const titles = lang === "en" ? ALERT_TITLE_EN : ALERT_TITLE;
51
60
  return {
52
61
  type: "paragraph",
53
62
  data: { hName: "p", hProperties: { className: ["markdown-alert-title"] } },
54
63
  children: [
55
64
  { type: "html", value: ALERT_ICON[kind] },
56
- { type: "text", value: label || ALERT_TITLE[kind] },
65
+ { type: "text", value: label || titles[kind] },
57
66
  ],
58
67
  };
59
68
  }
@@ -136,7 +145,7 @@ function strongOnlyText(node) {
136
145
  return toString(children[0]).trim();
137
146
  }
138
147
 
139
- function rewriteAlert(node) {
148
+ function rewriteAlert(node, lang = "zh") {
140
149
  const children = node.children ?? [];
141
150
  const gfm = gfmAlert(children[0]);
142
151
  const legacy = gfm ? null : legacyAlert(children[0]);
@@ -156,7 +165,7 @@ function rewriteAlert(node) {
156
165
  }
157
166
  }
158
167
  while (rest.length && isBlankParagraph(rest[0])) rest.shift();
159
- node.children = [alertTitle(kind, label), ...rest];
168
+ node.children = [alertTitle(kind, label, lang), ...rest];
160
169
  node.data ??= {};
161
170
  node.data.hProperties ??= {};
162
171
  const existing = node.data.hProperties.className;
@@ -203,17 +212,18 @@ function asFigure(node, imageSize) {
203
212
  };
204
213
  }
205
214
 
206
- function walk(node, imageSize) {
215
+ function walk(node, imageSize, lang = "zh") {
207
216
  if (!node || typeof node !== "object" || !Array.isArray(node.children)) return;
208
- if (node.type === "blockquote") rewriteAlert(node);
217
+ if (node.type === "blockquote") rewriteAlert(node, lang);
209
218
  node.children = node.children.map((child) => asFigure(child, imageSize) ?? child);
210
- for (const child of node.children) walk(child, imageSize);
219
+ for (const child of node.children) walk(child, imageSize, lang);
211
220
  }
212
221
 
213
222
  /** Demo alert cards and captioned figures. Article rendering only. */
214
223
  export function remarkA2(options = {}) {
215
224
  const imageSize = typeof options?.imageSize === "function" ? options.imageSize : () => null;
225
+ const lang = options?.lang === "en" ? "en" : "zh";
216
226
  return (tree) => {
217
- walk(tree, imageSize);
227
+ walk(tree, imageSize, lang);
218
228
  };
219
229
  }
@@ -76,10 +76,32 @@ function walk(parent, source, sameSite) {
76
76
  }
77
77
  }
78
78
 
79
+ const SOURCE_MARKER = "{source}";
80
+
81
+ /**
82
+ * `[research.google](https://research.google/...){source}` keeps the link in the
83
+ * sentence and draws it as a small source chip (site icon + the link text).
84
+ */
85
+ function markSources(parent, source) {
86
+ const children = parent.children ?? [];
87
+ for (let index = 0; index < children.length; index += 1) {
88
+ const node = children[index];
89
+ if (node.type === "code" || node.type === "inlineCode" || node.type === "html") continue;
90
+ const next = children[index + 1];
91
+ if (node.type === "link" && next?.type === "text" && next.value.startsWith(SOURCE_MARKER) && isHttp(node.url) && writtenAsLink(node, source)) {
92
+ next.value = next.value.slice(SOURCE_MARKER.length);
93
+ node.data = { ...node.data, hProperties: { ...node.data?.hProperties, className: ["g-src-chip"], dataSource: "true" } };
94
+ }
95
+ if (node.children) markSources(node, source);
96
+ }
97
+ parent.children = children.filter((child) => !(child.type === "text" && child.value === ""));
98
+ }
99
+
79
100
  export function remarkLinkCard(options = {}) {
80
101
  const sameSite = typeof options?.sameSite === "function" ? options.sameSite : () => null;
81
102
  return (tree, file) => {
82
103
  const source = typeof file?.value === "string" ? file.value : String(file?.value ?? "");
104
+ markSources(tree, source);
83
105
  walk(tree, source, sameSite);
84
106
  };
85
107
  }
@@ -0,0 +1,42 @@
1
+ import { classifyFence } from "../../ui/garden/parse.mjs";
2
+
3
+ /**
4
+ * Component reference pages: every garden fence becomes a demo that shows its
5
+ * source and the real rendered block side by side. Runs before remarkGarden,
6
+ * which then turns the second child into the block.
7
+ */
8
+ /**
9
+ * The whole fence as the author writes it: info line (with any `#id`), the
10
+ * body, and the closing marker, so copying it reproduces the block. A `garden
11
+ * file` body was replaced by built `resolved:` data; show the original YAML.
12
+ */
13
+ function fenceSource(child, block) {
14
+ const resolvedSource = block?.props?.resolved?.source;
15
+ const body = typeof resolvedSource === "string" ? resolvedSource : String(child.value ?? "");
16
+ const info = `${child.lang || ""} ${child.meta || ""}`.trim();
17
+ const longest = Math.max(2, ...(body.match(/`{3,}/gu) || []).map((run) => run.length));
18
+ const marker = "`".repeat(longest + 1);
19
+ return `${marker}${info}\n${body}\n${marker}`;
20
+ }
21
+
22
+ export function remarkRefDemos() {
23
+ return (tree) => {
24
+ const walk = (node) => {
25
+ if (!Array.isArray(node?.children)) return;
26
+ node.children = node.children.map((child) => {
27
+ walk(child);
28
+ if (child?.type !== "code") return child;
29
+ const lang = String(child.lang || "").toLowerCase();
30
+ const meta = String(child.meta || "");
31
+ const kind = classifyFence({ lang, meta, info: `${child.lang || ""} ${meta}`.trim(), body: child.value });
32
+ if (kind.kind !== "garden" || !kind.block?.name) return child;
33
+ return {
34
+ type: "refDemo",
35
+ data: { hName: "garden-refdemo", hProperties: { dataName: kind.block.name } },
36
+ children: [{ type: "code", lang: "markdown", meta: null, value: fenceSource(child, kind.block) }, child],
37
+ };
38
+ });
39
+ };
40
+ walk(tree);
41
+ };
42
+ }
@@ -153,7 +153,14 @@ export function tagFilterHref(selected: readonly string[], pathname = "/blog") {
153
153
  export const ARTICLE_TAG_LIST = "/blog/";
154
154
  export const articleTagHref = (tag: string) => tagFilterHref([tag], ARTICLE_TAG_LIST);
155
155
 
156
- /** 「共 N 篇」, or 「2 个标签,共 5 篇」 with a selection. */
157
- export function tagSummary(selectedCount: number, resultCount: number) {
156
+ /** 「共 N 篇」, or 「2 个标签,共 5 篇」 with a selection (or English equivalent when lang === "en"). */
157
+ export function tagSummary(selectedCount: number, resultCount: number, lang: "zh" | "en" = "zh") {
158
+ if (lang === "en") {
159
+ const postsWord = resultCount === 1 ? "post" : "posts";
160
+ if (!selectedCount) return `${resultCount} ${postsWord}`;
161
+ const tagsWord = selectedCount === 1 ? "tag" : "tags";
162
+ return `${selectedCount} ${tagsWord}, ${resultCount} ${postsWord}`;
163
+ }
158
164
  return selectedCount ? `${selectedCount} 个标签,共 ${resultCount} 篇` : `共 ${resultCount} 篇`;
159
165
  }
166
+
package/app/not-found.tsx CHANGED
@@ -1,15 +1,43 @@
1
+ "use client";
2
+
1
3
  import Link from "next/link";
2
- import { ui } from "./lib/i18n";
4
+ import { usePathname } from "next/navigation";
5
+ import { useLayoutEffect } from "react";
6
+ import { htmlLang, routeLang, ui } from "./lib/i18n";
7
+ import { siteConfig } from "./site.config";
3
8
 
4
9
  export default function NotFound() {
5
- const t = ui();
10
+ const pathname = usePathname();
11
+ const lang = routeLang(pathname);
12
+ const t = ui(lang);
13
+ const fullTitle = `${t.notFoundTitle} · ${siteConfig.name}`;
14
+ const ogLocale = lang === "en" ? "en_US" : "zh_CN";
15
+
16
+ useLayoutEffect(() => {
17
+ document.documentElement.lang = htmlLang(lang);
18
+ document.title = fullTitle;
19
+ const setMeta = (attr: string, key: string, value: string) => {
20
+ const el = document.querySelector<HTMLMetaElement>(`meta[${attr}="${key}"]`);
21
+ if (el) el.content = value;
22
+ };
23
+ setMeta("name", "description", t.notFoundDeck);
24
+ setMeta("property", "og:title", fullTitle);
25
+ setMeta("property", "og:description", t.notFoundDeck);
26
+ setMeta("property", "og:locale", ogLocale);
27
+ setMeta("property", "og:image:alt", fullTitle);
28
+ setMeta("name", "twitter:title", fullTitle);
29
+ setMeta("name", "twitter:description", t.notFoundDeck);
30
+ }, [fullTitle, lang, ogLocale, t.notFoundDeck]);
31
+
6
32
  return (
7
- <main className="page-shell">
33
+ <main className="sh-main" id="garden-not-found">
8
34
  <header className="sh-head">
9
35
  <h1>{t.notFoundTitle}</h1>
10
36
  <p className="sh-deck">{t.notFoundDeck}</p>
11
37
  </header>
12
- <p><Link href="/">{t.backHome}</Link></p>
38
+ <p>
39
+ <Link href="/">{t.backHome}</Link>
40
+ </p>
13
41
  </main>
14
42
  );
15
43
  }
package/app/page.tsx CHANGED
@@ -1,7 +1,8 @@
1
1
  import Link from "next/link";
2
2
  import { CoverImage } from "./components/CoverImage";
3
- import { homeListDate } from "./lib/a2-meta";
4
- import { columns, formatDate, postHref, posts } from "./lib/content";
3
+ import { fullListDate, homeListDate } from "./lib/a2-meta";
4
+ import { columnStatusLabel, siteLang, ui } from "./lib/i18n";
5
+ import { columns, postHref, posts } from "./lib/content";
5
6
  import { githubUrl, siteConfig } from "./site.config";
6
7
  import { siteHref } from "./lib/site-href";
7
8
  import { pageAlternates } from "./lib/page-alternates";
@@ -24,6 +25,7 @@ function day(value: string) {
24
25
  }
25
26
 
26
27
  export default function Home() {
28
+ const t = ui();
27
29
  const recentAll = [...posts].sort((left, right) => Date.parse(right.date) - Date.parse(left.date));
28
30
  const feature = recentAll[0];
29
31
  const recent = recentAll.filter((post) => post.path !== feature?.path);
@@ -43,9 +45,9 @@ export default function Home() {
43
45
  <h1>{siteConfig.pages.home.title}</h1>
44
46
  <p>{siteConfig.pages.home.subtitle || siteConfig.description}</p>
45
47
  <div className="links">
46
- <Link href={siteHref("/about")}>关于我</Link>
48
+ <Link href={siteHref("/about")}>{t.homeAbout}</Link>
47
49
  <a href={githubUrl} rel="me">GitHub</a>
48
- <a href="/rss.xml">RSS 订阅</a>
50
+ <a href={`${(process.env.NEXT_PUBLIC_BASE_PATH ?? "").replace(/\/+$/u, "")}/rss.xml`}>{t.homeRss}</a>
49
51
  </div>
50
52
  </section>
51
53
 
@@ -58,8 +60,8 @@ export default function Home() {
58
60
  ) : null}
59
61
  <div>
60
62
  <p className="kicker">
61
- <span>最新文章</span>
62
- <time className="num" dateTime={day(feature.date)}>{formatDate(feature.date)}</time>
63
+ <span>{t.homeLatestBadge}</span>
64
+ <time className="num" dateTime={day(feature.date)}>{fullListDate(feature.date, siteLang)}</time>
63
65
  </p>
64
66
  <h2 id="ft"><Link href={postHref(feature)}>{feature.title}</Link></h2>
65
67
  {feature.description ? <p>{feature.description}</p> : null}
@@ -73,13 +75,14 @@ export default function Home() {
73
75
  <div className="sh-home-grid">
74
76
  <section className="sh-section" aria-labelledby="h-recent">
75
77
  <div className="sh-sec-head">
76
- <h2 id="h-recent">最近更新</h2>
77
- <Link className="sh-more" href={siteHref("/blog")}>全部 {posts.length} 篇</Link>
78
+ <h2 id="h-recent">{t.homeRecentHeading}</h2>
79
+ {posts.length > 0 ? <Link className="sh-more" href={siteHref("/blog")}>{t.homeAllPosts(posts.length)}</Link> : null}
78
80
  </div>
81
+ {posts.length === 0 ? <p className="sh-empty">{t.emptyPostsTitle}</p> : null}
79
82
  <ol className="sh-rows">
80
83
  {recent.slice(0, 6).map((post) => (
81
84
  <li className="sh-row" key={post.path}>
82
- <time className="num" dateTime={day(post.date)}>{homeListDate(post.date)}</time>
85
+ <time className="num" dateTime={day(post.date)}>{homeListDate(post.date, undefined, siteLang)}</time>
83
86
  <h3><Link href={postHref(post)}>{post.title}</Link></h3>
84
87
  {post.description ? <p>{post.description}</p> : null}
85
88
  </li>
@@ -88,12 +91,12 @@ export default function Home() {
88
91
  </section>
89
92
  {pinned.length > 0 ? (
90
93
  <section className="sh-section" aria-labelledby="h-pin">
91
- <div className="sh-sec-head section-heading accent-heading"><h2 id="h-pin">置顶</h2></div>
94
+ <div className="sh-sec-head section-heading accent-heading"><h2 id="h-pin">{t.homePinned}</h2></div>
92
95
  <ul className="sh-side-list">
93
96
  {pinned.map((post) => (
94
97
  <li key={post.path}>
95
98
  <Link href={postHref(post)}>{post.title}</Link>
96
- <span><time className="num" dateTime={day(post.date)}>{formatDate(post.date)}</time></span>
99
+ <span><time className="num" dateTime={day(post.date)}>{fullListDate(post.date, siteLang)}</time></span>
97
100
  </li>
98
101
  ))}
99
102
  </ul>
@@ -104,8 +107,8 @@ export default function Home() {
104
107
  {columns[0] && series.length ? (
105
108
  <section className="sh-section" aria-labelledby="h-cols">
106
109
  <div className="sh-sec-head">
107
- <h2 id="h-cols">专栏</h2>
108
- <Link className="sh-more" href={siteHref("/columns")}>全部专栏</Link>
110
+ <h2 id="h-cols">{t.homeColumns}</h2>
111
+ <Link className="sh-more" href={siteHref("/columns")}>{t.homeAllColumns}</Link>
109
112
  </div>
110
113
  <div className="sh-cols">
111
114
  {series.map(([slug, entries]) => {
@@ -120,7 +123,7 @@ export default function Home() {
120
123
  </div>
121
124
  <div className="body">
122
125
  <h3><Link href={siteHref(`/columns/#${slug}`)}>{first.columnTitle}</Link></h3>
123
- <p className="count">{entries.length} 篇,{first.columnStatus ?? "持续更新"}</p>
126
+ <p className="count">{t.columnCount(entries.length, columnStatusLabel(first.columnStatus))}</p>
124
127
  <p>{first.columnDescription ?? first.description}</p>
125
128
  </div>
126
129
  </article>
@@ -5,6 +5,7 @@ import { configuredSiteUrl, siteDescription, siteName } from "../lib/site";
5
5
  import { contentForFeed } from "../../ui/core/transform.mjs";
6
6
  import { stripCardMarkers } from "../../ui/garden/markdown-links.mjs";
7
7
  import { withTrailingSlash } from "../../ui/garden/site-href.mjs";
8
+ import { htmlLang, siteLang } from "../lib/i18n";
8
9
 
9
10
  const escapeXml = (value: string) =>
10
11
  value
@@ -72,7 +73,7 @@ export async function GET() {
72
73
  <link>${origin}</link>
73
74
  <atom:link href="${origin}/rss.xml" rel="self" type="application/rss+xml" />
74
75
  <description>${escapeXml(siteDescription)}</description>
75
- <language>zh-CN</language>
76
+ <language>${htmlLang(siteLang)}</language>
76
77
  <lastBuildDate>${lastBuildDate}</lastBuildDate>
77
78
  ${items}
78
79
  </channel>