@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.
Files changed (48) hide show
  1. package/dist/client/core.d.ts +23 -0
  2. package/dist/client/index.d.ts +6 -0
  3. package/dist/client/index.js +17 -0
  4. package/dist/client/preact.d.ts +30 -0
  5. package/dist/client/preact.js +51 -0
  6. package/dist/client/react.d.ts +26 -0
  7. package/dist/client/react.js +51 -0
  8. package/dist/client/svelte.d.ts +28 -0
  9. package/dist/client/svelte.js +70 -0
  10. package/dist/client/vue.d.ts +27 -0
  11. package/dist/client/vue.js +1558 -0
  12. package/dist/composer.d.ts +21 -15
  13. package/dist/core-Bx9n-eFD.cjs +1 -0
  14. package/dist/core-D32Y48CN.js +42 -0
  15. package/dist/env.d.ts +2 -0
  16. package/dist/index.cjs +17 -1
  17. package/dist/index.d.ts +6 -2
  18. package/dist/index.mjs +442 -282
  19. package/dist/integration.d.ts +4 -0
  20. package/dist/load-translations.d.ts +44 -0
  21. package/dist/middleware.d.ts +2 -0
  22. package/dist/router/adapter.d.ts +8 -0
  23. package/dist/router/types.d.ts +65 -0
  24. package/dist/utils.d.ts +17 -1
  25. package/package.json +57 -13
  26. package/src/client/core.ts +121 -0
  27. package/src/client/index.ts +15 -0
  28. package/src/client/preact.tsx +114 -0
  29. package/src/client/react.tsx +111 -0
  30. package/src/client/svelte.ts +124 -0
  31. package/src/client/vue.ts +128 -0
  32. package/src/components/i18n-link.astro +37 -4
  33. package/src/components/i18n-switcher.astro +209 -17
  34. package/src/components/index.ts +8 -2
  35. package/src/composer.ts +210 -0
  36. package/src/env.d.ts +20 -0
  37. package/src/index.ts +59 -0
  38. package/src/integration.ts +120 -0
  39. package/src/load-translations.ts +130 -0
  40. package/src/middleware.ts +203 -0
  41. package/src/router/adapter.ts +184 -0
  42. package/src/router/types.ts +66 -0
  43. package/src/routing.ts +108 -0
  44. package/src/utils.ts +397 -0
  45. package/dist/bridge/astro-bridge.d.ts +0 -13
  46. package/dist/index-C-UMdqSG.cjs +0 -1
  47. package/dist/index-CVhedN6W.js +0 -146
  48. package/dist/toolbar-app.d.ts +0 -2
@@ -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
+ }