@jet-w/astro-blog 0.2.2 → 0.2.4

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.
@@ -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';