@jet-w/astro-blog 0.1.6 → 0.2.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 (122) hide show
  1. package/dist/chunk-6D3XRDNY.js +145 -0
  2. package/dist/chunk-A2E2VSAQ.js +246 -0
  3. package/dist/{chunk-GYLSY3OJ.js → chunk-TJTPX2WP.js} +1 -1
  4. package/dist/config/index.d.ts +3 -47
  5. package/dist/config/index.js +18 -2
  6. package/dist/i18n-PgMCFBw0.d.ts +222 -0
  7. package/dist/index.d.ts +204 -7
  8. package/dist/index.js +255 -3
  9. package/dist/integration.d.ts +9 -1
  10. package/dist/integration.js +2 -1
  11. package/dist/{sidebar-DNdiCKBw.d.ts → sidebar-Da-W_4Lr.d.ts} +1 -1
  12. package/dist/utils/sidebar.d.ts +1 -1
  13. package/package.json +1 -1
  14. package/src/components/blog/FloatingToc.vue +11 -3
  15. package/src/components/blog/Hero.astro +17 -2
  16. package/src/components/blog/NavigationTabs.vue +46 -15
  17. package/src/components/blog/PostCard.astro +28 -10
  18. package/src/components/blog/RelatedPosts.astro +23 -7
  19. package/src/components/blog/TableOfContents.astro +10 -4
  20. package/src/components/blog/TagCloud.astro +4 -3
  21. package/src/components/home/FeaturedPostsSection.astro +22 -6
  22. package/src/components/home/QuickNavSection.astro +33 -4
  23. package/src/components/home/RecentPostsSection.astro +22 -6
  24. package/src/components/home/StatsSection.astro +24 -6
  25. package/src/components/layout/Footer.astro +36 -20
  26. package/src/components/layout/Header.astro +75 -17
  27. package/src/components/layout/Sidebar.astro +40 -25
  28. package/src/components/ui/LanguageSwitcher.vue +183 -0
  29. package/src/components/ui/SearchBox.vue +13 -5
  30. package/src/components/ui/SearchInterface.vue +49 -25
  31. package/src/layouts/BaseLayout.astro +77 -52
  32. package/src/layouts/PageLayout.astro +22 -27
  33. package/src/layouts/SlidesLayout.astro +14 -2
  34. package/src/pages/archives/[year]/[month].astro +36 -17
  35. package/src/pages/archives/index.astro +36 -20
  36. package/src/pages/categories/[category].astro +33 -16
  37. package/src/pages/categories/index.astro +37 -14
  38. package/src/pages/posts/[...slug].astro +125 -18
  39. package/src/pages/posts/index.astro +59 -37
  40. package/src/pages/posts/page/[page].astro +65 -27
  41. package/src/pages/rss.xml.ts +18 -6
  42. package/src/pages/search.astro +50 -14
  43. package/src/pages/slides/index.astro +25 -6
  44. package/src/pages/tags/[tag].astro +32 -15
  45. package/src/pages/tags/index.astro +39 -16
  46. package/src/plugins/remark-containers.mjs +351 -322
  47. package/src/plugins/remark-protect-code.mjs +69 -0
  48. package/src/styles/global.css +35 -1
  49. package/templates/default/.claude/ralph-loop.local.md +48 -0
  50. package/templates/default/astro.config.mjs +33 -4
  51. package/templates/default/content/posts/blog_docs_en/01.get-started/01-intro.md +81 -0
  52. package/templates/default/content/posts/blog_docs_en/01.get-started/02-install.md +137 -0
  53. package/templates/default/content/posts/blog_docs_en/01.get-started/03-create-post.md +176 -0
  54. package/templates/default/content/posts/blog_docs_en/01.get-started/04-structure.md +173 -0
  55. package/templates/default/content/posts/blog_docs_en/01.get-started/05-deploy.md +208 -0
  56. package/templates/default/content/posts/blog_docs_en/01.get-started/README.md +52 -0
  57. package/templates/default/content/posts/blog_docs_en/02.guide/02-containers.md +245 -0
  58. package/templates/default/content/posts/blog_docs_en/02.guide/03-code-blocks.md +207 -0
  59. package/templates/default/content/posts/blog_docs_en/02.guide/03-mermaid.md +194 -0
  60. package/templates/default/content/posts/blog_docs_en/02.guide/04-icons.md +229 -0
  61. package/templates/default/content/posts/blog_docs_en/02.guide/06-latex.md +233 -0
  62. package/templates/default/content/posts/blog_docs_en/02.guide/07-video.md +184 -0
  63. package/templates/default/content/posts/blog_docs_en/02.guide/08-slides.md +359 -0
  64. package/templates/default/content/posts/blog_docs_en/02.guide/README.md +213 -0
  65. package/templates/default/content/posts/blog_docs_en/03.config/01-site.md +208 -0
  66. package/templates/default/content/posts/blog_docs_en/03.config/02-sidebar.md +240 -0
  67. package/templates/default/content/posts/blog_docs_en/03.config/03-i18n.md +349 -0
  68. package/templates/default/content/posts/blog_docs_en/03.config/README.md +85 -0
  69. package/templates/default/content/posts/blog_docs_en/README.md +79 -0
  70. package/templates/default/content/posts/blog_docs_zh/01.get-started/01-intro.md +81 -0
  71. package/templates/default/content/posts/blog_docs_zh/01.get-started/02-install.md +137 -0
  72. package/templates/default/content/posts/blog_docs_zh/01.get-started/03-create-post.md +176 -0
  73. package/templates/default/content/posts/blog_docs_zh/01.get-started/04-structure.md +173 -0
  74. package/templates/default/content/posts/blog_docs_zh/01.get-started/05-deploy.md +208 -0
  75. package/templates/default/content/posts/blog_docs_zh/01.get-started/README.md +52 -0
  76. package/templates/default/content/posts/blog_docs_zh/02.guide/02-containers.md +245 -0
  77. package/templates/default/content/posts/blog_docs_zh/02.guide/03-code-blocks.md +206 -0
  78. package/templates/default/content/posts/blog_docs_zh/02.guide/03-mermaid.md +194 -0
  79. package/templates/default/content/posts/blog_docs_zh/02.guide/04-icons.md +229 -0
  80. package/templates/default/content/posts/blog_docs_zh/02.guide/06-latex.md +233 -0
  81. package/templates/default/content/posts/blog_docs_zh/02.guide/07-video.md +184 -0
  82. package/templates/default/content/posts/blog_docs_zh/02.guide/08-slides.md +359 -0
  83. package/templates/default/content/posts/blog_docs_zh/02.guide/README.md +213 -0
  84. package/templates/default/content/posts/blog_docs_zh/03.config/01-site.md +208 -0
  85. package/templates/default/content/posts/blog_docs_zh/03.config/02-sidebar.md +240 -0
  86. package/templates/default/content/posts/blog_docs_zh/03.config/03-i18n.md +348 -0
  87. package/templates/default/content/posts/blog_docs_zh/03.config/README.md +85 -0
  88. package/templates/default/content/posts/blog_docs_zh/README.md +78 -0
  89. package/templates/default/package-lock.json +9667 -0
  90. package/templates/default/package.json +1 -1
  91. package/templates/default/src/config/footer.ts +14 -11
  92. package/templates/default/src/config/locales/en/footer.ts +17 -0
  93. package/templates/default/src/config/locales/en/index.ts +20 -0
  94. package/templates/default/src/config/locales/en/menu.ts +14 -0
  95. package/templates/default/src/config/locales/en/sidebar.ts +34 -0
  96. package/templates/default/src/config/locales/en/site.ts +7 -0
  97. package/templates/default/src/config/locales/en/ui.ts +29 -0
  98. package/templates/default/src/config/locales/index.ts +7 -0
  99. package/templates/default/src/config/locales/zh-CN/footer.ts +17 -0
  100. package/templates/default/src/config/locales/zh-CN/index.ts +20 -0
  101. package/templates/default/src/config/locales/zh-CN/menu.ts +14 -0
  102. package/templates/default/src/config/locales/zh-CN/sidebar.ts +34 -0
  103. package/templates/default/src/config/locales/zh-CN/site.ts +7 -0
  104. package/templates/default/src/config/locales/zh-CN/ui.ts +29 -0
  105. package/templates/default/src/config/sidebar.ts +10 -12
  106. package/templates/default/src/config/site.ts +2 -2
  107. package/templates/default/src/content.config.ts +15 -3
  108. package/templates/default/src/env.d.ts +7 -0
  109. package/dist/chunk-MQXPSOYB.js +0 -124
  110. package/templates/default/content/posts/blog_docs/01-quick-start.md +0 -162
  111. package/templates/default/content/posts/blog_docs/02-frontmatter.md +0 -277
  112. package/templates/default/content/posts/blog_docs/03-markdown-basic.md +0 -350
  113. package/templates/default/content/posts/blog_docs/04-containers.md +0 -331
  114. package/templates/default/content/posts/blog_docs/05-code-blocks.md +0 -388
  115. package/templates/default/content/posts/blog_docs/06-mermaid.md +0 -431
  116. package/templates/default/content/posts/blog_docs/07-video.md +0 -243
  117. package/templates/default/content/posts/blog_docs/08-latex.md +0 -382
  118. package/templates/default/content/posts/blog_docs/09-icons.md +0 -326
  119. package/templates/default/content/posts/blog_docs/10-sidebar.md +0 -445
  120. package/templates/default/content/posts/blog_docs/11-config.md +0 -334
  121. package/templates/default/content/posts/blog_docs/12-slides.mdx +0 -552
  122. package/templates/default/content/posts/blog_docs/README.md +0 -151
package/dist/index.js CHANGED
@@ -16,12 +16,237 @@ import {
16
16
  sidebarConfig,
17
17
  siteConfig,
18
18
  socialLinks
19
- } from "./chunk-GYLSY3OJ.js";
19
+ } from "./chunk-TJTPX2WP.js";
20
20
  import "./chunk-FXPGR372.js";
21
21
  import {
22
22
  astroBlogIntegration,
23
23
  integration_default
24
- } from "./chunk-MQXPSOYB.js";
24
+ } from "./chunk-6D3XRDNY.js";
25
+ import {
26
+ builtInTranslations,
27
+ defaultI18nConfig,
28
+ defaultLocales,
29
+ defineI18nConfig,
30
+ enTranslations,
31
+ getUITranslations,
32
+ zhCNTranslations
33
+ } from "./chunk-A2E2VSAQ.js";
34
+
35
+ // src/utils/i18n.ts
36
+ function getLocaleFromPath(pathname, config = defaultI18nConfig) {
37
+ const segments = pathname.replace(/^\//, "").split("/");
38
+ const firstSegment = segments[0];
39
+ const matchedLocale = config.locales.find(
40
+ (locale) => locale.code === firstSegment
41
+ );
42
+ if (matchedLocale) {
43
+ return matchedLocale.code;
44
+ }
45
+ return config.defaultLocale;
46
+ }
47
+ function getLocaleByCode(code, config = defaultI18nConfig) {
48
+ return config.locales.find((locale) => locale.code === code);
49
+ }
50
+ function removeLocalePrefix(pathname, config = defaultI18nConfig) {
51
+ const locale = getLocaleFromPath(pathname, config);
52
+ if (locale === config.defaultLocale && !config.routing.prefixDefaultLocale) {
53
+ return pathname;
54
+ }
55
+ const prefix = `/${locale}`;
56
+ if (pathname.startsWith(prefix)) {
57
+ const rest = pathname.slice(prefix.length);
58
+ return rest || "/";
59
+ }
60
+ return pathname;
61
+ }
62
+ function getLocalizedPath(pathname, targetLocale, config = defaultI18nConfig) {
63
+ const basePath = removeLocalePrefix(pathname, config);
64
+ if (targetLocale === config.defaultLocale && !config.routing.prefixDefaultLocale) {
65
+ return basePath;
66
+ }
67
+ if (basePath === "/") {
68
+ return `/${targetLocale}`;
69
+ }
70
+ return `/${targetLocale}${basePath}`;
71
+ }
72
+ function getAlternateLinks(pathname, baseUrl, config = defaultI18nConfig) {
73
+ const links = [];
74
+ for (const locale of config.locales) {
75
+ const localizedPath = getLocalizedPath(pathname, locale.code, config);
76
+ links.push({
77
+ locale: locale.code,
78
+ url: `${baseUrl.replace(/\/$/, "")}${localizedPath}`,
79
+ hreflang: locale.htmlLang
80
+ });
81
+ }
82
+ const defaultPath = getLocalizedPath(
83
+ pathname,
84
+ config.defaultLocale,
85
+ config
86
+ );
87
+ links.push({
88
+ locale: "x-default",
89
+ url: `${baseUrl.replace(/\/$/, "")}${defaultPath}`,
90
+ hreflang: "x-default"
91
+ });
92
+ return links;
93
+ }
94
+ function deepMerge(base, override) {
95
+ const result = { ...base };
96
+ for (const key in override) {
97
+ if (Object.prototype.hasOwnProperty.call(override, key)) {
98
+ const overrideValue = override[key];
99
+ const baseValue = base[key];
100
+ if (typeof overrideValue === "object" && overrideValue !== null && !Array.isArray(overrideValue) && typeof baseValue === "object" && baseValue !== null && !Array.isArray(baseValue)) {
101
+ result[key] = deepMerge(baseValue, overrideValue);
102
+ } else if (overrideValue !== void 0) {
103
+ result[key] = overrideValue;
104
+ }
105
+ }
106
+ }
107
+ return result;
108
+ }
109
+ function getLocaleConfig(locale, config = defaultI18nConfig) {
110
+ const localeData = getLocaleByCode(locale, config);
111
+ const localeOverrides = config.localeConfigs[locale] || {};
112
+ const localeInfo = localeData || {
113
+ code: locale,
114
+ name: locale,
115
+ htmlLang: locale,
116
+ dateLocale: locale,
117
+ direction: "ltr"
118
+ };
119
+ const site = deepMerge(defaultSiteConfig, localeOverrides.site || {});
120
+ const menu2 = localeOverrides.menu || defaultMenu;
121
+ const footer = deepMerge(defaultFooterConfig, localeOverrides.footer || {});
122
+ const sidebar = localeOverrides.sidebar ? {
123
+ ...sidebarConfig,
124
+ ...localeOverrides.sidebar,
125
+ // Use locale-specific groups if provided, otherwise keep default
126
+ groups: localeOverrides.sidebar.groups || sidebarConfig.groups
127
+ } : sidebarConfig;
128
+ const ui = getUITranslations(locale, config);
129
+ return {
130
+ locale: localeInfo,
131
+ site,
132
+ menu: menu2,
133
+ footer,
134
+ sidebar,
135
+ ui
136
+ };
137
+ }
138
+ function t(key, locale, config) {
139
+ const translations = getUITranslations(locale, config);
140
+ return translations[key] || key;
141
+ }
142
+ function formatDate(date, locale, options) {
143
+ const dateObj = typeof date === "string" ? new Date(date) : date;
144
+ const defaultOptions = {
145
+ year: "numeric",
146
+ month: "long",
147
+ day: "numeric"
148
+ };
149
+ return new Intl.DateTimeFormat(locale, options || defaultOptions).format(
150
+ dateObj
151
+ );
152
+ }
153
+ function formatDateShort(date, locale) {
154
+ const dateObj = typeof date === "string" ? new Date(date) : date;
155
+ return new Intl.DateTimeFormat(locale, {
156
+ year: "numeric",
157
+ month: "numeric",
158
+ day: "numeric"
159
+ }).format(dateObj);
160
+ }
161
+ function isRTL(locale, config = defaultI18nConfig) {
162
+ const localeData = getLocaleByCode(locale, config);
163
+ return localeData?.direction === "rtl";
164
+ }
165
+ function getTextDirection(locale, config = defaultI18nConfig) {
166
+ return isRTL(locale, config) ? "rtl" : "ltr";
167
+ }
168
+ function isMultiLanguageEnabled(config = defaultI18nConfig) {
169
+ return config.locales.length > 1;
170
+ }
171
+ function getLocalePrefix(locale, config = defaultI18nConfig) {
172
+ if (locale === config.defaultLocale && !config.routing.prefixDefaultLocale) {
173
+ return "";
174
+ }
175
+ return `/${locale}`;
176
+ }
177
+ function getContentPathPrefix(locale, config = defaultI18nConfig) {
178
+ const localeConfig = config.localeConfigs[locale];
179
+ return localeConfig?.contentPathPrefix;
180
+ }
181
+ function filterPostsByLocale(posts, locale, config = defaultI18nConfig) {
182
+ const contentPathPrefix = getContentPathPrefix(locale, config);
183
+ if (!contentPathPrefix) {
184
+ return posts;
185
+ }
186
+ return posts.filter((post) => {
187
+ const postPath = post.id.toLowerCase();
188
+ const prefix = contentPathPrefix.toLowerCase();
189
+ return postPath.startsWith(prefix + "/") || postPath === prefix;
190
+ });
191
+ }
192
+
193
+ // src/utils/useI18n.ts
194
+ import { inject, computed } from "vue";
195
+ var I18N_LOCALE_KEY = /* @__PURE__ */ Symbol("i18n-locale");
196
+ var I18N_CONFIG_KEY = /* @__PURE__ */ Symbol("i18n-config");
197
+ var I18N_TRANSLATIONS_KEY = /* @__PURE__ */ Symbol("i18n-translations");
198
+ function useI18n() {
199
+ const injectedLocale = inject(I18N_LOCALE_KEY, "zh-CN");
200
+ const injectedTranslations = inject(
201
+ I18N_TRANSLATIONS_KEY,
202
+ void 0
203
+ );
204
+ const injectedConfig = inject(I18N_CONFIG_KEY, void 0);
205
+ const locale = computed(() => injectedLocale);
206
+ const translations = computed(() => {
207
+ if (injectedTranslations) {
208
+ return injectedTranslations;
209
+ }
210
+ return getUITranslations(injectedLocale, injectedConfig);
211
+ });
212
+ function t2(key) {
213
+ return translations.value[key] || key;
214
+ }
215
+ function formatDate2(date, options) {
216
+ const dateObj = typeof date === "string" ? new Date(date) : date;
217
+ const defaultOptions = {
218
+ year: "numeric",
219
+ month: "long",
220
+ day: "numeric"
221
+ };
222
+ return new Intl.DateTimeFormat(
223
+ locale.value,
224
+ options || defaultOptions
225
+ ).format(dateObj);
226
+ }
227
+ function formatDateShort2(date) {
228
+ const dateObj = typeof date === "string" ? new Date(date) : date;
229
+ return new Intl.DateTimeFormat(locale.value, {
230
+ year: "numeric",
231
+ month: "numeric",
232
+ day: "numeric"
233
+ }).format(dateObj);
234
+ }
235
+ return {
236
+ locale,
237
+ t: t2,
238
+ formatDate: formatDate2,
239
+ formatDateShort: formatDateShort2,
240
+ translations
241
+ };
242
+ }
243
+ function createI18nContext(locale, config) {
244
+ return {
245
+ locale,
246
+ translations: getUITranslations(locale, config),
247
+ config
248
+ };
249
+ }
25
250
 
26
251
  // src/index.ts
27
252
  function defineBlogConfig(config) {
@@ -41,10 +266,17 @@ function getAstroConfig(options) {
41
266
  };
42
267
  }
43
268
  export {
269
+ I18N_CONFIG_KEY,
270
+ I18N_LOCALE_KEY,
271
+ I18N_TRANSLATIONS_KEY,
44
272
  integration_default as astroBlog,
45
273
  astroBlogIntegration,
274
+ builtInTranslations,
275
+ createI18nContext,
46
276
  defaultFooterConfig,
277
+ defaultI18nConfig,
47
278
  defaultIcons,
279
+ defaultLocales,
48
280
  defaultMenu,
49
281
  defaultSEO,
50
282
  defaultSidebarConfig,
@@ -52,14 +284,34 @@ export {
52
284
  defaultSocialLinks,
53
285
  defineBlogConfig,
54
286
  defineFooterConfig,
287
+ defineI18nConfig,
55
288
  defineMenu,
56
289
  defineSidebarConfig,
57
290
  defineSiteConfig,
58
291
  defineSocialLinks,
292
+ enTranslations,
293
+ filterPostsByLocale,
59
294
  footerConfig,
295
+ formatDate,
296
+ formatDateShort,
297
+ getAlternateLinks,
60
298
  getAstroConfig,
299
+ getContentPathPrefix,
300
+ getLocaleByCode,
301
+ getLocaleConfig,
302
+ getLocaleFromPath,
303
+ getLocalePrefix,
304
+ getLocalizedPath,
305
+ getTextDirection,
306
+ getUITranslations,
307
+ isMultiLanguageEnabled,
308
+ isRTL,
61
309
  menu,
310
+ removeLocalePrefix,
62
311
  sidebarConfig,
63
312
  siteConfig,
64
- socialLinks
313
+ socialLinks,
314
+ t,
315
+ useI18n,
316
+ zhCNTranslations
65
317
  };
@@ -1,9 +1,13 @@
1
1
  import { AstroIntegration } from 'astro';
2
+ import { I as I18nConfig } from './i18n-PgMCFBw0.js';
3
+ import './types/index.js';
4
+ import './sidebar-Da-W_4Lr.js';
2
5
 
3
6
  /**
4
7
  * @jet-w/astro-blog Integration
5
8
  *
6
9
  * This integration injects the blog pages into your Astro project
10
+ * with multi-language support.
7
11
  */
8
12
 
9
13
  interface AstroBlogIntegrationOptions {
@@ -19,7 +23,11 @@ interface AstroBlogIntegrationOptions {
19
23
  search?: boolean;
20
24
  rss?: boolean;
21
25
  };
26
+ /**
27
+ * i18n configuration for multi-language support
28
+ */
29
+ i18n?: I18nConfig;
22
30
  }
23
31
  declare function astroBlogIntegration(options?: AstroBlogIntegrationOptions): AstroIntegration;
24
32
 
25
- export { type AstroBlogIntegrationOptions, astroBlogIntegration, astroBlogIntegration as default };
33
+ export { type AstroBlogIntegrationOptions, I18nConfig, astroBlogIntegration, astroBlogIntegration as default };
@@ -1,7 +1,8 @@
1
1
  import {
2
2
  astroBlogIntegration,
3
3
  integration_default
4
- } from "./chunk-MQXPSOYB.js";
4
+ } from "./chunk-6D3XRDNY.js";
5
+ import "./chunk-A2E2VSAQ.js";
5
6
  export {
6
7
  astroBlogIntegration,
7
8
  integration_default as default
@@ -89,4 +89,4 @@ declare const sidebarConfig: SidebarConfig;
89
89
  declare function defineSidebarConfig(config: Partial<SidebarConfig>): SidebarConfig;
90
90
  declare const defaultSidebarConfig: SidebarConfig;
91
91
 
92
- export { type DividerConfig as D, type ManualConfig as M, type PathMatchConfig as P, type SidebarConfig as S, defineSidebarConfig as a, type SidebarGroup as b, type SidebarItem as c, defaultSidebarConfig as d, type ScanConfig as e, type MixedConfig as f, sidebarConfig as s };
92
+ export { type DividerConfig as D, type ManualConfig as M, type PathMatchConfig as P, type SidebarItem as S, type SidebarGroup as a, type SidebarConfig as b, defineSidebarConfig as c, defaultSidebarConfig as d, type ScanConfig as e, type MixedConfig as f, sidebarConfig as s };
@@ -1,4 +1,4 @@
1
- import { c as SidebarItem, b as SidebarGroup, S as SidebarConfig } from '../sidebar-DNdiCKBw.js';
1
+ import { S as SidebarItem, a as SidebarGroup, b as SidebarConfig } from '../sidebar-Da-W_4Lr.js';
2
2
 
3
3
  /**
4
4
  * 侧边栏工具函数
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jet-w/astro-blog",
3
- "version": "0.1.6",
3
+ "version": "0.2.1",
4
4
  "description": "A modern Astro blog theme with Vue and Tailwind CSS support",
5
5
  "type": "module",
6
6
  "exports": {
@@ -9,7 +9,7 @@
9
9
  <button
10
10
  class="p-3 bg-white dark:bg-slate-800 rounded-full shadow-lg border border-slate-200 dark:border-slate-700 hover:bg-slate-50 dark:hover:bg-slate-700 transition-colors"
11
11
  :class="{ 'bg-primary-50 dark:bg-primary-900/30': showToc }"
12
- aria-label="显示目录"
12
+ :aria-label="tocTitle"
13
13
  >
14
14
  <svg class="w-5 h-5 text-slate-600 dark:text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
15
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 10h16M4 14h16M4 18h16" />
@@ -35,7 +35,7 @@
35
35
  <svg class="w-4 h-4 text-primary-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
36
36
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
37
37
  </svg>
38
- 页面目录
38
+ {{ tocTitle }}
39
39
  </h3>
40
40
  </div>
41
41
 
@@ -66,7 +66,7 @@
66
66
  <!-- 进度条 -->
67
67
  <div class="px-4 py-2 border-t border-slate-200 dark:border-slate-700 bg-slate-50 dark:bg-slate-900/50">
68
68
  <div class="flex items-center justify-between text-xs text-slate-500 dark:text-slate-400 mb-1">
69
- <span>阅读进度</span>
69
+ <span>{{ progressTitle }}</span>
70
70
  <span>{{ Math.round(progress * 100) }}%</span>
71
71
  </div>
72
72
  <div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-1.5">
@@ -91,6 +91,14 @@ interface Heading {
91
91
  level: number
92
92
  }
93
93
 
94
+ const props = withDefaults(defineProps<{
95
+ tocTitle?: string
96
+ progressTitle?: string
97
+ }>(), {
98
+ tocTitle: 'Table of Contents',
99
+ progressTitle: 'Reading Progress',
100
+ })
101
+
94
102
  const showToc = ref(false)
95
103
  const headings = ref<Heading[]>([])
96
104
  const activeId = ref('')
@@ -1,5 +1,20 @@
1
1
  ---
2
2
  import { siteConfig } from '@jet-w/astro-blog/config';
3
+ import type { I18nConfig } from '../../config/i18n';
4
+ import { defaultI18nConfig } from '../../config/i18n';
5
+ import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
6
+ import { getLocaleFromPath, getLocaleConfig } from '../../utils/i18n';
7
+
8
+ export interface Props {
9
+ i18nConfig?: I18nConfig;
10
+ }
11
+
12
+ const { i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
13
+
14
+ // Get current locale from URL
15
+ const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
16
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
17
+ const ui = localeConfig.ui;
3
18
  ---
4
19
 
5
20
  <section class="py-16 mb-16">
@@ -89,7 +104,7 @@ import { siteConfig } from '@jet-w/astro-blog/config';
89
104
  href="/posts"
90
105
  class="btn-primary inline-flex items-center space-x-2"
91
106
  >
92
- <span>浏览文章</span>
107
+ <span>{ui.browsePosts}</span>
93
108
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
94
109
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
95
110
  </svg>
@@ -99,7 +114,7 @@ import { siteConfig } from '@jet-w/astro-blog/config';
99
114
  href="/about"
100
115
  class="btn-secondary inline-flex items-center space-x-2"
101
116
  >
102
- <span>关于我</span>
117
+ <span>{ui.aboutMe}</span>
103
118
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
104
119
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
105
120
  </svg>
@@ -26,7 +26,7 @@
26
26
  <a
27
27
  v-for="tag in tags"
28
28
  :key="tag.name"
29
- :href="`/tags/${encodeTag(tag.name)}`"
29
+ :href="`${props.localePrefix}/tags/${encodeTag(tag.name)}`"
30
30
  class="inline-flex items-center gap-1.5 px-3 py-1.5 rounded-full text-sm transition-colors"
31
31
  :class="getTagColorClass(tag.count)"
32
32
  >
@@ -40,7 +40,7 @@
40
40
  <a
41
41
  v-for="archive in archives"
42
42
  :key="archive.key"
43
- :href="`/archives/${archive.year}/${archive.month}`"
43
+ :href="`${props.localePrefix}/archives/${archive.year}/${archive.month}`"
44
44
  class="flex items-center justify-between px-3 py-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors group"
45
45
  >
46
46
  <div class="flex items-center gap-3">
@@ -48,11 +48,11 @@
48
48
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
49
49
  </svg>
50
50
  <span class="text-slate-700 dark:text-slate-300 group-hover:text-primary-600 dark:group-hover:text-primary-400">
51
- {{ archive.year }}年{{ archive.month }}
51
+ {{ formatArchiveDate(archive.year, archive.month) }}
52
52
  </span>
53
53
  </div>
54
54
  <span class="text-xs text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-0.5 rounded-full">
55
- {{ archive.count }}
55
+ {{ archive.count }} {{ props.ui?.postsCount || 'posts' }}
56
56
  </span>
57
57
  </a>
58
58
  </div>
@@ -62,7 +62,7 @@
62
62
  <a
63
63
  v-for="category in categories"
64
64
  :key="category.name"
65
- :href="`/categories/${encodeCategory(category.name)}`"
65
+ :href="`${props.localePrefix}/categories/${encodeCategory(category.name)}`"
66
66
  class="flex items-center justify-between px-3 py-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors group"
67
67
  >
68
68
  <div class="flex items-center gap-3">
@@ -74,7 +74,7 @@
74
74
  </span>
75
75
  </div>
76
76
  <span class="text-xs text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-0.5 rounded-full">
77
- {{ category.count }}
77
+ {{ category.count }} {{ props.ui?.postsCount || 'posts' }}
78
78
  </span>
79
79
  </a>
80
80
  </div>
@@ -86,7 +86,7 @@
86
86
  <div v-for="item in timeline" :key="item.slug" class="relative pl-10">
87
87
  <div class="absolute left-2.5 w-3 h-3 rounded-full bg-primary-500 border-2 border-white dark:border-slate-800"></div>
88
88
  <a
89
- :href="`/posts/${item.slug}`"
89
+ :href="`${props.localePrefix}/posts/${item.slug}`"
90
90
  class="block p-3 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-700/50 transition-colors group"
91
91
  >
92
92
  <div class="text-xs text-slate-400 mb-1">{{ formatDate(item.pubDate) }}</div>
@@ -97,10 +97,10 @@
97
97
  </div>
98
98
  </div>
99
99
  <a
100
- href="/archives"
100
+ :href="`${props.localePrefix}/archives`"
101
101
  class="mt-4 flex items-center justify-center gap-2 py-2 text-sm text-primary-500 hover:text-primary-600 dark:hover:text-primary-400 transition-colors"
102
102
  >
103
- <span>查看全部时间轴</span>
103
+ <span>{{ props.ui?.viewAllTimeline || 'View all timeline' }}</span>
104
104
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
105
105
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
106
106
  </svg>
@@ -136,14 +136,37 @@ interface TimelineItem {
136
136
  pubDate: string
137
137
  }
138
138
 
139
+ interface UIProps {
140
+ tags?: string
141
+ archives?: string
142
+ categories?: string
143
+ timeline?: string
144
+ viewAllTimeline?: string
145
+ postsCount?: string
146
+ }
147
+
139
148
  interface Props {
140
149
  tags: TagItem[]
141
150
  archives: ArchiveItem[]
142
151
  categories: CategoryItem[]
143
152
  timeline: TimelineItem[]
153
+ localePrefix?: string
154
+ dateLocale?: string
155
+ ui?: UIProps
144
156
  }
145
157
 
146
- const props = defineProps<Props>()
158
+ const props = withDefaults(defineProps<Props>(), {
159
+ localePrefix: '',
160
+ dateLocale: 'en',
161
+ ui: () => ({
162
+ tags: 'Tags',
163
+ archives: 'Archives',
164
+ categories: 'Categories',
165
+ timeline: 'Timeline',
166
+ viewAllTimeline: 'View all timeline',
167
+ postsCount: 'posts',
168
+ })
169
+ })
147
170
 
148
171
  const activeTab = ref('tags')
149
172
 
@@ -201,10 +224,10 @@ const TimelineIcon = {
201
224
  }
202
225
 
203
226
  const tabs = computed(() => [
204
- { id: 'tags', name: '标签', count: props.tags.length, icon: TagIcon },
205
- { id: 'archives', name: '归档', count: props.archives.length, icon: ArchiveIcon },
206
- { id: 'categories', name: '分类', count: props.categories.length, icon: CategoryIcon },
207
- { id: 'timeline', name: '时间轴', count: props.timeline.length, icon: TimelineIcon },
227
+ { id: 'tags', name: props.ui?.tags || 'Tags', count: props.tags.length, icon: TagIcon },
228
+ { id: 'archives', name: props.ui?.archives || 'Archives', count: props.archives.length, icon: ArchiveIcon },
229
+ { id: 'categories', name: props.ui?.categories || 'Categories', count: props.categories.length, icon: CategoryIcon },
230
+ { id: 'timeline', name: props.ui?.timeline || 'Timeline', count: props.timeline.length, icon: TimelineIcon },
208
231
  ])
209
232
 
210
233
  const encodeTag = (tag: string) => {
@@ -227,12 +250,20 @@ const getTagColorClass = (count: number) => {
227
250
 
228
251
  const formatDate = (dateStr: string) => {
229
252
  const date = new Date(dateStr)
230
- return new Intl.DateTimeFormat('zh-CN', {
253
+ return new Intl.DateTimeFormat(props.dateLocale, {
231
254
  year: 'numeric',
232
255
  month: '2-digit',
233
256
  day: '2-digit'
234
257
  }).format(date)
235
258
  }
259
+
260
+ const formatArchiveDate = (year: string, month: string) => {
261
+ const date = new Date(parseInt(year), parseInt(month) - 1, 1)
262
+ return new Intl.DateTimeFormat(props.dateLocale, {
263
+ year: 'numeric',
264
+ month: 'long'
265
+ }).format(date)
266
+ }
236
267
  </script>
237
268
 
238
269
  <style scoped>
@@ -13,12 +13,25 @@ export interface Props {
13
13
  };
14
14
  featured?: boolean;
15
15
  layout?: 'vertical' | 'horizontal';
16
+ localePrefix?: string;
17
+ locale?: string;
18
+ ui?: {
19
+ readMore?: string;
20
+ minuteRead?: string;
21
+ };
16
22
  }
17
23
 
18
- const { post, featured = false, layout = 'vertical' } = Astro.props;
24
+ const {
25
+ post,
26
+ featured = false,
27
+ layout = 'vertical',
28
+ localePrefix = '',
29
+ locale = 'zh-CN',
30
+ ui = { readMore: '阅读更多', minuteRead: '分钟' }
31
+ } = Astro.props;
19
32
 
20
33
  const formattedDate = post.pubDate
21
- ? new Intl.DateTimeFormat('zh-CN', {
34
+ ? new Intl.DateTimeFormat(locale, {
22
35
  year: 'numeric',
23
36
  month: 'long',
24
37
  day: 'numeric'
@@ -31,12 +44,17 @@ const isHorizontal = layout === 'horizontal';
31
44
  const tagToSlug = (tag: string) => tag.toLowerCase().replace(/\s+/g, '-');
32
45
  // 将分类名转换为 slug 格式
33
46
  const categoryToSlug = (category: string) => category.toLowerCase().replace(/\s+/g, '-');
47
+
48
+ // Build URLs with locale prefix
49
+ const postUrl = `${localePrefix}/posts/${post.slug}`;
50
+ const tagUrl = (tag: string) => `${localePrefix}/tags/${tagToSlug(tag)}`;
51
+ const categoryUrl = (category: string) => `${localePrefix}/categories/${categoryToSlug(category)}`;
34
52
  ---
35
53
 
36
54
  <article class={`group card hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 ${isHorizontal ? 'flex gap-6 items-center' : 'block'}`}>
37
55
  <!-- 文章图片 -->
38
56
  {post.image && (
39
- <a href={`/posts/${post.slug}`} class={`block overflow-hidden rounded-lg ${
57
+ <a href={postUrl} class={`block overflow-hidden rounded-lg ${
40
58
  isHorizontal ? 'w-48 h-32 flex-shrink-0' : 'w-full h-48 mb-4'
41
59
  }`}>
42
60
  <img
@@ -55,7 +73,7 @@ const categoryToSlug = (category: string) => category.toLowerCase().replace(/\s+
55
73
  <div class="flex flex-wrap gap-2 mb-2">
56
74
  {post.categories.slice(0, 2).map((category) => (
57
75
  <a
58
- href={`/categories/${categoryToSlug(category)}`}
76
+ href={categoryUrl(category)}
59
77
  class="text-xs px-2 py-1 bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-300 rounded-full hover:bg-amber-200 dark:hover:bg-amber-900/50 transition-colors"
60
78
  onclick="event.stopPropagation();"
61
79
  >
@@ -70,7 +88,7 @@ const categoryToSlug = (category: string) => category.toLowerCase().replace(/\s+
70
88
  <div class="flex flex-wrap gap-2 mb-3">
71
89
  {post.tags.slice(0, 3).map((tag) => (
72
90
  <a
73
- href={`/tags/${tagToSlug(tag)}`}
91
+ href={tagUrl(tag)}
74
92
  class="text-xs px-2 py-1 bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 rounded-full hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors"
75
93
  onclick="event.stopPropagation();"
76
94
  >
@@ -86,7 +104,7 @@ const categoryToSlug = (category: string) => category.toLowerCase().replace(/\s+
86
104
  )}
87
105
 
88
106
  <!-- 标题 -->
89
- <a href={`/posts/${post.slug}`}>
107
+ <a href={postUrl}>
90
108
  <h3 class={`font-bold text-slate-900 dark:text-slate-100 hover:text-primary-500 transition-colors line-clamp-2 mb-3 ${
91
109
  featured ? 'text-xl' : isHorizontal ? 'text-lg' : 'text-lg'
92
110
  }`}>
@@ -95,7 +113,7 @@ const categoryToSlug = (category: string) => category.toLowerCase().replace(/\s+
95
113
  </a>
96
114
 
97
115
  <!-- 描述 -->
98
- <a href={`/posts/${post.slug}`}>
116
+ <a href={postUrl}>
99
117
  <p class={`text-slate-600 dark:text-slate-400 line-clamp-3 mb-4 hover:text-slate-700 dark:hover:text-slate-300 transition-colors ${
100
118
  isHorizontal ? 'text-sm' : 'text-base'
101
119
  }`}>
@@ -129,13 +147,13 @@ const categoryToSlug = (category: string) => category.toLowerCase().replace(/\s+
129
147
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
130
148
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
131
149
  </svg>
132
- <span>{post.readingTime}分钟</span>
150
+ <span>{post.readingTime} {ui.minuteRead}</span>
133
151
  </span>
134
152
  )}
135
153
  </div>
136
154
 
137
- <a href={`/posts/${post.slug}`} class="flex items-center text-primary-500 hover:text-primary-600 transition-colors">
138
- <span class="text-sm font-medium">阅读更多</span>
155
+ <a href={postUrl} class="flex items-center text-primary-500 hover:text-primary-600 transition-colors">
156
+ <span class="text-sm font-medium">{ui.readMore}</span>
139
157
  <svg class="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
140
158
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
141
159
  </svg>