@i18n-micro/astro 1.0.0 → 1.1.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/client/core.d.ts +23 -0
- package/dist/client/index.d.ts +6 -0
- package/dist/client/index.js +17 -0
- package/dist/client/preact.d.ts +30 -0
- package/dist/client/preact.js +51 -0
- package/dist/client/react.d.ts +26 -0
- package/dist/client/react.js +51 -0
- package/dist/client/svelte.d.ts +28 -0
- package/dist/client/svelte.js +70 -0
- package/dist/client/vue.d.ts +27 -0
- package/dist/client/vue.js +1558 -0
- package/dist/composer.d.ts +21 -15
- package/dist/core-Bx9n-eFD.cjs +1 -0
- package/dist/core-D32Y48CN.js +42 -0
- package/dist/env.d.ts +2 -0
- package/dist/index.cjs +17 -1
- package/dist/index.d.ts +6 -2
- package/dist/index.mjs +442 -282
- package/dist/integration.d.ts +4 -0
- package/dist/load-translations.d.ts +44 -0
- package/dist/middleware.d.ts +2 -0
- package/dist/router/adapter.d.ts +8 -0
- package/dist/router/types.d.ts +65 -0
- package/dist/utils.d.ts +17 -1
- package/package.json +57 -13
- package/src/client/core.ts +121 -0
- package/src/client/index.ts +15 -0
- package/src/client/preact.tsx +114 -0
- package/src/client/react.tsx +111 -0
- package/src/client/svelte.ts +124 -0
- package/src/client/vue.ts +128 -0
- package/src/components/i18n-link.astro +37 -4
- package/src/components/i18n-switcher.astro +209 -17
- package/src/components/index.ts +8 -2
- package/src/composer.ts +210 -0
- package/src/env.d.ts +20 -0
- package/src/index.ts +59 -0
- package/src/integration.ts +120 -0
- package/src/load-translations.ts +130 -0
- package/src/middleware.ts +203 -0
- package/src/router/adapter.ts +184 -0
- package/src/router/types.ts +66 -0
- package/src/routing.ts +108 -0
- package/src/utils.ts +397 -0
- package/dist/bridge/astro-bridge.d.ts +0 -13
- package/dist/index-C-UMdqSG.cjs +0 -1
- package/dist/index-CVhedN6W.js +0 -146
- package/dist/toolbar-app.d.ts +0 -2
package/src/composer.ts
ADDED
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BaseI18n,
|
|
3
|
+
type TranslationCache,
|
|
4
|
+
} from '@i18n-micro/core'
|
|
5
|
+
import type {
|
|
6
|
+
Translations,
|
|
7
|
+
PluralFunc,
|
|
8
|
+
} from '@i18n-micro/types'
|
|
9
|
+
|
|
10
|
+
export interface AstroI18nOptions {
|
|
11
|
+
locale: string
|
|
12
|
+
fallbackLocale?: string
|
|
13
|
+
messages?: Record<string, Translations>
|
|
14
|
+
plural?: PluralFunc
|
|
15
|
+
missingWarn?: boolean
|
|
16
|
+
missingHandler?: (locale: string, key: string, routeName: string) => void
|
|
17
|
+
// NEW: Allow passing existing cache
|
|
18
|
+
_cache?: TranslationCache
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class AstroI18n extends BaseI18n {
|
|
22
|
+
private _locale: string
|
|
23
|
+
private _fallbackLocale: string
|
|
24
|
+
private _currentRoute: string
|
|
25
|
+
|
|
26
|
+
// Кэш для Core (без Vue реактивности)
|
|
27
|
+
public readonly cache: TranslationCache
|
|
28
|
+
|
|
29
|
+
// Сохраняем начальные переводы для восстановления после clearCache
|
|
30
|
+
private initialMessages: Record<string, Translations> = {}
|
|
31
|
+
|
|
32
|
+
constructor(options: AstroI18nOptions) {
|
|
33
|
+
// Если передан существующий кэш (от глобального инстанса), используем его.
|
|
34
|
+
// Иначе создаем новый.
|
|
35
|
+
const cache: TranslationCache = options._cache || {
|
|
36
|
+
generalLocaleCache: {},
|
|
37
|
+
routeLocaleCache: {},
|
|
38
|
+
dynamicTranslationsCaches: [],
|
|
39
|
+
serverTranslationCache: {},
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Call parent constructor with options
|
|
43
|
+
super({
|
|
44
|
+
cache,
|
|
45
|
+
plural: options.plural,
|
|
46
|
+
missingWarn: options.missingWarn,
|
|
47
|
+
missingHandler: options.missingHandler,
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
// Assign cache to public readonly property
|
|
51
|
+
this.cache = cache
|
|
52
|
+
|
|
53
|
+
this._locale = options.locale
|
|
54
|
+
this._fallbackLocale = options.fallbackLocale || options.locale
|
|
55
|
+
this._currentRoute = 'general'
|
|
56
|
+
|
|
57
|
+
// Загружаем начальные сообщения (только если это первичная инициализация или добавление новых)
|
|
58
|
+
if (options.messages) {
|
|
59
|
+
// Сохраняем начальные переводы для восстановления после clearCache
|
|
60
|
+
this.initialMessages = { ...options.messages }
|
|
61
|
+
for (const [lang, msgs] of Object.entries(options.messages)) {
|
|
62
|
+
this.helper.loadTranslations(lang, msgs)
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Clone cache with shallow copy to prevent memory leaks
|
|
69
|
+
* Each request-scoped instance gets its own cache structure,
|
|
70
|
+
* but can read from the global cache (read-only access to existing translations)
|
|
71
|
+
*/
|
|
72
|
+
private cloneCache(sourceCache: TranslationCache): TranslationCache {
|
|
73
|
+
// Helper to get value from RefLike or plain value
|
|
74
|
+
const getValue = <T>(refOrValue: T | { value: T }): T => {
|
|
75
|
+
return typeof refOrValue === 'object' && refOrValue !== null && 'value' in refOrValue
|
|
76
|
+
? (refOrValue as { value: T }).value
|
|
77
|
+
: refOrValue as T
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get actual values from cache (handling RefLike)
|
|
81
|
+
const generalCache = getValue(sourceCache.generalLocaleCache)
|
|
82
|
+
const routeCache = getValue(sourceCache.routeLocaleCache)
|
|
83
|
+
const dynamicCaches = getValue(sourceCache.dynamicTranslationsCaches)
|
|
84
|
+
const serverCache = getValue(sourceCache.serverTranslationCache)
|
|
85
|
+
|
|
86
|
+
// Create new cache structure with shallow copy of existing translations
|
|
87
|
+
// This allows read-only access to global translations while isolating writes
|
|
88
|
+
return {
|
|
89
|
+
generalLocaleCache: { ...generalCache },
|
|
90
|
+
routeLocaleCache: { ...routeCache },
|
|
91
|
+
dynamicTranslationsCaches: [...dynamicCaches],
|
|
92
|
+
serverTranslationCache: { ...serverCache },
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Create a request-scoped instance with isolated cache
|
|
98
|
+
* Prevents memory leaks by isolating per-request translations from global cache
|
|
99
|
+
*/
|
|
100
|
+
public clone(newLocale?: string): AstroI18n {
|
|
101
|
+
// Create isolated cache for this request to prevent memory leaks
|
|
102
|
+
// The new cache can read from global translations (shallow copy),
|
|
103
|
+
// but writes (addTranslations, addRouteTranslations) won't affect global cache
|
|
104
|
+
const isolatedCache = this.cloneCache(this.cache)
|
|
105
|
+
|
|
106
|
+
return new AstroI18n({
|
|
107
|
+
locale: newLocale || this._locale,
|
|
108
|
+
fallbackLocale: this._fallbackLocale,
|
|
109
|
+
plural: this.pluralFunc,
|
|
110
|
+
missingWarn: this.missingWarn,
|
|
111
|
+
missingHandler: this.missingHandler,
|
|
112
|
+
_cache: isolatedCache, // Изолированный кэш для предотвращения утечек памяти
|
|
113
|
+
})
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Геттер/Сеттер для локали
|
|
117
|
+
get locale(): string {
|
|
118
|
+
return this._locale
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
set locale(val: string) {
|
|
122
|
+
this._locale = val
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Геттер/Сеттер для fallback локали
|
|
126
|
+
get fallbackLocale(): string {
|
|
127
|
+
return this._fallbackLocale
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
set fallbackLocale(val: string) {
|
|
131
|
+
this._fallbackLocale = val
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Геттер/Сеттер для текущего роута
|
|
135
|
+
get currentRoute(): string {
|
|
136
|
+
return this._currentRoute
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
setRoute(routeName: string): void {
|
|
140
|
+
this._currentRoute = routeName
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// --- Implementation of abstract methods ---
|
|
144
|
+
|
|
145
|
+
public getLocale(): string {
|
|
146
|
+
return this._locale
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
public getFallbackLocale(): string {
|
|
150
|
+
return this._fallbackLocale
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public getRoute(): string {
|
|
154
|
+
return this._currentRoute
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get route-specific translations for a given locale and route
|
|
159
|
+
* This method encapsulates the cache key format, making it safe to use
|
|
160
|
+
* without direct cache access
|
|
161
|
+
*/
|
|
162
|
+
public getRouteTranslations(locale: string, routeName: string): Translations | null {
|
|
163
|
+
const cacheKey = `${locale}:${routeName}`
|
|
164
|
+
const routeCache = this.cache.routeLocaleCache
|
|
165
|
+
|
|
166
|
+
// Cache is a plain object in AstroI18n (not Vue ref)
|
|
167
|
+
if (routeCache && typeof routeCache === 'object' && !Array.isArray(routeCache)) {
|
|
168
|
+
return (routeCache as Record<string, Translations>)[cacheKey] || null
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return null
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Методы для добавления переводов
|
|
175
|
+
public addTranslations(locale: string, translations: Translations, merge: boolean = true): void {
|
|
176
|
+
super.loadTranslationsCore(locale, translations, merge)
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
public addRouteTranslations(
|
|
180
|
+
locale: string,
|
|
181
|
+
routeName: string,
|
|
182
|
+
translations: Translations,
|
|
183
|
+
merge: boolean = true,
|
|
184
|
+
): void {
|
|
185
|
+
super.loadRouteTranslationsCore(locale, routeName, translations, merge)
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
public mergeTranslations(locale: string, routeName: string, translations: Translations): void {
|
|
189
|
+
this.helper.mergeTranslation(locale, routeName, translations, true)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
public mergeGlobalTranslations(locale: string, translations: Translations): void {
|
|
193
|
+
this.helper.mergeGlobalTranslation(locale, translations, true)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
public override clearCache(): void {
|
|
197
|
+
// Сохраняем начальные переводы перед очисткой
|
|
198
|
+
const initialMessages = { ...this.initialMessages }
|
|
199
|
+
|
|
200
|
+
// Очищаем кэш
|
|
201
|
+
super.clearCache()
|
|
202
|
+
|
|
203
|
+
// Восстанавливаем начальные переводы
|
|
204
|
+
if (Object.keys(initialMessages).length > 0) {
|
|
205
|
+
for (const [lang, msgs] of Object.entries(initialMessages)) {
|
|
206
|
+
this.helper.loadTranslations(lang, msgs)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
package/src/env.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/// <reference types="astro/client" />
|
|
2
|
+
|
|
3
|
+
import type { AstroI18n } from './composer'
|
|
4
|
+
import type { Locale } from '@i18n-micro/types'
|
|
5
|
+
import type { I18nRoutingStrategy } from './router/types'
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
namespace App {
|
|
9
|
+
interface Locals {
|
|
10
|
+
i18n: AstroI18n
|
|
11
|
+
locale: string
|
|
12
|
+
defaultLocale: string
|
|
13
|
+
locales: Locale[]
|
|
14
|
+
currentUrl: URL
|
|
15
|
+
routingStrategy?: I18nRoutingStrategy
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export {}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Import shim to ensure types are available
|
|
2
|
+
import './env.d'
|
|
3
|
+
|
|
4
|
+
// Main exports
|
|
5
|
+
export { i18nIntegration, createI18n } from './integration'
|
|
6
|
+
export { AstroI18n, type AstroI18nOptions } from './composer'
|
|
7
|
+
export { createI18nMiddleware, detectLocale } from './middleware'
|
|
8
|
+
export type { I18nMiddlewareOptions } from './middleware'
|
|
9
|
+
|
|
10
|
+
// Utilities
|
|
11
|
+
export {
|
|
12
|
+
useI18n,
|
|
13
|
+
getI18n,
|
|
14
|
+
getLocale,
|
|
15
|
+
getDefaultLocale,
|
|
16
|
+
getLocales,
|
|
17
|
+
useLocaleHead,
|
|
18
|
+
getI18nProps,
|
|
19
|
+
} from './utils'
|
|
20
|
+
export type { LocaleHeadOptions, LocaleHeadResult, I18nClientProps } from './utils'
|
|
21
|
+
|
|
22
|
+
// Router abstraction
|
|
23
|
+
export type { I18nRoutingStrategy } from './router/types'
|
|
24
|
+
export { createAstroRouterAdapter } from './router/adapter'
|
|
25
|
+
|
|
26
|
+
// Legacy routing utilities (deprecated, use routingStrategy instead)
|
|
27
|
+
// Kept for backward compatibility
|
|
28
|
+
export {
|
|
29
|
+
getRouteName,
|
|
30
|
+
getLocaleFromPath,
|
|
31
|
+
switchLocalePath,
|
|
32
|
+
localizePath,
|
|
33
|
+
removeLocaleFromPath,
|
|
34
|
+
} from './routing'
|
|
35
|
+
|
|
36
|
+
// Re-export types from @i18n-micro/types
|
|
37
|
+
export type {
|
|
38
|
+
Translations,
|
|
39
|
+
Params,
|
|
40
|
+
PluralFunc,
|
|
41
|
+
Getter,
|
|
42
|
+
Locale,
|
|
43
|
+
LocaleCode,
|
|
44
|
+
CleanTranslation,
|
|
45
|
+
} from '@i18n-micro/types'
|
|
46
|
+
|
|
47
|
+
// Re-export utilities from core
|
|
48
|
+
export { interpolate, FormatService, defaultPlural } from '@i18n-micro/core'
|
|
49
|
+
|
|
50
|
+
// Export integration options type
|
|
51
|
+
export type { I18nIntegrationOptions } from './integration'
|
|
52
|
+
|
|
53
|
+
// Export translation loading utilities
|
|
54
|
+
export {
|
|
55
|
+
loadTranslationsFromDir,
|
|
56
|
+
loadTranslationsIntoI18n,
|
|
57
|
+
} from './load-translations'
|
|
58
|
+
export type { LoadTranslationsOptions, LoadedTranslations } from './load-translations'
|
|
59
|
+
// It's loaded directly by Astro via entrypoint in integration config
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import type { AstroIntegration, HookParameters } from 'astro'
|
|
2
|
+
import { AstroI18n, type AstroI18nOptions } from './composer'
|
|
3
|
+
import type { Locale, ModuleOptions, PluralFunc } from '@i18n-micro/types'
|
|
4
|
+
import type { I18nRoutingStrategy } from './router/types'
|
|
5
|
+
|
|
6
|
+
export interface I18nIntegrationOptions extends Omit<ModuleOptions, 'plural'> {
|
|
7
|
+
locale: string
|
|
8
|
+
fallbackLocale?: string
|
|
9
|
+
locales?: Locale[]
|
|
10
|
+
messages?: Record<string, Record<string, unknown>>
|
|
11
|
+
plural?: PluralFunc
|
|
12
|
+
missingWarn?: boolean
|
|
13
|
+
missingHandler?: (locale: string, key: string, routeName: string) => void
|
|
14
|
+
localeCookie?: string
|
|
15
|
+
autoDetect?: boolean
|
|
16
|
+
redirectToDefault?: boolean
|
|
17
|
+
translationDir?: string
|
|
18
|
+
routingStrategy?: I18nRoutingStrategy
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let globalRoutingStrategy: I18nRoutingStrategy | null = null
|
|
22
|
+
|
|
23
|
+
export function getGlobalRoutingStrategy(): I18nRoutingStrategy | null {
|
|
24
|
+
return globalRoutingStrategy
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function setGlobalRoutingStrategy(strategy: I18nRoutingStrategy | null): void {
|
|
28
|
+
globalRoutingStrategy = strategy
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Astro Integration for i18n-micro
|
|
33
|
+
*/
|
|
34
|
+
export function i18nIntegration(options: I18nIntegrationOptions): AstroIntegration {
|
|
35
|
+
const {
|
|
36
|
+
locale: defaultLocale,
|
|
37
|
+
fallbackLocale,
|
|
38
|
+
translationDir,
|
|
39
|
+
routingStrategy,
|
|
40
|
+
} = options
|
|
41
|
+
|
|
42
|
+
globalRoutingStrategy = routingStrategy || null
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
name: '@i18n-micro/astro',
|
|
46
|
+
hooks: {
|
|
47
|
+
// 1. Настройка Vite (Виртуальный модуль) происходит здесь
|
|
48
|
+
'astro:config:setup': (params) => {
|
|
49
|
+
const { updateConfig } = params as HookParameters<'astro:config:setup'>
|
|
50
|
+
|
|
51
|
+
const virtualModuleId = 'virtual:i18n-micro/config'
|
|
52
|
+
const resolvedVirtualModuleId = '\0' + virtualModuleId
|
|
53
|
+
|
|
54
|
+
const configData = {
|
|
55
|
+
defaultLocale,
|
|
56
|
+
fallbackLocale: fallbackLocale || defaultLocale,
|
|
57
|
+
locales: options.locales || [],
|
|
58
|
+
localeCodes: (options.locales || []).map(l => l.code),
|
|
59
|
+
translationDir: translationDir || null,
|
|
60
|
+
autoDetect: options.autoDetect ?? true,
|
|
61
|
+
redirectToDefault: options.redirectToDefault ?? false,
|
|
62
|
+
localeCookie: options.localeCookie || 'i18n-locale',
|
|
63
|
+
missingWarn: options.missingWarn ?? false,
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
updateConfig({
|
|
67
|
+
vite: {
|
|
68
|
+
plugins: [
|
|
69
|
+
{
|
|
70
|
+
name: 'vite-plugin-i18n-micro-config',
|
|
71
|
+
resolveId(id) {
|
|
72
|
+
if (id === virtualModuleId) {
|
|
73
|
+
return resolvedVirtualModuleId
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
load(id) {
|
|
77
|
+
if (id === resolvedVirtualModuleId) {
|
|
78
|
+
return `export const config = ${JSON.stringify(configData)}`
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
},
|
|
86
|
+
|
|
87
|
+
// 2. Инъекция типов происходит здесь (согласно документации Astro)
|
|
88
|
+
'astro:config:done': (params) => {
|
|
89
|
+
const { injectTypes } = params as HookParameters<'astro:config:done'>
|
|
90
|
+
|
|
91
|
+
injectTypes({
|
|
92
|
+
filename: 'i18n-micro-env.d.ts',
|
|
93
|
+
content: `
|
|
94
|
+
/// <reference types="@i18n-micro/astro/env" />
|
|
95
|
+
|
|
96
|
+
declare module 'virtual:i18n-micro/config' {
|
|
97
|
+
export const config: {
|
|
98
|
+
defaultLocale: string;
|
|
99
|
+
fallbackLocale: string;
|
|
100
|
+
locales: import('@i18n-micro/types').Locale[];
|
|
101
|
+
localeCodes: string[];
|
|
102
|
+
translationDir: string | null;
|
|
103
|
+
autoDetect: boolean;
|
|
104
|
+
redirectToDefault: boolean;
|
|
105
|
+
localeCookie: string;
|
|
106
|
+
missingWarn: boolean;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`,
|
|
110
|
+
})
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create i18n instance (for manual setup)
|
|
117
|
+
*/
|
|
118
|
+
export function createI18n(options: AstroI18nOptions): AstroI18n {
|
|
119
|
+
return new AstroI18n(options)
|
|
120
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs'
|
|
2
|
+
import { resolve, join } from 'node:path'
|
|
3
|
+
import type { Translations } from '@i18n-micro/types'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* WARNING: Node.js-only functions
|
|
7
|
+
*
|
|
8
|
+
* The functions in this file use Node.js filesystem APIs (node:fs) and will NOT work
|
|
9
|
+
* in Edge runtime environments (Cloudflare Workers, Vercel Edge, Deno Deploy, etc.).
|
|
10
|
+
*
|
|
11
|
+
* If you import this module in an Edge environment, the bundler will fail at build time
|
|
12
|
+
* because node:fs is not available. This is the intended behavior (Fail Fast).
|
|
13
|
+
*
|
|
14
|
+
* For Edge-compatible translation loading, use import.meta.glob in your middleware:
|
|
15
|
+
* const translations = import.meta.glob('/src/locales/*.json', { eager: true })
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Load translations from a directory structure
|
|
20
|
+
* Supports both flat structure (en.json, fr.json) and nested structure (pages/home/en.json)
|
|
21
|
+
*/
|
|
22
|
+
export interface LoadTranslationsOptions {
|
|
23
|
+
translationDir: string
|
|
24
|
+
rootDir?: string
|
|
25
|
+
disablePageLocales?: boolean
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LoadedTranslations {
|
|
29
|
+
general: Record<string, Translations>
|
|
30
|
+
routes: Record<string, Record<string, Translations>>
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Load all translations from a directory
|
|
35
|
+
*
|
|
36
|
+
* WARNING: Node.js-only - This function uses node:fs and will NOT work in Edge runtime.
|
|
37
|
+
* If imported in Edge environment, bundler will fail at build time (Fail Fast).
|
|
38
|
+
* Use import.meta.glob for Edge-compatible loading.
|
|
39
|
+
*/
|
|
40
|
+
export function loadTranslationsFromDir(options: LoadTranslationsOptions): LoadedTranslations {
|
|
41
|
+
const { translationDir, rootDir = process.cwd(), disablePageLocales = false } = options
|
|
42
|
+
const fullTranslationDir = resolve(rootDir, translationDir)
|
|
43
|
+
|
|
44
|
+
if (!existsSync(fullTranslationDir)) {
|
|
45
|
+
console.warn(`[i18n] Translation directory not found: ${fullTranslationDir}`)
|
|
46
|
+
return { general: {}, routes: {} }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const general: Record<string, Translations> = {}
|
|
50
|
+
const routes: Record<string, Record<string, Translations>> = {}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Recursively load translation files
|
|
54
|
+
*/
|
|
55
|
+
const loadFiles = (dir: string, routePrefix = ''): void => {
|
|
56
|
+
if (!existsSync(dir)) return
|
|
57
|
+
|
|
58
|
+
const entries = readdirSync(dir)
|
|
59
|
+
for (const entry of entries) {
|
|
60
|
+
const fullPath = join(dir, entry)
|
|
61
|
+
const stat = statSync(fullPath)
|
|
62
|
+
|
|
63
|
+
if (stat.isDirectory()) {
|
|
64
|
+
// If it's a 'pages' directory and page locales are enabled, treat as route-specific
|
|
65
|
+
if (entry === 'pages' && !disablePageLocales) {
|
|
66
|
+
loadFiles(fullPath, '')
|
|
67
|
+
}
|
|
68
|
+
else if (routePrefix || disablePageLocales) {
|
|
69
|
+
// Continue in general translations
|
|
70
|
+
loadFiles(fullPath, routePrefix)
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// This is a route directory (e.g., pages/home/)
|
|
74
|
+
loadFiles(fullPath, entry)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
else if (entry.endsWith('.json')) {
|
|
78
|
+
const locale = entry.replace('.json', '')
|
|
79
|
+
try {
|
|
80
|
+
const content = readFileSync(fullPath, 'utf-8')
|
|
81
|
+
const translations = JSON.parse(content) as Translations
|
|
82
|
+
|
|
83
|
+
if (routePrefix && !disablePageLocales) {
|
|
84
|
+
// Route-specific translation
|
|
85
|
+
if (!routes[routePrefix]) {
|
|
86
|
+
routes[routePrefix] = {}
|
|
87
|
+
}
|
|
88
|
+
routes[routePrefix][locale] = translations
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
// General translation
|
|
92
|
+
general[locale] = translations
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
console.error(`[i18n] Failed to load translation file: ${fullPath}`, error)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
loadFiles(fullTranslationDir)
|
|
103
|
+
|
|
104
|
+
return { general, routes }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Load translations and add them to an AstroI18n instance
|
|
109
|
+
*
|
|
110
|
+
* WARNING: Node.js-only - This function uses node:fs and will NOT work in Edge runtime.
|
|
111
|
+
* For Edge environments, use import.meta.glob to load translations at build time.
|
|
112
|
+
*/
|
|
113
|
+
export function loadTranslationsIntoI18n(
|
|
114
|
+
i18n: { addTranslations: (locale: string, translations: Translations, merge?: boolean) => void, addRouteTranslations: (locale: string, routeName: string, translations: Translations, merge?: boolean) => void },
|
|
115
|
+
options: LoadTranslationsOptions,
|
|
116
|
+
): void {
|
|
117
|
+
const { general, routes } = loadTranslationsFromDir(options)
|
|
118
|
+
|
|
119
|
+
// Load general translations
|
|
120
|
+
for (const [locale, translations] of Object.entries(general)) {
|
|
121
|
+
i18n.addTranslations(locale, translations, false)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Load route-specific translations
|
|
125
|
+
for (const [routeName, routeTranslations] of Object.entries(routes)) {
|
|
126
|
+
for (const [locale, translations] of Object.entries(routeTranslations)) {
|
|
127
|
+
i18n.addRouteTranslations(locale, routeName, translations, false)
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|