@jet-w/astro-blog 0.1.5 → 0.2.0

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 (61) hide show
  1. package/dist/chunk-ATRISB7B.js +206 -0
  2. package/dist/chunk-HVQKQN6B.js +145 -0
  3. package/dist/config/index.d.ts +3 -47
  4. package/dist/config/index.js +18 -2
  5. package/dist/i18n-5H4W145i.d.ts +202 -0
  6. package/dist/index.d.ts +186 -7
  7. package/dist/index.js +238 -3
  8. package/dist/integration.d.ts +9 -1
  9. package/dist/integration.js +2 -1
  10. package/dist/{sidebar-DNdiCKBw.d.ts → sidebar-Da-W_4Lr.d.ts} +1 -1
  11. package/dist/utils/sidebar.d.ts +1 -1
  12. package/package.json +4 -3
  13. package/src/components/layout/Footer.astro +36 -20
  14. package/src/components/layout/Header.astro +69 -15
  15. package/src/components/layout/Sidebar.astro +27 -15
  16. package/src/components/ui/LanguageSwitcher.vue +183 -0
  17. package/src/layouts/BaseLayout.astro +77 -52
  18. package/src/layouts/PageLayout.astro +22 -27
  19. package/src/layouts/SlidesLayout.astro +14 -2
  20. package/src/pages/rss.xml.ts +18 -6
  21. package/templates/default/astro.config.mjs +22 -2
  22. package/templates/default/content/posts/blog_docs/12-i18n.md +355 -0
  23. package/templates/default/content/posts/blog_docs/README.md +1 -0
  24. package/templates/default/content/posts/blog_docs_en/README.md +78 -0
  25. package/templates/default/content/posts/blog_docs_en/config/01-site.md +208 -0
  26. package/templates/default/content/posts/blog_docs_en/config/02-sidebar.md +240 -0
  27. package/templates/default/content/posts/blog_docs_en/config/03-i18n.md +285 -0
  28. package/templates/default/content/posts/blog_docs_en/config/README.md +85 -0
  29. package/templates/default/content/posts/blog_docs_en/get-started/01-intro.md +81 -0
  30. package/templates/default/content/posts/blog_docs_en/get-started/02-install.md +137 -0
  31. package/templates/default/content/posts/blog_docs_en/get-started/03-create-post.md +176 -0
  32. package/templates/default/content/posts/blog_docs_en/get-started/04-structure.md +173 -0
  33. package/templates/default/content/posts/blog_docs_en/get-started/05-deploy.md +197 -0
  34. package/templates/default/content/posts/blog_docs_en/get-started/README.md +52 -0
  35. package/templates/default/content/posts/blog_docs_en/guide/README.md +59 -0
  36. package/templates/default/content/posts/blog_docs_en/guide/features/01-mermaid.md +194 -0
  37. package/templates/default/content/posts/blog_docs_en/guide/features/02-latex.md +233 -0
  38. package/templates/default/content/posts/blog_docs_en/guide/features/03-video.md +184 -0
  39. package/templates/default/content/posts/blog_docs_en/guide/features/04-icons.md +227 -0
  40. package/templates/default/content/posts/blog_docs_en/guide/features/README.md +51 -0
  41. package/templates/default/content/posts/blog_docs_en/guide/markdown/02-containers.md +226 -0
  42. package/templates/default/content/posts/blog_docs_en/guide/markdown/03-code-blocks.md +206 -0
  43. package/templates/default/content/posts/blog_docs_en/guide/markdown/README.md +194 -0
  44. package/templates/default/package-lock.json +9667 -0
  45. package/templates/default/package.json +1 -1
  46. package/templates/default/src/config/footer.ts +14 -11
  47. package/templates/default/src/config/locales/en/footer.ts +17 -0
  48. package/templates/default/src/config/locales/en/index.ts +16 -0
  49. package/templates/default/src/config/locales/en/menu.ts +12 -0
  50. package/templates/default/src/config/locales/en/sidebar.ts +18 -0
  51. package/templates/default/src/config/locales/en/site.ts +7 -0
  52. package/templates/default/src/config/locales/index.ts +7 -0
  53. package/templates/default/src/config/locales/zh-CN/footer.ts +17 -0
  54. package/templates/default/src/config/locales/zh-CN/index.ts +16 -0
  55. package/templates/default/src/config/locales/zh-CN/menu.ts +12 -0
  56. package/templates/default/src/config/locales/zh-CN/sidebar.ts +18 -0
  57. package/templates/default/src/config/locales/zh-CN/site.ts +7 -0
  58. package/templates/default/src/config/sidebar.ts +10 -12
  59. package/templates/default/src/env.d.ts +7 -0
  60. package/dist/chunk-MQXPSOYB.js +0 -124
  61. /package/dist/{chunk-GYLSY3OJ.js → chunk-AZHCNNAC.js} +0 -0
@@ -4,6 +4,18 @@ import { siteConfig, defaultSEO } from '@jet-w/astro-blog/config';
4
4
  import '@jet-w/astro-blog/styles/global.css';
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
+ import {
8
+ getLocaleFromPath,
9
+ getLocaleConfig,
10
+ getAlternateLinks,
11
+ getTextDirection,
12
+ isMultiLanguageEnabled,
13
+ t,
14
+ type I18nConfig,
15
+ } from '../utils/i18n';
16
+ import { defaultI18nConfig } from '../config/i18n';
17
+ // Import i18n config from virtual module (injected by integration)
18
+ import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
7
19
 
8
20
  export interface Props {
9
21
  title?: string;
@@ -13,6 +25,7 @@ export interface Props {
13
25
  publishedTime?: string;
14
26
  modifiedTime?: string;
15
27
  tags?: string[];
28
+ i18nConfig?: I18nConfig;
16
29
  }
17
30
 
18
31
  const {
@@ -22,14 +35,35 @@ const {
22
35
  type = 'website',
23
36
  publishedTime,
24
37
  modifiedTime,
25
- tags
38
+ tags,
39
+ i18nConfig = virtualI18nConfig || defaultI18nConfig,
26
40
  } = Astro.props;
27
41
 
42
+ // Get current locale from URL
43
+ const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
44
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
45
+ const localeData = localeConfig.locale;
46
+ const ui = localeConfig.ui;
47
+
48
+ // Use locale-specific site config if available
49
+ const localeSiteConfig = localeConfig.site;
50
+
28
51
  const canonicalURL = new URL(Astro.url.pathname, Astro.site);
29
- const fullTitle = title === siteConfig.title ? title : `${title} | ${siteConfig.title}`;
52
+ const siteTitle = localeSiteConfig.title || siteConfig.title;
53
+ const fullTitle = title === siteTitle ? title : `${title} | ${siteTitle}`;
30
54
  const fullImage = new URL(image, Astro.site);
31
55
 
32
- // 检查 favicon 文件是否存在,不存在则使用 avatar
56
+ // Get alternate links for SEO (hreflang)
57
+ const baseUrl = Astro.site?.toString() || '';
58
+ const alternateLinks = isMultiLanguageEnabled(i18nConfig)
59
+ ? getAlternateLinks(Astro.url.pathname, baseUrl, i18nConfig)
60
+ : [];
61
+
62
+ // Get locale prefix for RSS link
63
+ const rssPath = currentLocale === i18nConfig.defaultLocale && !i18nConfig.routing.prefixDefaultLocale
64
+ ? '/rss.xml'
65
+ : `/${currentLocale}/rss.xml`;
66
+
33
67
  const publicDir = path.join(process.cwd(), 'public');
34
68
  const faviconSvgExists = fs.existsSync(path.join(publicDir, 'favicon.svg'));
35
69
  const faviconIcoExists = fs.existsSync(path.join(publicDir, 'favicon.ico'));
@@ -38,7 +72,7 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
38
72
  ---
39
73
 
40
74
  <!DOCTYPE html>
41
- <html lang="zh-CN" class="scroll-smooth">
75
+ <html lang={localeData.htmlLang} dir={getTextDirection(currentLocale, i18nConfig)} class="scroll-smooth">
42
76
  <head>
43
77
  <meta charset="UTF-8" />
44
78
  <meta name="description" content={description} />
@@ -50,13 +84,19 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
50
84
  <title>{fullTitle}</title>
51
85
  <link rel="canonical" href={canonicalURL} />
52
86
 
87
+ <!-- Alternate language links (hreflang) -->
88
+ {alternateLinks.map(link => (
89
+ <link rel="alternate" hreflang={link.hreflang} href={link.url} />
90
+ ))}
91
+
53
92
  <!-- Open Graph -->
54
93
  <meta property="og:type" content={type} />
55
94
  <meta property="og:title" content={fullTitle} />
56
95
  <meta property="og:description" content={description} />
57
96
  <meta property="og:url" content={canonicalURL} />
58
97
  <meta property="og:image" content={fullImage} />
59
- <meta property="og:site_name" content={siteConfig.title} />
98
+ <meta property="og:site_name" content={siteTitle} />
99
+ <meta property="og:locale" content={localeData.htmlLang} />
60
100
 
61
101
  {publishedTime && (
62
102
  <meta property="article:published_time" content={publishedTime} />
@@ -75,13 +115,11 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
75
115
  <meta name="twitter:image" content={fullImage} />
76
116
 
77
117
  <!-- RSS -->
78
- <link rel="alternate" type="application/rss+xml" title={siteConfig.title} href="/rss.xml" />
118
+ <link rel="alternate" type="application/rss+xml" title={siteTitle} href={rssPath} />
79
119
 
80
- <!-- 预加载关键资源 -->
81
120
  <link rel="preconnect" href="https://fonts.googleapis.com" />
82
121
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
83
122
 
84
- <!-- 图标库 -->
85
123
  <!-- Font Awesome -->
86
124
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.5.1/css/all.min.css" crossorigin="anonymous" />
87
125
  <!-- Material Icons -->
@@ -95,15 +133,11 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
95
133
  <!-- Ionicons -->
96
134
  <script type="module" src="https://unpkg.com/ionicons@7.1.0/dist/ionicons/ionicons.esm.js"></script>
97
135
 
98
- <!-- 主题检测脚本 -->
99
136
  <script is:inline>
100
- // 在页面加载前检测主题,避免闪烁
101
- // 默认使用深色模式
102
137
  const theme = (() => {
103
138
  if (typeof localStorage !== 'undefined' && localStorage.getItem('theme')) {
104
139
  return localStorage.getItem('theme');
105
140
  }
106
- // 默认深色模式,除非用户系统偏好浅色
107
141
  if (window.matchMedia('(prefers-color-scheme: light)').matches) {
108
142
  return 'light';
109
143
  }
@@ -123,15 +157,26 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
123
157
  <slot />
124
158
  </div>
125
159
 
126
- <!-- Mermaid 容器渲染 -->
160
+ <!-- Mermaid -->
127
161
  <script src="/js/mermaid-container.js" is:inline></script>
128
162
 
129
- <!-- Tabs 组件初始化 -->
163
+ <!-- Tabs -->
130
164
  <script src="/js/tabs-init.js" is:inline></script>
131
165
 
132
- <!-- 全局脚本 -->
166
+ <!-- Store i18n data for client-side scripts -->
167
+ <script is:inline define:vars={{ locale: currentLocale, uiTranslations: ui }}>
168
+ window.__i18n = {
169
+ locale: locale,
170
+ ui: uiTranslations
171
+ };
172
+ </script>
173
+
133
174
  <script>
134
- // 回到顶部功能
175
+ // Get i18n translations
176
+ const i18n = (window as any).__i18n || { locale: 'zh-CN', ui: {} };
177
+ const ui = i18n.ui;
178
+
179
+ // Back to top
135
180
  const backToTop = document.querySelector('.back-to-top');
136
181
  if (backToTop) {
137
182
  window.addEventListener('scroll', () => {
@@ -147,7 +192,7 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
147
192
  });
148
193
  }
149
194
 
150
- // 平滑滚动锚点链接
195
+ // Smooth scroll for anchor links
151
196
  document.querySelectorAll('a[href^="#"]').forEach(anchor => {
152
197
  anchor.addEventListener('click', function (this: HTMLAnchorElement, e: Event) {
153
198
  e.preventDefault();
@@ -161,20 +206,17 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
161
206
  });
162
207
  });
163
208
 
164
- // 代码块增强功能
209
+ // Code block enhancement
165
210
  function enhanceCodeBlocks() {
166
211
  const codeBlocks = document.querySelectorAll('pre:not([data-enhanced])');
167
- const COLLAPSE_THRESHOLD = 15; // 超过15行才显示收缩功能
212
+ const COLLAPSE_THRESHOLD = 15;
168
213
 
169
214
  codeBlocks.forEach((pre) => {
170
- // 标记为已处理
171
215
  pre.setAttribute('data-enhanced', 'true');
172
216
 
173
- // 获取语言信息
174
217
  const code = pre.querySelector('code');
175
218
  let lang = 'code';
176
219
 
177
- // 跳过 mermaid 代码块
178
220
  if (code?.classList.contains('language-mermaid') ||
179
221
  pre.classList.contains('mermaid') ||
180
222
  pre.closest('.mermaid-container')) {
@@ -182,7 +224,6 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
182
224
  }
183
225
 
184
226
  if (code) {
185
- // 从 class 中获取语言
186
227
  const classList = code.className.split(' ');
187
228
  for (const cls of classList) {
188
229
  if (cls.startsWith('language-')) {
@@ -192,58 +233,50 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
192
233
  }
193
234
  }
194
235
 
195
- // 从 data-language 属性获取(shiki 用这个)
196
236
  const preEl = pre as HTMLPreElement;
197
237
  if (preEl.dataset.language) {
198
238
  lang = preEl.dataset.language;
199
239
  }
200
240
 
201
- // 计算代码行数
202
241
  const codeText = code ? code.textContent : pre.textContent;
203
242
  const lineCount = (codeText || '').split('\n').length;
204
243
  const shouldCollapse = lineCount > COLLAPSE_THRESHOLD;
205
244
 
206
- // 创建包装器
207
245
  const wrapper = document.createElement('div');
208
246
  wrapper.className = 'code-block-wrapper' + (shouldCollapse ? ' collapsed' : '');
209
247
 
210
- // 创建头部
211
248
  const header = document.createElement('div');
212
249
  header.className = 'code-block-header';
213
250
  header.innerHTML = `
214
251
  <span class="code-block-lang">${lang}</span>
215
252
  <div class="code-block-actions">
216
253
  ${shouldCollapse ? `
217
- <button class="code-block-btn collapse-btn" title="展开/收缩">
254
+ <button class="code-block-btn collapse-btn" title="${ui.expand || 'Expand'}/${ui.collapse || 'Collapse'}">
218
255
  <svg class="collapse-icon transition-transform duration-200" fill="none" stroke="currentColor" viewBox="0 0 24 24">
219
256
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
220
257
  </svg>
221
- <span class="collapse-text">展开</span>
258
+ <span class="collapse-text">${ui.expand || 'Expand'}</span>
222
259
  </button>
223
260
  ` : ''}
224
- <button class="code-block-btn copy-btn" title="复制代码">
261
+ <button class="code-block-btn copy-btn" title="${ui.copyCode || 'Copy'}">
225
262
  <svg class="copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
226
263
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
227
264
  </svg>
228
- <span class="copy-text">复制</span>
265
+ <span class="copy-text">${ui.copyCode || 'Copy'}</span>
229
266
  </button>
230
267
  </div>
231
268
  `;
232
269
 
233
- // 创建内容区
234
270
  const content = document.createElement('div');
235
271
  content.className = 'code-block-content';
236
272
 
237
- // 插入DOM
238
273
  if (!pre.parentNode) return;
239
274
  pre.parentNode.insertBefore(wrapper, pre);
240
275
  wrapper.appendChild(header);
241
276
  wrapper.appendChild(content);
242
277
  content.appendChild(pre);
243
278
 
244
- // 如果需要收缩,添加展开按钮和底部收起按钮
245
279
  if (shouldCollapse) {
246
- // 展开按钮(收缩状态显示)
247
280
  const expandOverlay = document.createElement('div');
248
281
  expandOverlay.className = 'code-block-expand';
249
282
  expandOverlay.innerHTML = `
@@ -251,12 +284,11 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
251
284
  <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
252
285
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
253
286
  </svg>
254
- <span>展开代码 (${lineCount} )</span>
287
+ <span>${ui.expandCode || 'Expand code'} (${lineCount} ${ui.lines || 'lines'})</span>
255
288
  </button>
256
289
  `;
257
290
  content.appendChild(expandOverlay);
258
291
 
259
- // 底部收起按钮(展开状态显示)
260
292
  const collapseOverlay = document.createElement('div');
261
293
  collapseOverlay.className = 'code-block-collapse';
262
294
  collapseOverlay.innerHTML = `
@@ -264,35 +296,31 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
264
296
  <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
265
297
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 15l7-7 7 7" />
266
298
  </svg>
267
- <span>收起代码</span>
299
+ <span>${ui.collapseCode || 'Collapse code'}</span>
268
300
  </button>
269
301
  `;
270
302
  content.appendChild(collapseOverlay);
271
303
 
272
- // 展开按钮点击事件
273
304
  const expandBtn = expandOverlay.querySelector('.expand-btn');
274
305
  if (expandBtn) {
275
306
  expandBtn.addEventListener('click', () => {
276
307
  wrapper.classList.remove('collapsed');
277
308
  const collapseText = header.querySelector('.collapse-text') as HTMLElement | null;
278
- if (collapseText) collapseText.textContent = '收缩';
309
+ if (collapseText) collapseText.textContent = ui.collapse || 'Collapse';
279
310
  });
280
311
  }
281
312
 
282
- // 底部收起按钮点击事件
283
313
  const collapseBottomBtn = collapseOverlay.querySelector('.collapse-bottom-btn');
284
314
  if (collapseBottomBtn) {
285
315
  collapseBottomBtn.addEventListener('click', () => {
286
316
  wrapper.classList.add('collapsed');
287
317
  const collapseText = header.querySelector('.collapse-text') as HTMLElement | null;
288
- if (collapseText) collapseText.textContent = '展开';
289
- // 滚动到代码块顶部
318
+ if (collapseText) collapseText.textContent = ui.expand || 'Expand';
290
319
  wrapper.scrollIntoView({ behavior: 'smooth', block: 'start' });
291
320
  });
292
321
  }
293
322
  }
294
323
 
295
- // 复制按钮点击事件
296
324
  const copyBtn = header.querySelector('.copy-btn') as HTMLElement | null;
297
325
  if (copyBtn) {
298
326
  copyBtn.addEventListener('click', async () => {
@@ -302,40 +330,37 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
302
330
  copyBtn.classList.add('copied');
303
331
  const copyText = copyBtn.querySelector('.copy-text') as HTMLElement | null;
304
332
  const copyIcon = copyBtn.querySelector('.copy-icon') as HTMLElement | null;
305
- if (copyText) copyText.textContent = '已复制';
333
+ if (copyText) copyText.textContent = ui.copied || 'Copied';
306
334
  if (copyIcon) copyIcon.innerHTML = `
307
335
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
308
336
  `;
309
337
 
310
338
  setTimeout(() => {
311
339
  copyBtn.classList.remove('copied');
312
- if (copyText) copyText.textContent = '复制';
340
+ if (copyText) copyText.textContent = ui.copyCode || 'Copy';
313
341
  if (copyIcon) copyIcon.innerHTML = `
314
342
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
315
343
  `;
316
344
  }, 2000);
317
345
  } catch (err) {
318
- console.error('复制失败:', err);
346
+ console.error('Copy failed:', err);
319
347
  }
320
348
  });
321
349
  }
322
350
 
323
- // 收缩按钮点击事件
324
351
  const collapseBtn = header.querySelector('.collapse-btn') as HTMLElement | null;
325
352
  if (collapseBtn) {
326
353
  collapseBtn.addEventListener('click', () => {
327
354
  const isCollapsed = wrapper.classList.toggle('collapsed');
328
355
  const collapseText = collapseBtn.querySelector('.collapse-text') as HTMLElement | null;
329
- if (collapseText) collapseText.textContent = isCollapsed ? '展开' : '收缩';
356
+ if (collapseText) collapseText.textContent = isCollapsed ? (ui.expand || 'Expand') : (ui.collapse || 'Collapse');
330
357
  });
331
358
  }
332
359
  });
333
360
  }
334
361
 
335
- // 页面加载后执行
336
362
  enhanceCodeBlocks();
337
363
 
338
- // 监听DOM变化(处理动态加载的内容)
339
364
  const observer = new MutationObserver((mutations) => {
340
365
  let hasNewCodeBlocks = false;
341
366
  mutations.forEach((mutation) => {
@@ -359,4 +384,4 @@ const faviconType = faviconSvgExists ? 'image/svg+xml' : faviconIcoExists ? 'ima
359
384
  </script>
360
385
 
361
386
  </body>
362
- </html>
387
+ </html>
@@ -4,6 +4,11 @@ import Header from '../components/layout/Header.astro';
4
4
  import Footer from '../components/layout/Footer.astro';
5
5
  import Sidebar from '../components/layout/Sidebar.astro';
6
6
  import SidebarToggle from '../components/ui/SidebarToggle.vue';
7
+ import type { I18nConfig } from '../config/i18n';
8
+ import { defaultI18nConfig } from '../config/i18n';
9
+ import { getLocaleFromPath, getLocaleConfig } from '../utils/i18n';
10
+ // Import i18n config from virtual module (injected by integration)
11
+ import { i18nConfig as virtualI18nConfig } from 'virtual:astro-blog-i18n';
7
12
 
8
13
  export interface Props {
9
14
  title?: string;
@@ -15,6 +20,7 @@ export interface Props {
15
20
  tags?: string[];
16
21
  showSidebar?: boolean;
17
22
  showToc?: boolean;
23
+ i18nConfig?: I18nConfig;
18
24
  }
19
25
 
20
26
  const {
@@ -26,8 +32,14 @@ const {
26
32
  modifiedTime,
27
33
  tags,
28
34
  showSidebar = true,
29
- showToc = false
35
+ showToc = false,
36
+ i18nConfig = virtualI18nConfig || defaultI18nConfig,
30
37
  } = Astro.props;
38
+
39
+ // Get current locale
40
+ const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
41
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
42
+ const ui = localeConfig.ui;
31
43
  ---
32
44
 
33
45
  <BaseLayout
@@ -38,17 +50,16 @@ const {
38
50
  publishedTime={publishedTime}
39
51
  modifiedTime={modifiedTime}
40
52
  tags={tags}
53
+ i18nConfig={i18nConfig}
41
54
  >
42
- <Header />
55
+ <Header i18nConfig={i18nConfig} />
43
56
 
44
57
  <main class="flex-1 flex w-full min-w-0">
45
- <!-- 左侧边栏 - 桌面端 -->
46
58
  {showSidebar && (
47
59
  <aside class="hidden lg:block shrink-0 transition-all duration-300 relative" data-sidebar style="width: var(--sidebar-width, 256px);">
48
60
  <div class="sticky top-20 h-[calc(100vh-5rem)] overflow-y-auto">
49
- <Sidebar />
61
+ <Sidebar i18nConfig={i18nConfig} />
50
62
  </div>
51
- <!-- 拖拽调整宽度的手柄 -->
52
63
  <div
53
64
  class="absolute top-0 right-0 w-1 h-full cursor-col-resize group z-10"
54
65
  data-sidebar-resizer
@@ -60,7 +71,6 @@ const {
60
71
  </aside>
61
72
  )}
62
73
 
63
- <!-- 移动端侧边栏遮罩 -->
64
74
  {showSidebar && (
65
75
  <div
66
76
  class="lg:hidden fixed inset-0 bg-black/50 z-40 hidden"
@@ -69,33 +79,29 @@ const {
69
79
  ></div>
70
80
  )}
71
81
 
72
- <!-- 移动端侧边栏 -->
73
82
  {showSidebar && (
74
83
  <aside
75
84
  class="lg:hidden fixed top-16 left-0 w-72 h-[calc(100vh-4rem)] bg-white dark:bg-slate-900 z-50 transform -translate-x-full transition-transform duration-300 shadow-xl overflow-y-auto"
76
85
  data-mobile-sidebar
77
86
  >
78
87
  <div class="p-4">
79
- <Sidebar />
88
+ <Sidebar i18nConfig={i18nConfig} />
80
89
  </div>
81
90
  </aside>
82
91
  )}
83
92
 
84
- <!-- 主内容区 -->
85
93
  <div class={`flex-1 transition-all duration-300 ${showSidebar ? 'lg:px-8' : ''} ${showToc ? 'xl:pr-64' : ''} relative`} data-main-content>
86
- <!-- 侧边栏切换按钮 - 桌面端固定位置 -->
87
94
  {showSidebar && (
88
95
  <div class="hidden lg:block fixed top-20 left-1 z-40 transition-all duration-300" data-sidebar-toggle>
89
96
  <SidebarToggle client:load />
90
97
  </div>
91
98
  )}
92
99
 
93
- <!-- 侧边栏切换按钮 - 移动端浮动按钮 -->
94
100
  {showSidebar && (
95
101
  <button
96
102
  class="lg:hidden fixed bottom-20 left-4 z-40 p-3 bg-primary-500 hover:bg-primary-600 text-white rounded-full shadow-lg transition-colors"
97
103
  data-mobile-sidebar-toggle
98
- aria-label="打开侧边栏"
104
+ aria-label={ui.documentTree}
99
105
  onclick="document.querySelector('[data-sidebar-overlay]').classList.remove('hidden'); document.querySelector('[data-mobile-sidebar]').classList.remove('-translate-x-full');"
100
106
  >
101
107
  <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -109,7 +115,6 @@ const {
109
115
  </div>
110
116
  </div>
111
117
 
112
- <!-- 右侧TOC -->
113
118
  {showToc && (
114
119
  <aside class="hidden xl:block w-64 shrink-0">
115
120
  <div class="sticky top-20 h-[calc(100vh-5rem)] overflow-y-auto">
@@ -119,10 +124,9 @@ const {
119
124
  )}
120
125
  </main>
121
126
 
122
- <Footer />
127
+ <Footer i18nConfig={i18nConfig} />
123
128
 
124
- <!-- 回到顶部按钮 -->
125
- <button class="back-to-top" aria-label="回到顶部">
129
+ <button class="back-to-top" aria-label={ui.backToTop}>
126
130
  <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
127
131
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 10l7-7m0 0l7 7m-7-7v18" />
128
132
  </svg>
@@ -130,14 +134,12 @@ const {
130
134
  </BaseLayout>
131
135
 
132
136
  <script>
133
- // 侧边栏拖拽调整宽度
134
137
  function initSidebarResizer() {
135
138
  const sidebar = document.querySelector('[data-sidebar]') as HTMLElement;
136
139
  const resizer = document.querySelector('[data-sidebar-resizer]') as HTMLElement;
137
140
 
138
141
  if (!sidebar || !resizer) return;
139
142
 
140
- // 从 localStorage 恢复宽度
141
143
  const savedWidth = localStorage.getItem('sidebar-width');
142
144
  if (savedWidth) {
143
145
  sidebar.style.setProperty('--sidebar-width', savedWidth);
@@ -152,10 +154,8 @@ const {
152
154
  startX = e.clientX;
153
155
  startWidth = sidebar.offsetWidth;
154
156
 
155
- // 禁用过渡效果以获得流畅拖拽
156
157
  sidebar.style.transition = 'none';
157
158
 
158
- // 添加全局样式防止选中文本
159
159
  document.body.style.cursor = 'col-resize';
160
160
  document.body.style.userSelect = 'none';
161
161
 
@@ -167,7 +167,7 @@ const {
167
167
  if (!isResizing) return;
168
168
 
169
169
  const diff = e.clientX - startX;
170
- const newWidth = Math.min(Math.max(startWidth + diff, 200), 500); // 最小200px,最大500px
170
+ const newWidth = Math.min(Math.max(startWidth + diff, 200), 500);
171
171
 
172
172
  sidebar.style.setProperty('--sidebar-width', `${newWidth}px`);
173
173
  };
@@ -177,14 +177,11 @@ const {
177
177
 
178
178
  isResizing = false;
179
179
 
180
- // 恢复过渡效果
181
180
  sidebar.style.transition = '';
182
181
 
183
- // 恢复全局样式
184
182
  document.body.style.cursor = '';
185
183
  document.body.style.userSelect = '';
186
184
 
187
- // 保存宽度到 localStorage
188
185
  const currentWidth = getComputedStyle(sidebar).getPropertyValue('--sidebar-width');
189
186
  if (currentWidth) {
190
187
  localStorage.setItem('sidebar-width', currentWidth.trim());
@@ -197,7 +194,6 @@ const {
197
194
  resizer.addEventListener('mousedown', startResize);
198
195
  }
199
196
 
200
- // DOM 加载完成后初始化
201
197
  if (document.readyState === 'loading') {
202
198
  document.addEventListener('DOMContentLoaded', initSidebarResizer);
203
199
  } else {
@@ -206,7 +202,6 @@ const {
206
202
  </script>
207
203
 
208
204
  <style>
209
- /* 拖拽时的视觉反馈 */
210
205
  [data-sidebar-resizer]:hover {
211
206
  background: linear-gradient(to right, transparent, rgba(var(--color-primary-500), 0.1), transparent);
212
207
  }
@@ -214,4 +209,4 @@ const {
214
209
  [data-sidebar-resizer]:active {
215
210
  background: linear-gradient(to right, transparent, rgba(var(--color-primary-500), 0.2), transparent);
216
211
  }
217
- </style>
212
+ </style>
@@ -1,6 +1,9 @@
1
1
  ---
2
2
  import { siteConfig } from '@jet-w/astro-blog/config';
3
3
  import '@jet-w/astro-blog/styles/slides.css';
4
+ import type { I18nConfig } from '../config/i18n';
5
+ import { defaultI18nConfig } from '../config/i18n';
6
+ import { getLocaleFromPath, getLocaleConfig } from '../utils/i18n';
4
7
 
5
8
  export interface Props {
6
9
  title: string;
@@ -13,6 +16,7 @@ export interface Props {
13
16
  hash?: boolean;
14
17
  slideNumber?: boolean;
15
18
  showBackButton?: boolean;
19
+ i18nConfig?: I18nConfig;
16
20
  }
17
21
 
18
22
  const {
@@ -26,9 +30,17 @@ const {
26
30
  hash = true,
27
31
  slideNumber = false,
28
32
  showBackButton = true,
33
+ i18nConfig = defaultI18nConfig,
29
34
  } = Astro.props;
30
35
 
31
- const fullTitle = `${title} | ${siteConfig.title}`;
36
+ // Get current locale
37
+ const currentLocale = getLocaleFromPath(Astro.url.pathname, i18nConfig);
38
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
39
+ const localeData = localeConfig.locale;
40
+ const localeSiteConfig = localeConfig.site;
41
+
42
+ const siteTitle = localeSiteConfig.title || siteConfig.title;
43
+ const fullTitle = `${title} | ${siteTitle}`;
32
44
 
33
45
  // Reveal.js 配置
34
46
  const revealConfig = JSON.stringify({
@@ -48,7 +60,7 @@ const revealConfig = JSON.stringify({
48
60
  ---
49
61
 
50
62
  <!DOCTYPE html>
51
- <html lang="zh-CN">
63
+ <html lang={localeData.htmlLang}>
52
64
  <head>
53
65
  <meta charset="UTF-8" />
54
66
  <meta name="description" content={description} />
@@ -1,11 +1,23 @@
1
1
  import rss from '@astrojs/rss';
2
2
  import { getCollection } from 'astro:content';
3
3
  import { siteConfig } from '@jet-w/astro-blog/config';
4
+ import { defaultI18nConfig } from '../config/i18n';
5
+ import { getLocaleFromPath, getLocaleConfig, getLocalePrefix } from '../utils/i18n';
4
6
 
5
- export async function GET(context: { site: URL }) {
7
+ export async function GET(context: { site: URL; request: Request }) {
6
8
  const posts = await getCollection('posts', ({ data }) => !data.draft);
7
9
 
8
- // 按日期排序,最新的在前
10
+ // Get locale from URL path
11
+ const url = new URL(context.request.url);
12
+ const i18nConfig = defaultI18nConfig; // In real usage, this would be passed from integration
13
+ const currentLocale = getLocaleFromPath(url.pathname, i18nConfig);
14
+ const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
15
+ const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
16
+
17
+ // Use locale-specific site config
18
+ const localeSiteConfig = localeConfig.site;
19
+
20
+ // Sort by date, newest first
9
21
  const sortedPosts = posts.sort((a, b) => {
10
22
  const dateA = new Date(a.data.date || 0);
11
23
  const dateB = new Date(b.data.date || 0);
@@ -13,16 +25,16 @@ export async function GET(context: { site: URL }) {
13
25
  });
14
26
 
15
27
  return rss({
16
- title: siteConfig.title,
17
- description: siteConfig.description,
28
+ title: localeSiteConfig.title || siteConfig.title,
29
+ description: localeSiteConfig.description || siteConfig.description,
18
30
  site: context.site,
19
31
  items: sortedPosts.map((post) => ({
20
32
  title: post.data.title,
21
33
  pubDate: post.data.date ? new Date(post.data.date) : new Date(),
22
34
  description: post.data.description || '',
23
- link: `/posts/${post.id.toLowerCase()}/`,
35
+ link: `${localePrefix}/posts/${post.id.toLowerCase()}/`,
24
36
  categories: [...(post.data.categories || []), ...(post.data.tags || [])]
25
37
  })),
26
- customData: `<language>zh-CN</language>`
38
+ customData: `<language>${localeConfig.locale.htmlLang}</language>`
27
39
  });
28
40
  }