@jet-w/astro-blog 0.2.1 → 0.2.3
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-DAH2XP4W.js +154 -0
- package/dist/{chunk-TJTPX2WP.js → chunk-PG43JO4O.js} +1 -153
- package/dist/chunk-PZICDGJG.js +69 -0
- package/dist/chunk-Z3O3JK56.js +186 -0
- package/dist/config/index.d.ts +2 -2
- package/dist/config/index.js +8 -6
- package/dist/{i18n-PgMCFBw0.d.ts → i18n-DYYPTq4o.d.ts} +1 -1
- package/dist/index.d.ts +10 -202
- package/dist/index.js +33 -223
- package/dist/integration.d.ts +2 -2
- package/dist/{sidebar-Da-W_4Lr.d.ts → sidebar-DNdiCKBw.d.ts} +1 -1
- package/dist/utils/i18n.d.ts +133 -0
- package/dist/utils/i18n.js +49 -0
- package/dist/utils/sidebar.d.ts +1 -1
- package/dist/utils/useI18n.d.ts +74 -0
- package/dist/utils/useI18n.js +15 -0
- package/package.json +12 -1
- package/src/config/footer.ts +58 -0
- package/src/config/i18n.ts +485 -0
- package/src/config/index.ts +42 -0
- package/src/config/menu.ts +32 -0
- package/src/config/sidebar.ts +142 -0
- package/src/config/site.ts +61 -0
- package/src/config/social.ts +29 -0
- package/src/types/index.ts +80 -0
- package/src/utils/i18n.ts +418 -0
- package/src/utils/sidebar.ts +503 -0
- package/src/utils/useI18n.ts +155 -0
- package/templates/default/package.dev.json +31 -0
- package/templates/default/package.json +1 -1
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { SiteConfig } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Default site configuration
|
|
5
|
+
* Users should override this in their own config
|
|
6
|
+
*/
|
|
7
|
+
export const siteConfig: SiteConfig = {
|
|
8
|
+
title: 'My Astro Blog',
|
|
9
|
+
description: '',
|
|
10
|
+
author: 'Author',
|
|
11
|
+
email: '',
|
|
12
|
+
avatar: '/images/avatar.svg',
|
|
13
|
+
social: {
|
|
14
|
+
github: '',
|
|
15
|
+
twitter: '',
|
|
16
|
+
linkedin: '',
|
|
17
|
+
email: ''
|
|
18
|
+
},
|
|
19
|
+
menu: [
|
|
20
|
+
{
|
|
21
|
+
name: '首页',
|
|
22
|
+
href: '/',
|
|
23
|
+
icon: 'home'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: '博客',
|
|
27
|
+
href: '/posts',
|
|
28
|
+
icon: 'posts'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: '关于',
|
|
32
|
+
href: '/about',
|
|
33
|
+
icon: 'about'
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const defaultSEO = {
|
|
39
|
+
title: siteConfig.title,
|
|
40
|
+
description: siteConfig.description,
|
|
41
|
+
image: '/images/og-image.jpg',
|
|
42
|
+
type: 'website' as const
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Create site config with user overrides
|
|
47
|
+
*/
|
|
48
|
+
export function defineSiteConfig(config: Partial<SiteConfig>): SiteConfig {
|
|
49
|
+
return {
|
|
50
|
+
...siteConfig,
|
|
51
|
+
...config,
|
|
52
|
+
social: {
|
|
53
|
+
...siteConfig.social,
|
|
54
|
+
...config.social
|
|
55
|
+
},
|
|
56
|
+
menu: config.menu || siteConfig.menu
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// 向后兼容的别名
|
|
61
|
+
export const defaultSiteConfig = siteConfig;
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 社交链接配置
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface SocialLink {
|
|
6
|
+
type: string;
|
|
7
|
+
url: string;
|
|
8
|
+
label?: string;
|
|
9
|
+
icon?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const defaultIcons: Record<string, string> = {
|
|
13
|
+
github: 'M12 0C5.374 0 0 5.373 0 12c0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23A11.509 11.509 0 0112 5.803c1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576C20.566 21.797 24 17.3 24 12c0-6.627-5.373-12-12-12z',
|
|
14
|
+
twitter: 'M23.953 4.57a10 10 0 01-2.825.775 4.958 4.958 0 002.163-2.723c-.951.555-2.005.959-3.127 1.184a4.92 4.92 0 00-8.384 4.482C7.69 8.095 4.067 6.13 1.64 3.162a4.822 4.822 0 00-.666 2.475c0 1.71.87 3.213 2.188 4.096a4.904 4.904 0 01-2.228-.616v.06a4.923 4.923 0 003.946 4.827 4.996 4.996 0 01-2.212.085 4.936 4.936 0 004.604 3.417 9.867 9.867 0 01-6.102 2.105c-.39 0-.779-.023-1.17-.067a13.995 13.995 0 007.557 2.209c9.053 0 13.998-7.496 13.998-13.985 0-.21 0-.42-.015-.63A9.935 9.935 0 0024 4.59z',
|
|
15
|
+
linkedin: 'M20.447 20.452h-3.554v-5.569c0-1.328-.027-3.037-1.852-3.037-1.853 0-2.136 1.445-2.136 2.939v5.667H9.351V9h3.414v1.561h.046c.477-.9 1.637-1.85 3.37-1.85 3.601 0 4.267 2.37 4.267 5.455v6.286zM5.337 7.433c-1.144 0-2.063-.926-2.063-2.065 0-1.138.92-2.063 2.063-2.063 1.14 0 2.064.925 2.064 2.063 0 1.139-.925 2.065-2.064 2.065zm1.782 13.019H3.555V9h3.564v11.452zM22.225 0H1.771C.792 0 0 .774 0 1.729v20.542C0 23.227.792 24 1.771 24h20.451C23.2 24 24 23.227 24 22.271V1.729C24 .774 23.2 0 22.222 0h.003z',
|
|
16
|
+
email: 'M3 8l7.89 4.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z',
|
|
17
|
+
youtube: 'M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z',
|
|
18
|
+
discord: 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189z'
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const socialLinks: SocialLink[] = [];
|
|
22
|
+
export const defaultSocialLinks = socialLinks;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Define social links
|
|
26
|
+
*/
|
|
27
|
+
export function defineSocialLinks(links: SocialLink[]): SocialLink[] {
|
|
28
|
+
return links;
|
|
29
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export interface BlogPost {
|
|
2
|
+
slug: string;
|
|
3
|
+
title: string;
|
|
4
|
+
description: string;
|
|
5
|
+
content: string;
|
|
6
|
+
pubDate: Date;
|
|
7
|
+
updatedDate?: Date;
|
|
8
|
+
tags: string[];
|
|
9
|
+
categories: string[];
|
|
10
|
+
author?: string;
|
|
11
|
+
image?: string;
|
|
12
|
+
draft?: boolean;
|
|
13
|
+
readingTime?: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface PostFrontmatter {
|
|
17
|
+
title: string;
|
|
18
|
+
description: string;
|
|
19
|
+
pubDate: string;
|
|
20
|
+
updatedDate?: string;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
categories?: string[];
|
|
23
|
+
author?: string;
|
|
24
|
+
image?: string;
|
|
25
|
+
draft?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface Tag {
|
|
29
|
+
name: string;
|
|
30
|
+
count: number;
|
|
31
|
+
slug: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface Category {
|
|
35
|
+
name: string;
|
|
36
|
+
count: number;
|
|
37
|
+
slug: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface SearchResult {
|
|
41
|
+
title: string;
|
|
42
|
+
description: string;
|
|
43
|
+
url: string;
|
|
44
|
+
content: string;
|
|
45
|
+
tags: string[];
|
|
46
|
+
categories: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface NavigationItem {
|
|
50
|
+
name: string;
|
|
51
|
+
href: string;
|
|
52
|
+
icon?: string;
|
|
53
|
+
children?: NavigationItem[];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export interface SiteConfig {
|
|
57
|
+
title: string;
|
|
58
|
+
description: string;
|
|
59
|
+
author: string;
|
|
60
|
+
email?: string;
|
|
61
|
+
avatar?: string;
|
|
62
|
+
social?: {
|
|
63
|
+
github?: string;
|
|
64
|
+
twitter?: string;
|
|
65
|
+
linkedin?: string;
|
|
66
|
+
email?: string;
|
|
67
|
+
};
|
|
68
|
+
menu: NavigationItem[];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface SEOProps {
|
|
72
|
+
title?: string;
|
|
73
|
+
description?: string;
|
|
74
|
+
image?: string;
|
|
75
|
+
url?: string;
|
|
76
|
+
type?: 'website' | 'article';
|
|
77
|
+
publishedTime?: string;
|
|
78
|
+
modifiedTime?: string;
|
|
79
|
+
tags?: string[];
|
|
80
|
+
}
|
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* i18n Utility Functions
|
|
3
|
+
*
|
|
4
|
+
* Provides helper functions for multi-language support.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type {
|
|
8
|
+
I18nConfig,
|
|
9
|
+
Locale,
|
|
10
|
+
UITranslations,
|
|
11
|
+
} from '../config/i18n';
|
|
12
|
+
import {
|
|
13
|
+
defaultI18nConfig,
|
|
14
|
+
getUITranslations,
|
|
15
|
+
} from '../config/i18n';
|
|
16
|
+
import type { SiteConfig, NavigationItem } from '../types';
|
|
17
|
+
import type { FooterConfig } from '../config/footer';
|
|
18
|
+
import type { SidebarConfig } from '../config/sidebar';
|
|
19
|
+
import { defaultSiteConfig } from '../config/site';
|
|
20
|
+
import { defaultMenu } from '../config/menu';
|
|
21
|
+
import { defaultFooterConfig } from '../config/footer';
|
|
22
|
+
import { sidebarConfig as defaultSidebarConfig } from '../config/sidebar';
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Merged locale configuration with all defaults applied
|
|
26
|
+
*/
|
|
27
|
+
export interface MergedLocaleConfig {
|
|
28
|
+
locale: Locale;
|
|
29
|
+
site: SiteConfig;
|
|
30
|
+
menu: NavigationItem[];
|
|
31
|
+
footer: FooterConfig;
|
|
32
|
+
sidebar: SidebarConfig;
|
|
33
|
+
ui: UITranslations;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Alternate link for SEO (hreflang)
|
|
38
|
+
*/
|
|
39
|
+
export interface AlternateLink {
|
|
40
|
+
locale: string;
|
|
41
|
+
url: string;
|
|
42
|
+
hreflang: string;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get current locale from URL pathname
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* getLocaleFromPath('/en/posts', config) // 'en'
|
|
50
|
+
* getLocaleFromPath('/posts', config) // 'zh-CN' (default)
|
|
51
|
+
* getLocaleFromPath('/zh-CN/about', config) // 'zh-CN'
|
|
52
|
+
*/
|
|
53
|
+
export function getLocaleFromPath(
|
|
54
|
+
pathname: string,
|
|
55
|
+
config: I18nConfig = defaultI18nConfig
|
|
56
|
+
): string {
|
|
57
|
+
// Remove leading slash and get first segment
|
|
58
|
+
const segments = pathname.replace(/^\//, '').split('/');
|
|
59
|
+
const firstSegment = segments[0];
|
|
60
|
+
|
|
61
|
+
// Check if first segment matches any locale code
|
|
62
|
+
const matchedLocale = config.locales.find(
|
|
63
|
+
(locale) => locale.code === firstSegment
|
|
64
|
+
);
|
|
65
|
+
|
|
66
|
+
if (matchedLocale) {
|
|
67
|
+
return matchedLocale.code;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Return default locale
|
|
71
|
+
return config.defaultLocale;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get locale data by code
|
|
76
|
+
*/
|
|
77
|
+
export function getLocaleByCode(
|
|
78
|
+
code: string,
|
|
79
|
+
config: I18nConfig = defaultI18nConfig
|
|
80
|
+
): Locale | undefined {
|
|
81
|
+
return config.locales.find((locale) => locale.code === code);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Remove locale prefix from pathname
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* removeLocalePrefix('/en/posts', config) // '/posts'
|
|
89
|
+
* removeLocalePrefix('/posts', config) // '/posts'
|
|
90
|
+
*/
|
|
91
|
+
export function removeLocalePrefix(
|
|
92
|
+
pathname: string,
|
|
93
|
+
config: I18nConfig = defaultI18nConfig
|
|
94
|
+
): string {
|
|
95
|
+
const locale = getLocaleFromPath(pathname, config);
|
|
96
|
+
|
|
97
|
+
// If it's default locale and prefix is not required, pathname has no prefix
|
|
98
|
+
if (locale === config.defaultLocale && !config.routing.prefixDefaultLocale) {
|
|
99
|
+
return pathname;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Remove the locale prefix
|
|
103
|
+
const prefix = `/${locale}`;
|
|
104
|
+
if (pathname.startsWith(prefix)) {
|
|
105
|
+
const rest = pathname.slice(prefix.length);
|
|
106
|
+
return rest || '/';
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return pathname;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get localized path for a given locale
|
|
114
|
+
*
|
|
115
|
+
* @example
|
|
116
|
+
* getLocalizedPath('/posts', 'en', config) // '/en/posts'
|
|
117
|
+
* getLocalizedPath('/en/posts', 'zh-CN', config) // '/posts' (if zh-CN is default)
|
|
118
|
+
*/
|
|
119
|
+
export function getLocalizedPath(
|
|
120
|
+
pathname: string,
|
|
121
|
+
targetLocale: string,
|
|
122
|
+
config: I18nConfig = defaultI18nConfig
|
|
123
|
+
): string {
|
|
124
|
+
// First, remove any existing locale prefix
|
|
125
|
+
const basePath = removeLocalePrefix(pathname, config);
|
|
126
|
+
|
|
127
|
+
// If target is default locale and prefix is not required
|
|
128
|
+
if (
|
|
129
|
+
targetLocale === config.defaultLocale &&
|
|
130
|
+
!config.routing.prefixDefaultLocale
|
|
131
|
+
) {
|
|
132
|
+
return basePath;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Add locale prefix
|
|
136
|
+
if (basePath === '/') {
|
|
137
|
+
return `/${targetLocale}`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return `/${targetLocale}${basePath}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get all alternate links for SEO (hreflang tags)
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* getAlternateLinks('/posts', 'https://example.com', config)
|
|
148
|
+
* // Returns links for all locales
|
|
149
|
+
*/
|
|
150
|
+
export function getAlternateLinks(
|
|
151
|
+
pathname: string,
|
|
152
|
+
baseUrl: string,
|
|
153
|
+
config: I18nConfig = defaultI18nConfig
|
|
154
|
+
): AlternateLink[] {
|
|
155
|
+
const links: AlternateLink[] = [];
|
|
156
|
+
|
|
157
|
+
for (const locale of config.locales) {
|
|
158
|
+
const localizedPath = getLocalizedPath(pathname, locale.code, config);
|
|
159
|
+
links.push({
|
|
160
|
+
locale: locale.code,
|
|
161
|
+
url: `${baseUrl.replace(/\/$/, '')}${localizedPath}`,
|
|
162
|
+
hreflang: locale.htmlLang,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Add x-default pointing to default locale
|
|
167
|
+
const defaultPath = getLocalizedPath(
|
|
168
|
+
pathname,
|
|
169
|
+
config.defaultLocale,
|
|
170
|
+
config
|
|
171
|
+
);
|
|
172
|
+
links.push({
|
|
173
|
+
locale: 'x-default',
|
|
174
|
+
url: `${baseUrl.replace(/\/$/, '')}${defaultPath}`,
|
|
175
|
+
hreflang: 'x-default',
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
return links;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Deep merge two objects
|
|
183
|
+
*/
|
|
184
|
+
function deepMerge<T extends object>(base: T, override: Partial<T>): T {
|
|
185
|
+
const result = { ...base };
|
|
186
|
+
|
|
187
|
+
for (const key in override) {
|
|
188
|
+
if (Object.prototype.hasOwnProperty.call(override, key)) {
|
|
189
|
+
const overrideValue = override[key];
|
|
190
|
+
const baseValue = base[key];
|
|
191
|
+
|
|
192
|
+
if (
|
|
193
|
+
typeof overrideValue === 'object' &&
|
|
194
|
+
overrideValue !== null &&
|
|
195
|
+
!Array.isArray(overrideValue) &&
|
|
196
|
+
typeof baseValue === 'object' &&
|
|
197
|
+
baseValue !== null &&
|
|
198
|
+
!Array.isArray(baseValue)
|
|
199
|
+
) {
|
|
200
|
+
(result as any)[key] = deepMerge(baseValue as object, overrideValue as object);
|
|
201
|
+
} else if (overrideValue !== undefined) {
|
|
202
|
+
(result as any)[key] = overrideValue;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return result;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Get merged configuration for a specific locale
|
|
212
|
+
* Combines default config with locale-specific overrides
|
|
213
|
+
*/
|
|
214
|
+
export function getLocaleConfig(
|
|
215
|
+
locale: string,
|
|
216
|
+
config: I18nConfig = defaultI18nConfig
|
|
217
|
+
): MergedLocaleConfig {
|
|
218
|
+
const localeData = getLocaleByCode(locale, config);
|
|
219
|
+
const localeOverrides = config.localeConfigs[locale] || {};
|
|
220
|
+
|
|
221
|
+
// Get locale info (fallback to creating one from code)
|
|
222
|
+
const localeInfo: Locale = localeData || {
|
|
223
|
+
code: locale,
|
|
224
|
+
name: locale,
|
|
225
|
+
htmlLang: locale,
|
|
226
|
+
dateLocale: locale,
|
|
227
|
+
direction: 'ltr',
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// Merge site config
|
|
231
|
+
const site = deepMerge(defaultSiteConfig, localeOverrides.site || {});
|
|
232
|
+
|
|
233
|
+
// Use locale-specific menu or default menu
|
|
234
|
+
const menu = localeOverrides.menu || defaultMenu;
|
|
235
|
+
|
|
236
|
+
// Merge footer config
|
|
237
|
+
const footer = deepMerge(defaultFooterConfig, localeOverrides.footer || {});
|
|
238
|
+
|
|
239
|
+
// Merge sidebar config
|
|
240
|
+
const sidebar = localeOverrides.sidebar
|
|
241
|
+
? {
|
|
242
|
+
...defaultSidebarConfig,
|
|
243
|
+
...localeOverrides.sidebar,
|
|
244
|
+
// Use locale-specific groups if provided, otherwise keep default
|
|
245
|
+
groups: localeOverrides.sidebar.groups || defaultSidebarConfig.groups,
|
|
246
|
+
}
|
|
247
|
+
: defaultSidebarConfig;
|
|
248
|
+
|
|
249
|
+
// Get UI translations
|
|
250
|
+
const ui = getUITranslations(locale, config);
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
locale: localeInfo,
|
|
254
|
+
site,
|
|
255
|
+
menu,
|
|
256
|
+
footer,
|
|
257
|
+
sidebar,
|
|
258
|
+
ui,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Translation function - get a UI translation string
|
|
264
|
+
*
|
|
265
|
+
* @example
|
|
266
|
+
* t('readMore', 'en') // 'Read more'
|
|
267
|
+
* t('readMore', 'zh-CN') // '阅读更多'
|
|
268
|
+
*/
|
|
269
|
+
export function t(
|
|
270
|
+
key: keyof UITranslations,
|
|
271
|
+
locale: string,
|
|
272
|
+
config?: I18nConfig
|
|
273
|
+
): string {
|
|
274
|
+
const translations = getUITranslations(locale, config);
|
|
275
|
+
return translations[key] || key;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Format date according to locale
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* formatDate(new Date(), 'en') // 'January 1, 2024'
|
|
283
|
+
* formatDate(new Date(), 'zh-CN') // '2024年1月1日'
|
|
284
|
+
*/
|
|
285
|
+
export function formatDate(
|
|
286
|
+
date: Date | string,
|
|
287
|
+
locale: string,
|
|
288
|
+
options?: Intl.DateTimeFormatOptions
|
|
289
|
+
): string {
|
|
290
|
+
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
291
|
+
|
|
292
|
+
const defaultOptions: Intl.DateTimeFormatOptions = {
|
|
293
|
+
year: 'numeric',
|
|
294
|
+
month: 'long',
|
|
295
|
+
day: 'numeric',
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
return new Intl.DateTimeFormat(locale, options || defaultOptions).format(
|
|
299
|
+
dateObj
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Format date in short format
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* formatDateShort(new Date(), 'en') // '1/1/2024'
|
|
308
|
+
* formatDateShort(new Date(), 'zh-CN') // '2024/1/1'
|
|
309
|
+
*/
|
|
310
|
+
export function formatDateShort(
|
|
311
|
+
date: Date | string,
|
|
312
|
+
locale: string
|
|
313
|
+
): string {
|
|
314
|
+
const dateObj = typeof date === 'string' ? new Date(date) : date;
|
|
315
|
+
|
|
316
|
+
return new Intl.DateTimeFormat(locale, {
|
|
317
|
+
year: 'numeric',
|
|
318
|
+
month: 'numeric',
|
|
319
|
+
day: 'numeric',
|
|
320
|
+
}).format(dateObj);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
/**
|
|
324
|
+
* Check if a locale is RTL (right-to-left)
|
|
325
|
+
*/
|
|
326
|
+
export function isRTL(
|
|
327
|
+
locale: string,
|
|
328
|
+
config: I18nConfig = defaultI18nConfig
|
|
329
|
+
): boolean {
|
|
330
|
+
const localeData = getLocaleByCode(locale, config);
|
|
331
|
+
return localeData?.direction === 'rtl';
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Get the HTML dir attribute value
|
|
336
|
+
*/
|
|
337
|
+
export function getTextDirection(
|
|
338
|
+
locale: string,
|
|
339
|
+
config: I18nConfig = defaultI18nConfig
|
|
340
|
+
): 'ltr' | 'rtl' {
|
|
341
|
+
return isRTL(locale, config) ? 'rtl' : 'ltr';
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Check if multi-language is enabled (more than one locale)
|
|
346
|
+
*/
|
|
347
|
+
export function isMultiLanguageEnabled(
|
|
348
|
+
config: I18nConfig = defaultI18nConfig
|
|
349
|
+
): boolean {
|
|
350
|
+
return config.locales.length > 1;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Get prefix for a locale in routes
|
|
355
|
+
* Returns empty string for default locale if prefixDefaultLocale is false
|
|
356
|
+
*/
|
|
357
|
+
export function getLocalePrefix(
|
|
358
|
+
locale: string,
|
|
359
|
+
config: I18nConfig = defaultI18nConfig
|
|
360
|
+
): string {
|
|
361
|
+
if (locale === config.defaultLocale && !config.routing.prefixDefaultLocale) {
|
|
362
|
+
return '';
|
|
363
|
+
}
|
|
364
|
+
return `/${locale}`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Get content path prefix for a specific locale
|
|
369
|
+
* Returns the contentPathPrefix from locale config, or undefined if not set
|
|
370
|
+
*/
|
|
371
|
+
export function getContentPathPrefix(
|
|
372
|
+
locale: string,
|
|
373
|
+
config: I18nConfig = defaultI18nConfig
|
|
374
|
+
): string | undefined {
|
|
375
|
+
const localeConfig = config.localeConfigs[locale];
|
|
376
|
+
return localeConfig?.contentPathPrefix;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Filter posts by locale based on contentPathPrefix
|
|
381
|
+
* If contentPathPrefix is set, only return posts that start with that prefix
|
|
382
|
+
* If not set, return all posts (backward compatible)
|
|
383
|
+
*
|
|
384
|
+
* @example
|
|
385
|
+
* // If en locale has contentPathPrefix: 'blog_docs_en'
|
|
386
|
+
* filterPostsByLocale(posts, 'en', config)
|
|
387
|
+
* // Returns only posts with id starting with 'blog_docs_en/'
|
|
388
|
+
*/
|
|
389
|
+
export function filterPostsByLocale<T extends { id: string }>(
|
|
390
|
+
posts: T[],
|
|
391
|
+
locale: string,
|
|
392
|
+
config: I18nConfig = defaultI18nConfig
|
|
393
|
+
): T[] {
|
|
394
|
+
const contentPathPrefix = getContentPathPrefix(locale, config);
|
|
395
|
+
|
|
396
|
+
// If no contentPathPrefix is set, return all posts (backward compatible)
|
|
397
|
+
if (!contentPathPrefix) {
|
|
398
|
+
return posts;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// Filter posts that start with the content path prefix
|
|
402
|
+
return posts.filter((post) => {
|
|
403
|
+
const postPath = post.id.toLowerCase();
|
|
404
|
+
const prefix = contentPathPrefix.toLowerCase();
|
|
405
|
+
return postPath.startsWith(prefix + '/') || postPath === prefix;
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// Re-export types and config functions
|
|
410
|
+
export type { I18nConfig, Locale, LocaleConfig, UITranslations } from '../config/i18n';
|
|
411
|
+
export {
|
|
412
|
+
defaultI18nConfig,
|
|
413
|
+
defineI18nConfig,
|
|
414
|
+
getUITranslations,
|
|
415
|
+
builtInTranslations,
|
|
416
|
+
zhCNTranslations,
|
|
417
|
+
enTranslations,
|
|
418
|
+
} from '../config/i18n';
|