@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
@@ -6,9 +6,25 @@ export interface Props {
6
6
  tags?: string[];
7
7
  categories?: string[];
8
8
  };
9
+ ui?: {
10
+ relatedPosts?: string;
11
+ readMore?: string;
12
+ minuteRead?: string;
13
+ postList?: string;
14
+ };
15
+ localePrefix?: string;
9
16
  }
10
17
 
11
- const { currentPost } = Astro.props;
18
+ const {
19
+ currentPost,
20
+ ui = {
21
+ relatedPosts: '相关文章',
22
+ readMore: '阅读更多',
23
+ minuteRead: '分钟',
24
+ postList: '查看更多文章',
25
+ },
26
+ localePrefix = '',
27
+ } = Astro.props;
12
28
 
13
29
  // 模拟获取相关文章
14
30
  const allPosts = [
@@ -83,14 +99,14 @@ const formattedDate = (date: Date) => new Intl.DateTimeFormat('zh-CN', {
83
99
  {relatedPosts.length > 0 && (
84
100
  <section class="mt-16">
85
101
  <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100 mb-8">
86
- 相关文章
102
+ {ui.relatedPosts}
87
103
  </h2>
88
104
 
89
105
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
90
106
  {relatedPosts.map((post) => (
91
107
  <article class="group">
92
108
  <a
93
- href={`/posts/${post.slug}`}
109
+ href={`${localePrefix}/posts/${post.slug}`}
94
110
  class="card hover:shadow-lg transform hover:-translate-y-1 transition-all duration-300 block h-full"
95
111
  >
96
112
  <div class="flex flex-col h-full">
@@ -100,7 +116,7 @@ const formattedDate = (date: Date) => new Intl.DateTimeFormat('zh-CN', {
100
116
  {formattedDate(post.pubDate)}
101
117
  </time>
102
118
  {post.readingTime && (
103
- <span>{post.readingTime}分钟</span>
119
+ <span>{post.readingTime} {ui.minuteRead}</span>
104
120
  )}
105
121
  </div>
106
122
 
@@ -132,7 +148,7 @@ const formattedDate = (date: Date) => new Intl.DateTimeFormat('zh-CN', {
132
148
 
133
149
  <!-- 阅读链接 -->
134
150
  <div class="flex items-center text-primary-500 group-hover:text-primary-600 transition-colors text-sm font-medium">
135
- <span>阅读更多</span>
151
+ <span>{ui.readMore}</span>
136
152
  <svg class="w-4 h-4 ml-1 group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
137
153
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
138
154
  </svg>
@@ -146,10 +162,10 @@ const formattedDate = (date: Date) => new Intl.DateTimeFormat('zh-CN', {
146
162
  <!-- 查看更多按钮 -->
147
163
  <div class="text-center mt-8">
148
164
  <a
149
- href="/posts"
165
+ href={`${localePrefix}/posts`}
150
166
  class="btn-secondary inline-flex items-center space-x-2"
151
167
  >
152
- <span>查看更多文章</span>
168
+ <span>{ui.postList}</span>
153
169
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
154
170
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
155
171
  </svg>
@@ -5,15 +5,21 @@ export interface Props {
5
5
  text: string;
6
6
  id: string;
7
7
  }>;
8
+ tocTitle?: string;
9
+ progressTitle?: string;
8
10
  }
9
11
 
10
- const { headings } = Astro.props;
12
+ const {
13
+ headings,
14
+ tocTitle = 'Table of Contents',
15
+ progressTitle = 'Reading Progress',
16
+ } = Astro.props;
11
17
  ---
12
18
 
13
19
  {headings.length > 0 && (
14
- <nav class="sticky top-20 p-6" aria-label="目录">
20
+ <nav class="sticky top-20 p-6" aria-label={tocTitle}>
15
21
  <h3 class="text-lg font-semibold text-slate-900 dark:text-slate-100 mb-4 border-b border-slate-200 dark:border-slate-700 pb-2">
16
- 目录
22
+ {tocTitle}
17
23
  </h3>
18
24
 
19
25
  <ol class="space-y-2 text-sm">
@@ -38,7 +44,7 @@ const { headings } = Astro.props;
38
44
 
39
45
  <!-- 进度指示器 -->
40
46
  <div class="mt-8 pt-4 border-t border-slate-200 dark:border-slate-700">
41
- <div class="text-xs text-slate-500 dark:text-slate-400 mb-2">阅读进度</div>
47
+ <div class="text-xs text-slate-500 dark:text-slate-400 mb-2">{progressTitle}</div>
42
48
  <div class="w-full bg-slate-200 dark:bg-slate-700 rounded-full h-2">
43
49
  <div class="bg-primary-500 h-2 rounded-full transition-all duration-300" id="reading-progress" style="width: 0%"></div>
44
50
  </div>
@@ -7,9 +7,10 @@ export interface Props {
7
7
  }>;
8
8
  maxSize?: number;
9
9
  minSize?: number;
10
+ localePrefix?: string;
10
11
  }
11
12
 
12
- const { tags, maxSize = 2.5, minSize = 0.8 } = Astro.props;
13
+ const { tags, maxSize = 2.5, minSize = 0.8, localePrefix = '' } = Astro.props;
13
14
 
14
15
  // 计算字体大小
15
16
  const maxCount = Math.max(...tags.map(tag => tag.count));
@@ -48,10 +49,10 @@ const shuffledTags = [...tags].sort(() => Math.random() - 0.5);
48
49
  <div class="flex flex-wrap items-center justify-center gap-3 leading-relaxed">
49
50
  {shuffledTags.map((tag, index) => (
50
51
  <a
51
- href={`/tags/${tag.slug}`}
52
+ href={`${localePrefix}/tags/${tag.slug}`}
52
53
  class={`inline-block font-medium transition-all duration-300 hover:scale-110 ${getColorClass(index)}`}
53
54
  style={`font-size: ${getFontSize(tag.count)}rem`}
54
- title={`${tag.name} (${tag.count} 篇文章)`}
55
+ title={`${tag.name} (${tag.count})`}
55
56
  >
56
57
  {tag.name}
57
58
  </a>
@@ -4,19 +4,33 @@
4
4
  */
5
5
  import PostCard from '../../components/blog/PostCard.astro';
6
6
  import { getCollection } from 'astro:content';
7
+ import type { I18nConfig } from '../../config/i18n';
8
+ import { defaultI18nConfig } from '../../config/i18n';
9
+ import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
10
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale } from '../../utils/i18n';
7
11
 
8
12
  export interface Props {
9
13
  count?: number;
14
+ i18nConfig?: I18nConfig;
10
15
  }
11
16
 
12
- const { count = 3 } = Astro.props;
17
+ const { count = 3, i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
18
+
19
+ // Get current locale from URL
20
+ const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
21
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
22
+ const ui = localeConfig.ui;
23
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
13
24
 
14
25
  const allPosts = await getCollection('posts');
15
26
  const publishedPosts = allPosts
16
27
  .filter(post => !post.data.draft)
17
28
  .sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0));
18
29
 
19
- const featuredPosts = publishedPosts.slice(0, count).map(post => ({
30
+ // Filter posts by current locale
31
+ const localePosts = filterPostsByLocale(publishedPosts, currentLocale, i18nConfig);
32
+
33
+ const featuredPosts = localePosts.slice(0, count).map(post => ({
20
34
  slug: post.id.toLowerCase(),
21
35
  title: post.data.title,
22
36
  description: post.data.description,
@@ -26,19 +40,21 @@ const featuredPosts = publishedPosts.slice(0, count).map(post => ({
26
40
  author: post.data.author,
27
41
  readingTime: 5
28
42
  }));
43
+
44
+ const postsUrl = `${localePrefix}/posts`;
29
45
  ---
30
46
 
31
47
  {featuredPosts.length > 0 && (
32
48
  <section class="mb-16">
33
49
  <div class="flex items-center justify-between mb-8">
34
50
  <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">
35
- 特色文章
51
+ {ui.posts}
36
52
  </h2>
37
53
  <a
38
- href="/posts"
54
+ href={postsUrl}
39
55
  class="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400 font-medium text-sm flex items-center space-x-1 transition-colors"
40
56
  >
41
- <span>查看全部</span>
57
+ <span>{ui.allPosts}</span>
42
58
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
43
59
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
44
60
  </svg>
@@ -47,7 +63,7 @@ const featuredPosts = publishedPosts.slice(0, count).map(post => ({
47
63
 
48
64
  <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
49
65
  {featuredPosts.map((post) => (
50
- <PostCard post={post} featured={true} />
66
+ <PostCard post={post} featured={true} localePrefix={localePrefix} locale={localeConfig.locale.dateLocale} ui={ui} />
51
67
  ))}
52
68
  </div>
53
69
  </section>
@@ -4,10 +4,29 @@
4
4
  */
5
5
  import NavigationTabs from '../../components/blog/NavigationTabs.vue';
6
6
  import { getCollection } from 'astro:content';
7
+ import type { I18nConfig } from '../../config/i18n';
8
+ import { defaultI18nConfig } from '../../config/i18n';
9
+ import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
10
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale } from '../../utils/i18n';
7
11
 
8
- const allPosts = await getCollection('posts');
9
- const publishedPosts = allPosts
10
- .filter(post => !post.data.draft)
12
+ interface Props {
13
+ i18nConfig?: I18nConfig;
14
+ }
15
+
16
+ const { i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
17
+
18
+ // Get current locale and translations
19
+ const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
20
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
21
+ const ui = localeConfig.ui;
22
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
23
+
24
+ const allPosts = await getCollection('posts', ({ data }) => !data.draft);
25
+
26
+ // Filter posts by locale
27
+ const localePosts = filterPostsByLocale(allPosts, currentLocale, i18nConfig);
28
+
29
+ const publishedPosts = localePosts
11
30
  .sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0));
12
31
 
13
32
  const posts = publishedPosts.map(post => ({
@@ -68,7 +87,7 @@ const timelineData = posts.slice(0, 10).map(post => ({
68
87
  <section class="mb-16">
69
88
  <div class="flex items-center justify-between mb-8">
70
89
  <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">
71
- 快速导航
90
+ {ui.quickNavigation}
72
91
  </h2>
73
92
  </div>
74
93
  <NavigationTabs
@@ -77,5 +96,15 @@ const timelineData = posts.slice(0, 10).map(post => ({
77
96
  archives={archivesData}
78
97
  categories={categoriesData}
79
98
  timeline={timelineData}
99
+ localePrefix={localePrefix}
100
+ dateLocale={localeConfig.locale?.dateLocale || currentLocale}
101
+ ui={{
102
+ tags: ui.tags,
103
+ archives: ui.archives,
104
+ categories: ui.categories,
105
+ timeline: ui.timeline,
106
+ viewAllTimeline: ui.viewAllTimeline,
107
+ postsCount: ui.postsCount,
108
+ }}
80
109
  />
81
110
  </section>
@@ -4,19 +4,33 @@
4
4
  */
5
5
  import PostCard from '../../components/blog/PostCard.astro';
6
6
  import { getCollection } from 'astro:content';
7
+ import type { I18nConfig } from '../../config/i18n';
8
+ import { defaultI18nConfig } from '../../config/i18n';
9
+ import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
10
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, filterPostsByLocale } from '../../utils/i18n';
7
11
 
8
12
  export interface Props {
9
13
  count?: number;
14
+ i18nConfig?: I18nConfig;
10
15
  }
11
16
 
12
- const { count = 6 } = Astro.props;
17
+ const { count = 6, i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
18
+
19
+ // Get current locale from URL
20
+ const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
21
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
22
+ const ui = localeConfig.ui;
23
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
13
24
 
14
25
  const allPosts = await getCollection('posts');
15
26
  const publishedPosts = allPosts
16
27
  .filter(post => !post.data.draft)
17
28
  .sort((a, b) => (b.data.pubDate?.getTime() ?? 0) - (a.data.pubDate?.getTime() ?? 0));
18
29
 
19
- const recentPosts = publishedPosts.slice(0, count).map(post => ({
30
+ // Filter posts by current locale
31
+ const localePosts = filterPostsByLocale(publishedPosts, currentLocale, i18nConfig);
32
+
33
+ const recentPosts = localePosts.slice(0, count).map(post => ({
20
34
  slug: post.id.toLowerCase(),
21
35
  title: post.data.title,
22
36
  description: post.data.description,
@@ -26,18 +40,20 @@ const recentPosts = publishedPosts.slice(0, count).map(post => ({
26
40
  author: post.data.author,
27
41
  readingTime: 5
28
42
  }));
43
+
44
+ const postsUrl = `${localePrefix}/posts`;
29
45
  ---
30
46
 
31
47
  <section class="mb-16">
32
48
  <div class="flex items-center justify-between mb-8">
33
49
  <h2 class="text-2xl font-bold text-slate-900 dark:text-slate-100">
34
- 最新文章
50
+ {ui.recentPosts}
35
51
  </h2>
36
52
  <a
37
- href="/posts"
53
+ href={postsUrl}
38
54
  class="text-primary-500 hover:text-primary-600 dark:hover:text-primary-400 font-medium text-sm flex items-center space-x-1 transition-colors"
39
55
  >
40
- <span>查看全部</span>
56
+ <span>{ui.allPosts}</span>
41
57
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
42
58
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" />
43
59
  </svg>
@@ -46,7 +62,7 @@ const recentPosts = publishedPosts.slice(0, count).map(post => ({
46
62
 
47
63
  <div class="space-y-6">
48
64
  {recentPosts.map((post) => (
49
- <PostCard post={post} layout="horizontal" />
65
+ <PostCard post={post} layout="horizontal" localePrefix={localePrefix} />
50
66
  ))}
51
67
  </div>
52
68
  </section>
@@ -3,13 +3,31 @@
3
3
  * 首页统计信息组件
4
4
  */
5
5
  import { getCollection } from 'astro:content';
6
+ import type { I18nConfig } from '../../config/i18n';
7
+ import { defaultI18nConfig } from '../../config/i18n';
8
+ import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
9
+ import { getLocaleFromPath, getLocaleConfig, filterPostsByLocale } from '../../utils/i18n';
10
+
11
+ export interface Props {
12
+ i18nConfig?: I18nConfig;
13
+ }
14
+
15
+ const { i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
16
+
17
+ // Get current locale from URL
18
+ const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
19
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
20
+ const ui = localeConfig.ui;
6
21
 
7
22
  const allPosts = await getCollection('posts');
8
23
  const publishedPosts = allPosts.filter(post => !post.data.draft);
9
24
 
10
- const totalPosts = publishedPosts.length;
11
- const allTags = new Set(publishedPosts.flatMap(post => post.data.tags || []));
12
- const allCategories = new Set(publishedPosts.flatMap(post => post.data.categories || []));
25
+ // Filter posts by current locale
26
+ const localePosts = filterPostsByLocale(publishedPosts, currentLocale, i18nConfig);
27
+
28
+ const totalPosts = localePosts.length;
29
+ const allTags = new Set(localePosts.flatMap(post => post.data.tags || []));
30
+ const allCategories = new Set(localePosts.flatMap(post => post.data.categories || []));
13
31
  const totalTags = allTags.size;
14
32
  const totalCategories = allCategories.size;
15
33
  ---
@@ -21,7 +39,7 @@ const totalCategories = allCategories.size;
21
39
  {totalPosts}
22
40
  </div>
23
41
  <div class="text-sm text-slate-600 dark:text-slate-400">
24
- 文章总数
42
+ {ui.posts}
25
43
  </div>
26
44
  </div>
27
45
  <div class="card text-center">
@@ -29,7 +47,7 @@ const totalCategories = allCategories.size;
29
47
  {totalTags}
30
48
  </div>
31
49
  <div class="text-sm text-slate-600 dark:text-slate-400">
32
- 标签数量
50
+ {ui.tags}
33
51
  </div>
34
52
  </div>
35
53
  <div class="card text-center">
@@ -37,7 +55,7 @@ const totalCategories = allCategories.size;
37
55
  {totalCategories}
38
56
  </div>
39
57
  <div class="text-sm text-slate-600 dark:text-slate-400">
40
- 分类数量
58
+ {ui.categories}
41
59
  </div>
42
60
  </div>
43
61
  </div>
@@ -1,5 +1,22 @@
1
1
  ---
2
2
  import { siteConfig, footerConfig, defaultIcons } from '@jet-w/astro-blog/config';
3
+ import type { I18nConfig } from '../../config/i18n';
4
+ import { defaultI18nConfig } from '../../config/i18n';
5
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix } from '../../utils/i18n';
6
+
7
+ export interface Props {
8
+ i18nConfig?: I18nConfig;
9
+ }
10
+
11
+ const { i18nConfig = defaultI18nConfig } = Astro.props;
12
+
13
+ const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
14
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
15
+ const ui = localeConfig.ui;
16
+
17
+ // Use locale-specific configs
18
+ const localeSiteConfig = localeConfig.site;
19
+ const localeFooterConfig = localeConfig.footer;
3
20
 
4
21
  const currentYear = new Date().getFullYear();
5
22
 
@@ -7,43 +24,44 @@ function getIcon(link: { type: string; icon?: string }): string {
7
24
  return link.icon || defaultIcons[link.type] || '';
8
25
  }
9
26
 
10
- // 替换版权信息中的占位符
11
- const copyright = footerConfig.copyright
27
+ // Get locale prefix for RSS link
28
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
29
+ const rssUrl = localeFooterConfig.rssUrl || footerConfig.rssUrl || `${localePrefix}/rss.xml`;
30
+
31
+ const copyright = (localeFooterConfig.copyright || footerConfig.copyright)
12
32
  .replace('{year}', String(currentYear))
13
- .replace('{author}', siteConfig.author);
33
+ .replace('{author}', localeSiteConfig.author || siteConfig.author);
14
34
  ---
15
35
 
16
36
  <footer class="bg-slate-50 dark:bg-slate-800 border-t border-slate-200 dark:border-slate-700 mt-auto w-full min-w-0">
17
37
  <div class="container mx-auto px-4 py-6">
18
38
  <div class="grid grid-cols-1 md:grid-cols-3 gap-6">
19
- <!-- 站点信息 -->
20
39
  <div>
21
40
  <div class="flex items-center space-x-3 mb-3">
22
- {siteConfig.avatar && (
41
+ {(localeSiteConfig.avatar || siteConfig.avatar) && (
23
42
  <img
24
- src={siteConfig.avatar}
25
- alt={siteConfig.title}
43
+ src={localeSiteConfig.avatar || siteConfig.avatar}
44
+ alt={localeSiteConfig.title || siteConfig.title}
26
45
  class="w-8 h-8 rounded-full"
27
46
  />
28
47
  )}
29
48
  <div>
30
49
  <h3 class="text-base font-semibold text-slate-900 dark:text-slate-100">
31
- {siteConfig.title}
50
+ {localeSiteConfig.title || siteConfig.title}
32
51
  </h3>
33
52
  <p class="text-xs text-slate-600 dark:text-slate-400">
34
- {siteConfig.description}
53
+ {localeSiteConfig.description || siteConfig.description}
35
54
  </p>
36
55
  </div>
37
56
  </div>
38
57
  </div>
39
58
 
40
- <!-- 快速链接 -->
41
59
  <div>
42
60
  <h4 class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-3">
43
- {footerConfig.quickLinksTitle}
61
+ {localeFooterConfig.quickLinksTitle || footerConfig.quickLinksTitle || ui.quickLinks}
44
62
  </h4>
45
63
  <div class="flex flex-wrap gap-x-4 gap-y-1">
46
- {footerConfig.quickLinks.map((item) => (
64
+ {(localeFooterConfig.quickLinks || footerConfig.quickLinks).map((item) => (
47
65
  <a
48
66
  href={item.href}
49
67
  class="text-sm text-slate-600 dark:text-slate-400 hover:text-primary-500 transition-colors"
@@ -54,13 +72,12 @@ const copyright = footerConfig.copyright
54
72
  </div>
55
73
  </div>
56
74
 
57
- <!-- 社交链接 -->
58
75
  <div>
59
76
  <h4 class="text-sm font-semibold text-slate-900 dark:text-slate-100 mb-3">
60
- {footerConfig.contactTitle}
77
+ {localeFooterConfig.contactTitle || footerConfig.contactTitle || ui.contact}
61
78
  </h4>
62
79
  <div class="flex space-x-4">
63
- {footerConfig.socialLinks.map((link) => (
80
+ {(localeFooterConfig.socialLinks || footerConfig.socialLinks).map((link) => (
64
81
  <a
65
82
  href={link.url}
66
83
  target={link.type === 'email' ? undefined : '_blank'}
@@ -74,11 +91,11 @@ const copyright = footerConfig.copyright
74
91
  </a>
75
92
  ))}
76
93
 
77
- {footerConfig.showRss && (
94
+ {(localeFooterConfig.showRss ?? footerConfig.showRss) && (
78
95
  <a
79
- href={footerConfig.rssUrl}
96
+ href={rssUrl}
80
97
  class="text-slate-600 dark:text-slate-400 hover:text-primary-500 transition-colors"
81
- aria-label="RSS订阅"
98
+ aria-label={ui.rssFeed}
82
99
  >
83
100
  <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 24 24">
84
101
  <path d="M3.429 2.486c9.36 0 16.943 7.583 16.943 16.943h-3.257c0-7.563-6.123-13.686-13.686-13.686V2.486zM3.429 9.114c5.647 0 10.229 4.581 10.229 10.229h-3.257c0-3.844-3.128-6.972-6.972-6.972V9.114zM6.686 16.486c0 1.8-1.457 3.257-3.257 3.257S0.172 18.286 0.172 16.486s1.457-3.257 3.257-3.257 3.257 1.457 3.257 3.257z"/>
@@ -89,12 +106,11 @@ const copyright = footerConfig.copyright
89
106
  </div>
90
107
  </div>
91
108
 
92
- <!-- 版权信息 -->
93
109
  <div class="border-t border-slate-200 dark:border-slate-700 pt-4 mt-6">
94
110
  <div class="flex flex-col sm:flex-row justify-between items-center text-sm text-slate-600 dark:text-slate-400">
95
111
  <p>{copyright}</p>
96
112
  <p class="mt-2 sm:mt-0">
97
- Powered by <a href={footerConfig.poweredBy.url} class="link" target="_blank" rel="noopener noreferrer">{footerConfig.poweredBy.text}</a>
113
+ Powered by <a href={(localeFooterConfig.poweredBy || footerConfig.poweredBy).url} class="link" target="_blank" rel="noopener noreferrer">{(localeFooterConfig.poweredBy || footerConfig.poweredBy).text}</a>
98
114
  </p>
99
115
  </div>
100
116
  </div>
@@ -3,37 +3,72 @@ import { siteConfig } from '@jet-w/astro-blog/config';
3
3
  import ThemeToggle from '../ui/ThemeToggle.vue';
4
4
  import SearchBox from '../ui/SearchBox.vue';
5
5
  import MobileMenu from '../ui/MobileMenu.vue';
6
+ import LanguageSwitcher from '../ui/LanguageSwitcher.vue';
7
+ import type { I18nConfig } from '../../config/i18n';
8
+ import { defaultI18nConfig } from '../../config/i18n';
9
+ import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
10
+ import {
11
+ getLocaleFromPath,
12
+ getLocaleConfig,
13
+ removeLocalePrefix,
14
+ isMultiLanguageEnabled,
15
+ } from '../../utils/i18n';
16
+
17
+ export interface Props {
18
+ i18nConfig?: I18nConfig;
19
+ }
20
+
21
+ const { i18nConfig = virtualI18nConfig || defaultI18nConfig } = Astro.props;
6
22
 
7
23
  const currentPath = Astro.url.pathname;
24
+ const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
25
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
26
+ const ui = localeConfig.ui;
27
+
28
+ // Use locale-specific site config and menu
29
+ const localeSiteConfig = localeConfig.site;
30
+ const menu = localeConfig.menu;
31
+
32
+ // Get base path without locale prefix for language switcher
33
+ const basePath = removeLocalePrefix(currentPath, i18nConfig);
34
+
35
+ // Check if multi-language is enabled
36
+ const showLanguageSwitcher = isMultiLanguageEnabled(i18nConfig);
37
+
38
+ // Prepare locales for language switcher
39
+ const localesForSwitcher = i18nConfig.locales.map(l => ({
40
+ code: l.code,
41
+ name: l.name,
42
+ }));
8
43
  ---
9
44
 
10
45
  <header class="sticky top-0 z-50 bg-white/80 dark:bg-slate-900/80 backdrop-blur-sm border-b border-slate-200 dark:border-slate-700">
11
46
  <div class="container mx-auto px-4">
12
47
  <nav class="flex items-center justify-between h-16">
13
- <!-- Logo 和站点名称 -->
14
48
  <div class="flex items-center space-x-4">
15
49
  <a href="/" class="flex items-center space-x-3 hover:opacity-80 transition-opacity">
16
- {siteConfig.avatar && (
50
+ {(localeSiteConfig.avatar || siteConfig.avatar) && (
17
51
  <img
18
- src={siteConfig.avatar}
19
- alt={siteConfig.title}
52
+ src={localeSiteConfig.avatar || siteConfig.avatar}
53
+ alt={localeSiteConfig.title || siteConfig.title}
20
54
  class="w-8 h-8 rounded-full"
21
55
  />
22
56
  )}
23
57
  <div class="hidden sm:block">
24
58
  <h1 class="text-xl font-bold text-slate-900 dark:text-slate-100">
25
- {siteConfig.title}
59
+ {localeSiteConfig.title ?? siteConfig.title}
26
60
  </h1>
27
- <p class="text-xs text-slate-600 dark:text-slate-400 -mt-1">
28
- 技术博客
29
- </p>
61
+ {(localeSiteConfig.description ?? siteConfig.description) && (
62
+ <p class="text-xs text-slate-600 dark:text-slate-400 -mt-1">
63
+ {localeSiteConfig.description ?? siteConfig.description}
64
+ </p>
65
+ )}
30
66
  </div>
31
67
  </a>
32
68
  </div>
33
69
 
34
- <!-- 桌面端导航 -->
35
70
  <div class="hidden md:flex items-center space-x-8">
36
- {siteConfig.menu.map((item) => (
71
+ {menu.map((item) => (
37
72
  <a
38
73
  href={item.href}
39
74
  class={`text-sm font-medium transition-colors hover:text-primary-500 ${
@@ -47,22 +82,45 @@ const currentPath = Astro.url.pathname;
47
82
  ))}
48
83
  </div>
49
84
 
50
- <!-- 右侧功能区 -->
51
85
  <div class="flex items-center">
52
- <!-- 搜索框 -->
53
86
  <div class="hidden sm:block mr-2">
54
- <SearchBox client:load />
87
+ <SearchBox
88
+ client:load
89
+ placeholder={ui.searchPlaceholder}
90
+ searchLabel={ui.search}
91
+ noResultsText={ui.noResults}
92
+ />
55
93
  </div>
56
94
 
95
+ {showLanguageSwitcher && (
96
+ <div class="hidden sm:block mr-2">
97
+ <LanguageSwitcher
98
+ client:load
99
+ locales={localesForSwitcher}
100
+ currentLocale={currentLocale}
101
+ currentPath={basePath}
102
+ defaultLocale={i18nConfig.defaultLocale}
103
+ prefixDefaultLocale={i18nConfig.routing.prefixDefaultLocale}
104
+ />
105
+ </div>
106
+ )}
107
+
57
108
  <div class="hidden sm:block mr-2">
58
- <!-- 主题切换 -->
59
109
  <ThemeToggle client:load />
60
110
  </div>
61
- <!-- 移动端菜单 -->
111
+
62
112
  <div class="md:hidden">
63
- <MobileMenu client:load navigation={siteConfig.menu} />
113
+ <MobileMenu
114
+ client:load
115
+ navigation={menu}
116
+ locales={showLanguageSwitcher ? localesForSwitcher : []}
117
+ currentLocale={currentLocale}
118
+ currentPath={basePath}
119
+ defaultLocale={i18nConfig.defaultLocale}
120
+ prefixDefaultLocale={i18nConfig.routing.prefixDefaultLocale}
121
+ />
64
122
  </div>
65
123
  </div>
66
124
  </nav>
67
125
  </div>
68
- </header>
126
+ </header>