@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.
- package/dist/chunk-ATRISB7B.js +206 -0
- package/dist/chunk-HVQKQN6B.js +145 -0
- package/dist/config/index.d.ts +3 -47
- package/dist/config/index.js +18 -2
- package/dist/i18n-5H4W145i.d.ts +202 -0
- package/dist/index.d.ts +186 -7
- package/dist/index.js +238 -3
- package/dist/integration.d.ts +9 -1
- package/dist/integration.js +2 -1
- package/dist/{sidebar-DNdiCKBw.d.ts → sidebar-Da-W_4Lr.d.ts} +1 -1
- package/dist/utils/sidebar.d.ts +1 -1
- package/package.json +4 -3
- package/src/components/layout/Footer.astro +36 -20
- package/src/components/layout/Header.astro +69 -15
- package/src/components/layout/Sidebar.astro +27 -15
- package/src/components/ui/LanguageSwitcher.vue +183 -0
- package/src/layouts/BaseLayout.astro +77 -52
- package/src/layouts/PageLayout.astro +22 -27
- package/src/layouts/SlidesLayout.astro +14 -2
- package/src/pages/rss.xml.ts +18 -6
- package/templates/default/astro.config.mjs +22 -2
- package/templates/default/content/posts/blog_docs/12-i18n.md +355 -0
- package/templates/default/content/posts/blog_docs/README.md +1 -0
- package/templates/default/content/posts/blog_docs_en/README.md +78 -0
- package/templates/default/content/posts/blog_docs_en/config/01-site.md +208 -0
- package/templates/default/content/posts/blog_docs_en/config/02-sidebar.md +240 -0
- package/templates/default/content/posts/blog_docs_en/config/03-i18n.md +285 -0
- package/templates/default/content/posts/blog_docs_en/config/README.md +85 -0
- package/templates/default/content/posts/blog_docs_en/get-started/01-intro.md +81 -0
- package/templates/default/content/posts/blog_docs_en/get-started/02-install.md +137 -0
- package/templates/default/content/posts/blog_docs_en/get-started/03-create-post.md +176 -0
- package/templates/default/content/posts/blog_docs_en/get-started/04-structure.md +173 -0
- package/templates/default/content/posts/blog_docs_en/get-started/05-deploy.md +197 -0
- package/templates/default/content/posts/blog_docs_en/get-started/README.md +52 -0
- package/templates/default/content/posts/blog_docs_en/guide/README.md +59 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/01-mermaid.md +194 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/02-latex.md +233 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/03-video.md +184 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/04-icons.md +227 -0
- package/templates/default/content/posts/blog_docs_en/guide/features/README.md +51 -0
- package/templates/default/content/posts/blog_docs_en/guide/markdown/02-containers.md +226 -0
- package/templates/default/content/posts/blog_docs_en/guide/markdown/03-code-blocks.md +206 -0
- package/templates/default/content/posts/blog_docs_en/guide/markdown/README.md +194 -0
- package/templates/default/package-lock.json +9667 -0
- package/templates/default/package.json +1 -1
- package/templates/default/src/config/footer.ts +14 -11
- package/templates/default/src/config/locales/en/footer.ts +17 -0
- package/templates/default/src/config/locales/en/index.ts +16 -0
- package/templates/default/src/config/locales/en/menu.ts +12 -0
- package/templates/default/src/config/locales/en/sidebar.ts +18 -0
- package/templates/default/src/config/locales/en/site.ts +7 -0
- package/templates/default/src/config/locales/index.ts +7 -0
- package/templates/default/src/config/locales/zh-CN/footer.ts +17 -0
- package/templates/default/src/config/locales/zh-CN/index.ts +16 -0
- package/templates/default/src/config/locales/zh-CN/menu.ts +12 -0
- package/templates/default/src/config/locales/zh-CN/sidebar.ts +18 -0
- package/templates/default/src/config/locales/zh-CN/site.ts +7 -0
- package/templates/default/src/config/sidebar.ts +10 -12
- package/templates/default/src/env.d.ts +7 -0
- package/dist/chunk-MQXPSOYB.js +0 -124
- /package/dist/{chunk-GYLSY3OJ.js → chunk-AZHCNNAC.js} +0 -0
|
@@ -3,37 +3,69 @@ 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 {
|
|
10
|
+
getLocaleFromPath,
|
|
11
|
+
getLocaleConfig,
|
|
12
|
+
removeLocalePrefix,
|
|
13
|
+
isMultiLanguageEnabled,
|
|
14
|
+
} from '../../utils/i18n';
|
|
15
|
+
|
|
16
|
+
export interface Props {
|
|
17
|
+
i18nConfig?: I18nConfig;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const { i18nConfig = defaultI18nConfig } = Astro.props;
|
|
6
21
|
|
|
7
22
|
const currentPath = Astro.url.pathname;
|
|
23
|
+
const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
|
|
24
|
+
const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
|
|
25
|
+
const ui = localeConfig.ui;
|
|
26
|
+
|
|
27
|
+
// Use locale-specific site config and menu
|
|
28
|
+
const localeSiteConfig = localeConfig.site;
|
|
29
|
+
const menu = localeConfig.menu;
|
|
30
|
+
|
|
31
|
+
// Get base path without locale prefix for language switcher
|
|
32
|
+
const basePath = removeLocalePrefix(currentPath, i18nConfig);
|
|
33
|
+
|
|
34
|
+
// Check if multi-language is enabled
|
|
35
|
+
const showLanguageSwitcher = isMultiLanguageEnabled(i18nConfig);
|
|
36
|
+
|
|
37
|
+
// Prepare locales for language switcher
|
|
38
|
+
const localesForSwitcher = i18nConfig.locales.map(l => ({
|
|
39
|
+
code: l.code,
|
|
40
|
+
name: l.name,
|
|
41
|
+
}));
|
|
8
42
|
---
|
|
9
43
|
|
|
10
44
|
<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
45
|
<div class="container mx-auto px-4">
|
|
12
46
|
<nav class="flex items-center justify-between h-16">
|
|
13
|
-
<!-- Logo 和站点名称 -->
|
|
14
47
|
<div class="flex items-center space-x-4">
|
|
15
48
|
<a href="/" class="flex items-center space-x-3 hover:opacity-80 transition-opacity">
|
|
16
|
-
{siteConfig.avatar && (
|
|
49
|
+
{(localeSiteConfig.avatar || siteConfig.avatar) && (
|
|
17
50
|
<img
|
|
18
|
-
src={siteConfig.avatar}
|
|
19
|
-
alt={siteConfig.title}
|
|
51
|
+
src={localeSiteConfig.avatar || siteConfig.avatar}
|
|
52
|
+
alt={localeSiteConfig.title || siteConfig.title}
|
|
20
53
|
class="w-8 h-8 rounded-full"
|
|
21
54
|
/>
|
|
22
55
|
)}
|
|
23
56
|
<div class="hidden sm:block">
|
|
24
57
|
<h1 class="text-xl font-bold text-slate-900 dark:text-slate-100">
|
|
25
|
-
{siteConfig.title}
|
|
58
|
+
{localeSiteConfig.title || siteConfig.title}
|
|
26
59
|
</h1>
|
|
27
60
|
<p class="text-xs text-slate-600 dark:text-slate-400 -mt-1">
|
|
28
|
-
|
|
61
|
+
{localeSiteConfig.description || siteConfig.description}
|
|
29
62
|
</p>
|
|
30
63
|
</div>
|
|
31
64
|
</a>
|
|
32
65
|
</div>
|
|
33
66
|
|
|
34
|
-
<!-- 桌面端导航 -->
|
|
35
67
|
<div class="hidden md:flex items-center space-x-8">
|
|
36
|
-
{
|
|
68
|
+
{menu.map((item) => (
|
|
37
69
|
<a
|
|
38
70
|
href={item.href}
|
|
39
71
|
class={`text-sm font-medium transition-colors hover:text-primary-500 ${
|
|
@@ -47,22 +79,44 @@ const currentPath = Astro.url.pathname;
|
|
|
47
79
|
))}
|
|
48
80
|
</div>
|
|
49
81
|
|
|
50
|
-
<!-- 右侧功能区 -->
|
|
51
82
|
<div class="flex items-center">
|
|
52
|
-
<!-- 搜索框 -->
|
|
53
83
|
<div class="hidden sm:block mr-2">
|
|
54
|
-
<SearchBox
|
|
84
|
+
<SearchBox
|
|
85
|
+
client:load
|
|
86
|
+
placeholder={ui.searchPlaceholder}
|
|
87
|
+
searchLabel={ui.search}
|
|
88
|
+
/>
|
|
55
89
|
</div>
|
|
56
90
|
|
|
91
|
+
{showLanguageSwitcher && (
|
|
92
|
+
<div class="hidden sm:block mr-2">
|
|
93
|
+
<LanguageSwitcher
|
|
94
|
+
client:load
|
|
95
|
+
locales={localesForSwitcher}
|
|
96
|
+
currentLocale={currentLocale}
|
|
97
|
+
currentPath={basePath}
|
|
98
|
+
defaultLocale={i18nConfig.defaultLocale}
|
|
99
|
+
prefixDefaultLocale={i18nConfig.routing.prefixDefaultLocale}
|
|
100
|
+
/>
|
|
101
|
+
</div>
|
|
102
|
+
)}
|
|
103
|
+
|
|
57
104
|
<div class="hidden sm:block mr-2">
|
|
58
|
-
<!-- 主题切换 -->
|
|
59
105
|
<ThemeToggle client:load />
|
|
60
106
|
</div>
|
|
61
|
-
|
|
107
|
+
|
|
62
108
|
<div class="md:hidden">
|
|
63
|
-
<MobileMenu
|
|
109
|
+
<MobileMenu
|
|
110
|
+
client:load
|
|
111
|
+
navigation={menu}
|
|
112
|
+
locales={showLanguageSwitcher ? localesForSwitcher : []}
|
|
113
|
+
currentLocale={currentLocale}
|
|
114
|
+
currentPath={basePath}
|
|
115
|
+
defaultLocale={i18nConfig.defaultLocale}
|
|
116
|
+
prefixDefaultLocale={i18nConfig.routing.prefixDefaultLocale}
|
|
117
|
+
/>
|
|
64
118
|
</div>
|
|
65
119
|
</div>
|
|
66
120
|
</nav>
|
|
67
121
|
</div>
|
|
68
|
-
</header>
|
|
122
|
+
</header>
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
import { getCollection } from 'astro:content';
|
|
3
3
|
import Icon from '../ui/Icon.astro';
|
|
4
|
-
import { sidebarConfig } from '@jet-w/astro-blog/config';
|
|
5
4
|
import {
|
|
6
5
|
processSidebarConfig,
|
|
7
6
|
getRecentPosts,
|
|
@@ -9,12 +8,25 @@ import {
|
|
|
9
8
|
getArchives,
|
|
10
9
|
filterGroupsByPath,
|
|
11
10
|
} from '@jet-w/astro-blog/utils/sidebar';
|
|
11
|
+
import type { I18nConfig } from '../../config/i18n';
|
|
12
|
+
import { defaultI18nConfig } from '../../config/i18n';
|
|
13
|
+
import { getLocaleFromPath, getLocaleConfig, getLocalePrefix, formatDate } from '../../utils/i18n';
|
|
12
14
|
|
|
13
15
|
interface Props {
|
|
14
16
|
currentPath?: string;
|
|
17
|
+
i18nConfig?: I18nConfig;
|
|
15
18
|
}
|
|
16
19
|
|
|
17
|
-
const { currentPath = Astro.url.pathname } = Astro.props;
|
|
20
|
+
const { currentPath = Astro.url.pathname, i18nConfig = defaultI18nConfig } = Astro.props;
|
|
21
|
+
|
|
22
|
+
// Get current locale and translations
|
|
23
|
+
const currentLocale = getLocaleFromPath(currentPath, i18nConfig);
|
|
24
|
+
const localeConfig = getLocaleConfig(currentLocale, i18nConfig);
|
|
25
|
+
const ui = localeConfig.ui;
|
|
26
|
+
const localePrefix = getLocalePrefix(currentLocale, i18nConfig);
|
|
27
|
+
|
|
28
|
+
// Get locale-specific sidebar config (already merged with defaults in getLocaleConfig)
|
|
29
|
+
const sidebarConfig = localeConfig.sidebar;
|
|
18
30
|
|
|
19
31
|
// 获取所有文章
|
|
20
32
|
const allPosts = await getCollection('posts', ({ data }) => !data.draft);
|
|
@@ -362,17 +374,17 @@ const archives = sidebarConfig.showArchives
|
|
|
362
374
|
)
|
|
363
375
|
))}
|
|
364
376
|
|
|
365
|
-
{/*
|
|
377
|
+
{/* Recent Posts */}
|
|
366
378
|
{sidebarConfig.showRecentPosts && recentPosts.length > 0 && (
|
|
367
379
|
<section>
|
|
368
380
|
<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">
|
|
369
|
-
|
|
381
|
+
{ui.recentPosts}
|
|
370
382
|
</h3>
|
|
371
383
|
<div class="space-y-3">
|
|
372
384
|
{recentPosts.map((post) => (
|
|
373
385
|
<article class="group">
|
|
374
386
|
<a
|
|
375
|
-
href={
|
|
387
|
+
href={`${localePrefix}/posts/${post.id.toLowerCase()}`}
|
|
376
388
|
class="block p-3 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors"
|
|
377
389
|
>
|
|
378
390
|
<h4 class="text-sm font-medium text-slate-900 dark:text-slate-100 group-hover:text-primary-500 line-clamp-2 mb-1">
|
|
@@ -380,7 +392,7 @@ const archives = sidebarConfig.showArchives
|
|
|
380
392
|
</h4>
|
|
381
393
|
{post.data.pubDate && (
|
|
382
394
|
<time class="text-xs text-slate-500 dark:text-slate-400">
|
|
383
|
-
{
|
|
395
|
+
{formatDate(post.data.pubDate, localeConfig.locale.dateLocale)}
|
|
384
396
|
</time>
|
|
385
397
|
)}
|
|
386
398
|
</a>
|
|
@@ -390,16 +402,16 @@ const archives = sidebarConfig.showArchives
|
|
|
390
402
|
</section>
|
|
391
403
|
)}
|
|
392
404
|
|
|
393
|
-
{/*
|
|
405
|
+
{/* Popular Tags */}
|
|
394
406
|
{sidebarConfig.showPopularTags && popularTags.length > 0 && (
|
|
395
407
|
<section>
|
|
396
408
|
<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">
|
|
397
|
-
|
|
409
|
+
{ui.popularTags}
|
|
398
410
|
</h3>
|
|
399
411
|
<div class="flex flex-wrap gap-2">
|
|
400
412
|
{popularTags.map((tag) => (
|
|
401
413
|
<a
|
|
402
|
-
href={
|
|
414
|
+
href={`${localePrefix}/tags/${tag.slug}`}
|
|
403
415
|
class="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-primary-100 dark:bg-primary-900/30 text-primary-700 dark:text-primary-300 hover:bg-primary-200 dark:hover:bg-primary-900/50 transition-colors"
|
|
404
416
|
style={`font-size: ${Math.min(14, 10 + tag.count * 0.5)}px`}
|
|
405
417
|
>
|
|
@@ -411,20 +423,20 @@ const archives = sidebarConfig.showArchives
|
|
|
411
423
|
</section>
|
|
412
424
|
)}
|
|
413
425
|
|
|
414
|
-
{/*
|
|
426
|
+
{/* Archives */}
|
|
415
427
|
{sidebarConfig.showArchives && archives.length > 0 && (
|
|
416
428
|
<section>
|
|
417
429
|
<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">
|
|
418
|
-
|
|
430
|
+
{ui.archives}
|
|
419
431
|
</h3>
|
|
420
432
|
<div class="space-y-2">
|
|
421
433
|
{archives.map((archive) => (
|
|
422
434
|
<a
|
|
423
|
-
href={
|
|
435
|
+
href={`${localePrefix}/archives/${archive.year}/${String(archive.month).padStart(2, '0')}`}
|
|
424
436
|
class="flex items-center justify-between p-2 rounded-lg hover:bg-slate-50 dark:hover:bg-slate-800 transition-colors group"
|
|
425
437
|
>
|
|
426
438
|
<span class="text-sm text-slate-700 dark:text-slate-300 group-hover:text-primary-500">
|
|
427
|
-
{archive.year
|
|
439
|
+
{new Date(archive.year, archive.month - 1).toLocaleDateString(localeConfig.locale.dateLocale, { year: 'numeric', month: 'long' })}
|
|
428
440
|
</span>
|
|
429
441
|
<span class="text-xs text-slate-500 dark:text-slate-400 bg-slate-100 dark:bg-slate-700 px-2 py-1 rounded-full">
|
|
430
442
|
{archive.count}
|
|
@@ -435,11 +447,11 @@ const archives = sidebarConfig.showArchives
|
|
|
435
447
|
</section>
|
|
436
448
|
)}
|
|
437
449
|
|
|
438
|
-
{/*
|
|
450
|
+
{/* Friend Links */}
|
|
439
451
|
{sidebarConfig.showFriendLinks && sidebarConfig.friendLinks && sidebarConfig.friendLinks.length > 0 && (
|
|
440
452
|
<section>
|
|
441
453
|
<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">
|
|
442
|
-
|
|
454
|
+
{ui.friendLinks}
|
|
443
455
|
</h3>
|
|
444
456
|
<div class="space-y-2">
|
|
445
457
|
{sidebarConfig.friendLinks.map((link) => (
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
<template>
|
|
2
|
+
<div class="relative" ref="dropdownRef">
|
|
3
|
+
<button
|
|
4
|
+
@click="toggleDropdown"
|
|
5
|
+
class="flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-900 dark:hover:text-white bg-gray-100 dark:bg-gray-800 hover:bg-gray-200 dark:hover:bg-gray-700 rounded-lg transition-colors"
|
|
6
|
+
:aria-expanded="isOpen"
|
|
7
|
+
aria-haspopup="listbox"
|
|
8
|
+
:aria-label="ariaLabel"
|
|
9
|
+
>
|
|
10
|
+
<svg
|
|
11
|
+
class="w-4 h-4"
|
|
12
|
+
fill="none"
|
|
13
|
+
stroke="currentColor"
|
|
14
|
+
viewBox="0 0 24 24"
|
|
15
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
16
|
+
>
|
|
17
|
+
<path
|
|
18
|
+
stroke-linecap="round"
|
|
19
|
+
stroke-linejoin="round"
|
|
20
|
+
stroke-width="2"
|
|
21
|
+
d="M3 5h12M9 3v2m1.048 9.5A18.022 18.022 0 016.412 9m6.088 9h7M11 21l5-10 5 10M12.751 5C11.783 10.77 8.07 15.61 3 18.129"
|
|
22
|
+
/>
|
|
23
|
+
</svg>
|
|
24
|
+
<span>{{ currentLocaleName }}</span>
|
|
25
|
+
<svg
|
|
26
|
+
class="w-3 h-3 transition-transform"
|
|
27
|
+
:class="{ 'rotate-180': isOpen }"
|
|
28
|
+
fill="none"
|
|
29
|
+
stroke="currentColor"
|
|
30
|
+
viewBox="0 0 24 24"
|
|
31
|
+
>
|
|
32
|
+
<path
|
|
33
|
+
stroke-linecap="round"
|
|
34
|
+
stroke-linejoin="round"
|
|
35
|
+
stroke-width="2"
|
|
36
|
+
d="M19 9l-7 7-7-7"
|
|
37
|
+
/>
|
|
38
|
+
</svg>
|
|
39
|
+
</button>
|
|
40
|
+
|
|
41
|
+
<Transition
|
|
42
|
+
enter-active-class="transition ease-out duration-100"
|
|
43
|
+
enter-from-class="transform opacity-0 scale-95"
|
|
44
|
+
enter-to-class="transform opacity-100 scale-100"
|
|
45
|
+
leave-active-class="transition ease-in duration-75"
|
|
46
|
+
leave-from-class="transform opacity-100 scale-100"
|
|
47
|
+
leave-to-class="transform opacity-0 scale-95"
|
|
48
|
+
>
|
|
49
|
+
<div
|
|
50
|
+
v-if="isOpen"
|
|
51
|
+
class="absolute right-0 mt-2 w-40 origin-top-right rounded-lg bg-white dark:bg-gray-800 shadow-lg ring-1 ring-black ring-opacity-5 dark:ring-gray-700 focus:outline-none z-50"
|
|
52
|
+
role="listbox"
|
|
53
|
+
:aria-label="listLabel"
|
|
54
|
+
>
|
|
55
|
+
<div class="py-1">
|
|
56
|
+
<a
|
|
57
|
+
v-for="locale in locales"
|
|
58
|
+
:key="locale.code"
|
|
59
|
+
:href="getLocalizedUrl(locale.code)"
|
|
60
|
+
class="flex items-center gap-2 px-4 py-2 text-sm transition-colors"
|
|
61
|
+
:class="[
|
|
62
|
+
locale.code === currentLocale
|
|
63
|
+
? 'bg-blue-50 dark:bg-blue-900/30 text-blue-700 dark:text-blue-300'
|
|
64
|
+
: 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'
|
|
65
|
+
]"
|
|
66
|
+
role="option"
|
|
67
|
+
:aria-selected="locale.code === currentLocale"
|
|
68
|
+
@click="closeDropdown"
|
|
69
|
+
>
|
|
70
|
+
<span
|
|
71
|
+
v-if="locale.code === currentLocale"
|
|
72
|
+
class="w-4 h-4 flex items-center justify-center"
|
|
73
|
+
>
|
|
74
|
+
<svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
|
75
|
+
<path
|
|
76
|
+
fill-rule="evenodd"
|
|
77
|
+
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
|
|
78
|
+
clip-rule="evenodd"
|
|
79
|
+
/>
|
|
80
|
+
</svg>
|
|
81
|
+
</span>
|
|
82
|
+
<span v-else class="w-4 h-4" />
|
|
83
|
+
<span>{{ locale.name }}</span>
|
|
84
|
+
</a>
|
|
85
|
+
</div>
|
|
86
|
+
</div>
|
|
87
|
+
</Transition>
|
|
88
|
+
</div>
|
|
89
|
+
</template>
|
|
90
|
+
|
|
91
|
+
<script setup lang="ts">
|
|
92
|
+
import { ref, computed, onMounted, onUnmounted } from 'vue';
|
|
93
|
+
|
|
94
|
+
interface LocaleInfo {
|
|
95
|
+
code: string;
|
|
96
|
+
name: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
interface Props {
|
|
100
|
+
/** Available locales */
|
|
101
|
+
locales: LocaleInfo[];
|
|
102
|
+
/** Current locale code */
|
|
103
|
+
currentLocale: string;
|
|
104
|
+
/** Current page path (without locale prefix) */
|
|
105
|
+
currentPath: string;
|
|
106
|
+
/** Default locale code */
|
|
107
|
+
defaultLocale: string;
|
|
108
|
+
/** Whether to prefix the default locale in URLs */
|
|
109
|
+
prefixDefaultLocale?: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const props = withDefaults(defineProps<Props>(), {
|
|
113
|
+
prefixDefaultLocale: false,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
const isOpen = ref(false);
|
|
117
|
+
const dropdownRef = ref<HTMLElement | null>(null);
|
|
118
|
+
|
|
119
|
+
const currentLocaleName = computed(() => {
|
|
120
|
+
const locale = props.locales.find((l) => l.code === props.currentLocale);
|
|
121
|
+
return locale?.name || props.currentLocale;
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
const ariaLabel = computed(() => {
|
|
125
|
+
return `Select language, current: ${currentLocaleName.value}`;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const listLabel = computed(() => {
|
|
129
|
+
return 'Available languages';
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
function toggleDropdown() {
|
|
133
|
+
isOpen.value = !isOpen.value;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function closeDropdown() {
|
|
137
|
+
isOpen.value = false;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function getLocalizedUrl(targetLocale: string): string {
|
|
141
|
+
// Normalize the path
|
|
142
|
+
let basePath = props.currentPath;
|
|
143
|
+
if (!basePath.startsWith('/')) {
|
|
144
|
+
basePath = '/' + basePath;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// If target is default locale and prefix is not required
|
|
148
|
+
if (targetLocale === props.defaultLocale && !props.prefixDefaultLocale) {
|
|
149
|
+
return basePath === '/' ? '/' : basePath;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Add locale prefix
|
|
153
|
+
if (basePath === '/') {
|
|
154
|
+
return `/${targetLocale}`;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return `/${targetLocale}${basePath}`;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Close dropdown when clicking outside
|
|
161
|
+
function handleClickOutside(event: MouseEvent) {
|
|
162
|
+
if (dropdownRef.value && !dropdownRef.value.contains(event.target as Node)) {
|
|
163
|
+
closeDropdown();
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Close dropdown on escape key
|
|
168
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
169
|
+
if (event.key === 'Escape') {
|
|
170
|
+
closeDropdown();
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
onMounted(() => {
|
|
175
|
+
document.addEventListener('click', handleClickOutside);
|
|
176
|
+
document.addEventListener('keydown', handleKeyDown);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
onUnmounted(() => {
|
|
180
|
+
document.removeEventListener('click', handleClickOutside);
|
|
181
|
+
document.removeEventListener('keydown', handleKeyDown);
|
|
182
|
+
});
|
|
183
|
+
</script>
|