@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
@@ -1,6 +1,7 @@
1
1
  import { AstroIntegration } from 'astro';
2
2
  import { AstroI18n, AstroI18nOptions } from './composer';
3
3
  import { Locale, ModuleOptions, PluralFunc } from '@i18n-micro/types';
4
+ import { I18nRoutingStrategy } from './router/types';
4
5
  export interface I18nIntegrationOptions extends Omit<ModuleOptions, 'plural'> {
5
6
  locale: string;
6
7
  fallbackLocale?: string;
@@ -13,7 +14,10 @@ export interface I18nIntegrationOptions extends Omit<ModuleOptions, 'plural'> {
13
14
  autoDetect?: boolean;
14
15
  redirectToDefault?: boolean;
15
16
  translationDir?: string;
17
+ routingStrategy?: I18nRoutingStrategy;
16
18
  }
19
+ export declare function getGlobalRoutingStrategy(): I18nRoutingStrategy | null;
20
+ export declare function setGlobalRoutingStrategy(strategy: I18nRoutingStrategy | null): void;
17
21
  /**
18
22
  * Astro Integration for i18n-micro
19
23
  */
@@ -0,0 +1,44 @@
1
+ import { Translations } from '@i18n-micro/types';
2
+ /**
3
+ * WARNING: Node.js-only functions
4
+ *
5
+ * The functions in this file use Node.js filesystem APIs (node:fs) and will NOT work
6
+ * in Edge runtime environments (Cloudflare Workers, Vercel Edge, Deno Deploy, etc.).
7
+ *
8
+ * If you import this module in an Edge environment, the bundler will fail at build time
9
+ * because node:fs is not available. This is the intended behavior (Fail Fast).
10
+ *
11
+ * For Edge-compatible translation loading, use import.meta.glob in your middleware:
12
+ * const translations = import.meta.glob('/src/locales/*.json', { eager: true })
13
+ */
14
+ /**
15
+ * Load translations from a directory structure
16
+ * Supports both flat structure (en.json, fr.json) and nested structure (pages/home/en.json)
17
+ */
18
+ export interface LoadTranslationsOptions {
19
+ translationDir: string;
20
+ rootDir?: string;
21
+ disablePageLocales?: boolean;
22
+ }
23
+ export interface LoadedTranslations {
24
+ general: Record<string, Translations>;
25
+ routes: Record<string, Record<string, Translations>>;
26
+ }
27
+ /**
28
+ * Load all translations from a directory
29
+ *
30
+ * WARNING: Node.js-only - This function uses node:fs and will NOT work in Edge runtime.
31
+ * If imported in Edge environment, bundler will fail at build time (Fail Fast).
32
+ * Use import.meta.glob for Edge-compatible loading.
33
+ */
34
+ export declare function loadTranslationsFromDir(options: LoadTranslationsOptions): LoadedTranslations;
35
+ /**
36
+ * Load translations and add them to an AstroI18n instance
37
+ *
38
+ * WARNING: Node.js-only - This function uses node:fs and will NOT work in Edge runtime.
39
+ * For Edge environments, use import.meta.glob to load translations at build time.
40
+ */
41
+ export declare function loadTranslationsIntoI18n(i18n: {
42
+ addTranslations: (locale: string, translations: Translations, merge?: boolean) => void;
43
+ addRouteTranslations: (locale: string, routeName: string, translations: Translations, merge?: boolean) => void;
44
+ }, options: LoadTranslationsOptions): void;
@@ -1,6 +1,7 @@
1
1
  import { AstroI18n } from './composer';
2
2
  import { MiddlewareHandler } from 'astro';
3
3
  import { Locale } from '@i18n-micro/types';
4
+ import { I18nRoutingStrategy } from './router/types';
4
5
  export interface I18nMiddlewareOptions {
5
6
  i18n: AstroI18n;
6
7
  defaultLocale: string;
@@ -8,6 +9,7 @@ export interface I18nMiddlewareOptions {
8
9
  localeObjects?: Locale[];
9
10
  autoDetect?: boolean;
10
11
  redirectToDefault?: boolean;
12
+ routingStrategy?: I18nRoutingStrategy;
11
13
  }
12
14
  /**
13
15
  * Create Astro middleware for i18n locale detection
@@ -0,0 +1,8 @@
1
+ import { I18nRoutingStrategy } from './types';
2
+ import { Locale } from '@i18n-micro/types';
3
+ /**
4
+ * Factory for Astro router adapter
5
+ * Implements routing utilities for Astro file-based routing
6
+ * Uses standard Astro APIs: Astro.url, context.url
7
+ */
8
+ export declare function createAstroRouterAdapter(locales: Locale[], defaultLocale: string, getCurrentUrl?: () => URL): I18nRoutingStrategy;
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Routing strategy interface for i18n in Astro
3
+ * Implement this interface to integrate with any routing logic
4
+ * Matches Vue/Solid/React package interface for consistency
5
+ * Adapted for Astro SSR context (push/replace are optional)
6
+ */
7
+ export interface I18nRoutingStrategy {
8
+ /**
9
+ * Returns current path (without locale prefix if needed, or full path)
10
+ * Used for determining active classes in links
11
+ */
12
+ getCurrentPath: () => string;
13
+ /**
14
+ * Generate path for specific locale
15
+ */
16
+ resolvePath?: (to: string | {
17
+ path?: string;
18
+ }, locale: string) => string | {
19
+ path?: string;
20
+ };
21
+ /**
22
+ * Get route name from path (e.g., /en/about -> about)
23
+ * Used in middleware to set route name
24
+ */
25
+ getRouteName?: (path: string, locales: string[]) => string;
26
+ /**
27
+ * Get locale from path
28
+ * Checks if first segment is a locale code
29
+ */
30
+ getLocaleFromPath?: (path: string, defaultLocale: string, locales: string[]) => string;
31
+ /**
32
+ * Switch locale in path
33
+ * Replaces or adds locale prefix to path
34
+ */
35
+ switchLocalePath?: (path: string, newLocale: string, locales: string[], defaultLocale?: string) => string;
36
+ /**
37
+ * Localize path with locale prefix
38
+ */
39
+ localizePath?: (path: string, locale: string, locales: string[], defaultLocale?: string) => string;
40
+ /**
41
+ * Remove locale from path
42
+ */
43
+ removeLocaleFromPath?: (path: string, locales: string[]) => string;
44
+ /**
45
+ * (Optional) Function to navigate to another route/locale
46
+ * Not used in SSR, but can be used in client-side islands
47
+ */
48
+ push?: (target: {
49
+ path: string;
50
+ }) => void;
51
+ /**
52
+ * (Optional) Function to replace current route
53
+ * Not used in SSR, but can be used in client-side islands
54
+ */
55
+ replace?: (target: {
56
+ path: string;
57
+ }) => void;
58
+ /**
59
+ * (Optional) Get current route object for SEO/Meta tags
60
+ */
61
+ getRoute?: () => {
62
+ fullPath: string;
63
+ query: Record<string, unknown>;
64
+ };
65
+ }
package/dist/utils.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { AstroI18n } from './composer';
2
- import { Params, Locale, CleanTranslation, TranslationKey } from '@i18n-micro/types';
2
+ import { Params, Locale, CleanTranslation, TranslationKey, Translations } from '@i18n-micro/types';
3
3
  import { AstroGlobal } from 'astro';
4
4
  /**
5
5
  * Get i18n instance from Astro context
@@ -69,3 +69,19 @@ export interface LocaleHeadResult {
69
69
  }>;
70
70
  }
71
71
  export declare function useLocaleHead(astro: AstroGlobal, options?: LocaleHeadOptions): LocaleHeadResult;
72
+ /**
73
+ * Props для передачи в клиентские острова (Vue, React, Svelte, Preact)
74
+ */
75
+ export interface I18nClientProps {
76
+ locale: string;
77
+ fallbackLocale: string;
78
+ translations: Record<string, Translations>;
79
+ currentRoute: string;
80
+ }
81
+ /**
82
+ * Подготавливает пропсы для передачи в клиентский остров.
83
+ * Принимает список ключей, которые нужно передать в остров.
84
+ * Использует методы i18n для правильной работы с routesLocaleLinks.
85
+ * currentRoute уже нормализован через middleware (getRouteName), поэтому используем его напрямую.
86
+ */
87
+ export declare function getI18nProps(astro: AstroGlobal, keys?: string[]): I18nClientProps;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i18n-micro/astro",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.cjs",
6
6
  "module": "./dist/index.mjs",
@@ -18,31 +18,75 @@
18
18
  "./components/i18n-link.astro": "./src/components/i18n-link.astro",
19
19
  "./components/i18n-switcher.astro": "./src/components/i18n-switcher.astro",
20
20
  "./components/i18n-group.astro": "./src/components/i18n-group.astro",
21
- "./toolbar-app": "./src/toolbar-app.ts"
21
+ "./i18n-t": "./src/components/i18n-t.astro",
22
+ "./i18n-link": "./src/components/i18n-link.astro",
23
+ "./i18n-switcher": "./src/components/i18n-switcher.astro",
24
+ "./i18n-group": "./src/components/i18n-group.astro",
25
+ "./client": {
26
+ "types": "./dist/client/index.d.ts",
27
+ "import": "./dist/client/index.js"
28
+ },
29
+ "./client/vue": {
30
+ "types": "./dist/client/vue.d.ts",
31
+ "import": "./dist/client/vue.js"
32
+ },
33
+ "./client/react": {
34
+ "types": "./dist/client/react.d.ts",
35
+ "import": "./dist/client/react.js"
36
+ },
37
+ "./client/preact": {
38
+ "types": "./dist/client/preact.d.ts",
39
+ "import": "./dist/client/preact.js"
40
+ },
41
+ "./client/svelte": {
42
+ "types": "./dist/client/svelte.d.ts",
43
+ "import": "./dist/client/svelte.js"
44
+ }
22
45
  },
23
46
  "files": [
24
47
  "dist",
25
- "src/components"
48
+ "src"
26
49
  ],
27
50
  "publishConfig": {
28
51
  "access": "public"
29
52
  },
30
53
  "dependencies": {
31
- "@i18n-micro/devtools-ui": "1.0.0",
32
- "@i18n-micro/core": "1.0.27",
33
- "@i18n-micro/types": "1.0.15",
34
- "@i18n-micro/node": "1.0.0"
54
+ "@i18n-micro/core": "1.0.28",
55
+ "@i18n-micro/types": "1.0.16",
56
+ "@i18n-micro/node": "1.0.1"
35
57
  },
36
58
  "peerDependencies": {
37
- "astro": "^5.0.0"
59
+ "astro": "^5.0.0",
60
+ "svelte": "^4.0.0 || ^5.0.0",
61
+ "vue": "^3.0.0",
62
+ "react": "^17.0.0 || ^18.0.0 || ^19.0.0",
63
+ "preact": "^10.0.0"
64
+ },
65
+ "peerDependenciesMeta": {
66
+ "svelte": {
67
+ "optional": true
68
+ },
69
+ "vue": {
70
+ "optional": true
71
+ },
72
+ "react": {
73
+ "optional": true
74
+ },
75
+ "preact": {
76
+ "optional": true
77
+ }
38
78
  },
39
79
  "devDependencies": {
40
- "astro": "^5.0.0",
41
- "vite": "^5.0.0",
42
- "vite-plugin-dts": "^4.3.0",
80
+ "astro": "^5.16.5",
81
+ "vite": "^7.2.7",
82
+ "vite-plugin-dts": "^4.5.4",
43
83
  "jest": "^29.7.0",
44
- "ts-jest": "^29.1.2",
45
- "@types/jest": "^29.5.0"
84
+ "ts-jest": "^29.4.6",
85
+ "@types/jest": "^29.5.14",
86
+ "svelte": "^5.0.0",
87
+ "vue": "^3.4.0",
88
+ "react": "^18.3.0",
89
+ "preact": "^10.0.0"
46
90
  },
47
91
  "scripts": {
48
92
  "build": "vite build",
@@ -0,0 +1,121 @@
1
+ import type { Translations, Params } from '@i18n-micro/types'
2
+ import { interpolate } from '@i18n-micro/core'
3
+
4
+ /**
5
+ * Состояние i18n для клиентских островов
6
+ */
7
+ export interface I18nState {
8
+ locale: string
9
+ fallbackLocale: string
10
+ translations: Record<string, Translations> // routeName -> translations
11
+ currentRoute: string
12
+ }
13
+
14
+ // Вспомогательная функция для поиска перевода в объекте Translations
15
+ // Returns the value as-is, including objects (for nested translations)
16
+ function findTranslation<T = unknown>(translations: Translations | null, key: string): T | null {
17
+ if (translations === null || typeof key !== 'string') {
18
+ return null
19
+ }
20
+
21
+ let value: string | number | boolean | Translations | unknown | null = translations
22
+
23
+ // Прямой доступ к ключу
24
+ if (translations[key]) {
25
+ value = translations[key]
26
+ }
27
+ else {
28
+ // Поиск по вложенным ключам (например, "nested.message")
29
+ const parts = key.toString().split('.')
30
+ for (const part of parts) {
31
+ if (value && typeof value === 'object' && value !== null && part in value) {
32
+ value = (value as Translations)[part]
33
+ }
34
+ else {
35
+ return null
36
+ }
37
+ }
38
+ }
39
+
40
+ // Return value as-is (can be string, number, boolean, object, or null)
41
+ // This matches CleanTranslation type which allows objects
42
+ return (value as T) ?? null
43
+ }
44
+
45
+ /**
46
+ * Чистая функция для получения перевода из состояния
47
+ *
48
+ * Note: This is a simplified version optimized for client-side islands.
49
+ * It supports basic translation lookup, interpolation, and fallback to general translations.
50
+ * Returns CleanTranslation which can be string, number, boolean, object, or null.
51
+ * For advanced features like Linked Messages (@:path.to.key), use the server-side i18n instance.
52
+ */
53
+ export function translate(
54
+ state: I18nState,
55
+ key: string,
56
+ params?: Params,
57
+ defaultValue?: string | null,
58
+ routeName?: string,
59
+ ): string | number | boolean | Translations | null {
60
+ if (!key) {
61
+ return defaultValue || key || ''
62
+ }
63
+
64
+ const route = routeName || state.currentRoute
65
+ let value: string | number | boolean | Translations | null = null
66
+
67
+ // 1. Ищем в route-specific переводах
68
+ if (state.translations[route]) {
69
+ value = findTranslation<string | number | boolean | Translations>(state.translations[route], key)
70
+ }
71
+
72
+ // 2. Fallback на general переводы
73
+ if (!value && state.translations.general) {
74
+ value = findTranslation<string | number | boolean | Translations>(state.translations.general, key)
75
+ }
76
+
77
+ // 3. Если не найдено, используем defaultValue или key
78
+ if (!value) {
79
+ value = defaultValue === undefined ? key : (defaultValue || key)
80
+ }
81
+
82
+ // 4. Интерполяция параметров (только для строк)
83
+ if (typeof value === 'string' && params) {
84
+ return interpolate(value, params)
85
+ }
86
+
87
+ // 5. Возвращаем value как есть (может быть string, number, boolean, object, или null)
88
+ // Это соответствует типу CleanTranslation
89
+ return value
90
+ }
91
+
92
+ /**
93
+ * Проверяет наличие перевода в состоянии
94
+ */
95
+ export function hasTranslation(
96
+ state: I18nState,
97
+ key: string,
98
+ routeName?: string,
99
+ ): boolean {
100
+ const route = routeName || state.currentRoute
101
+ const routeTranslations = state.translations[route]
102
+ const generalTranslations = state.translations.general
103
+
104
+ // Проверяем в route-specific переводах
105
+ if (routeTranslations) {
106
+ const value = findTranslation(routeTranslations, key)
107
+ if (value !== null && typeof value !== 'object') {
108
+ return true
109
+ }
110
+ }
111
+
112
+ // Проверяем в general переводах
113
+ if (generalTranslations) {
114
+ const value = findTranslation(generalTranslations, key)
115
+ if (value !== null && typeof value !== 'object') {
116
+ return true
117
+ }
118
+ }
119
+
120
+ return false
121
+ }
@@ -0,0 +1,15 @@
1
+ // Core utilities (чистые функции)
2
+ export { translate, hasTranslation } from './core'
3
+ export type { I18nState } from './core'
4
+
5
+ // Vue adapter
6
+ export { provideI18n, useAstroI18n as useAstroI18nVue } from './vue'
7
+
8
+ // React adapter
9
+ export { I18nProvider, useAstroI18n as useAstroI18nReact } from './react'
10
+
11
+ // Preact adapter
12
+ export { I18nProvider as I18nProviderPreact, useAstroI18n as useAstroI18nPreact } from './preact'
13
+
14
+ // Svelte adapter
15
+ export { createI18nStore, useAstroI18n as useAstroI18nSvelte } from './svelte'
@@ -0,0 +1,114 @@
1
+ // Preact может использовать React Context API, так как Preact совместим с React
2
+ import type { ComponentChildren } from 'preact'
3
+ import { createContext, createElement } from 'preact'
4
+ import { useContext, useState, useMemo } from 'preact/hooks'
5
+ import type { I18nClientProps } from '../utils'
6
+ import { translate, hasTranslation, type I18nState } from './core'
7
+ import { defaultPlural, FormatService } from '@i18n-micro/core'
8
+ import type { Params, TranslationKey, CleanTranslation } from '@i18n-micro/types'
9
+
10
+ const formatter = new FormatService()
11
+
12
+ const I18nContext = createContext<I18nState | null>(null)
13
+
14
+ /**
15
+ * Провайдер для i18n в Preact островах
16
+ */
17
+ export const I18nProvider = ({ children, value }: { children: ComponentChildren, value: I18nClientProps }) => {
18
+ const [state] = useState<I18nState>(() => ({
19
+ locale: value.locale,
20
+ fallbackLocale: value.fallbackLocale,
21
+ translations: value.translations,
22
+ currentRoute: value.currentRoute,
23
+ }))
24
+
25
+ return createElement(I18nContext.Provider, { value: state }, children)
26
+ }
27
+
28
+ /**
29
+ * Хук для использования i18n в Preact компонентах
30
+ */
31
+ export function useAstroI18n() {
32
+ const state = useContext(I18nContext)
33
+ if (!state) {
34
+ throw new Error('useAstroI18n must be used within an I18nProvider')
35
+ }
36
+
37
+ const t = useMemo(() => (
38
+ key: TranslationKey,
39
+ params?: Params,
40
+ defaultValue?: string | null,
41
+ routeName?: string,
42
+ ): CleanTranslation => {
43
+ return translate(state, key as string, params, defaultValue, routeName)
44
+ }, [state])
45
+
46
+ const ts = useMemo(() => (
47
+ key: TranslationKey,
48
+ params?: Params,
49
+ defaultValue?: string,
50
+ routeName?: string,
51
+ ): string => {
52
+ const value = t(key, params, defaultValue, routeName)
53
+ return value?.toString() ?? defaultValue ?? (key as string)
54
+ }, [t])
55
+
56
+ const tc = useMemo(() => (key: TranslationKey, count: number | Params, defaultValue?: string): string => {
57
+ const { count: countValue, ...params } = typeof count === 'number' ? { count } : count
58
+
59
+ if (countValue === undefined) {
60
+ return defaultValue ?? (key as string)
61
+ }
62
+
63
+ const getter = (k: TranslationKey, p?: Params, dv?: string) => {
64
+ return t(k, p, dv)
65
+ }
66
+
67
+ const result = defaultPlural(
68
+ key,
69
+ Number.parseInt(countValue.toString(), 10),
70
+ params,
71
+ state.locale,
72
+ getter,
73
+ )
74
+
75
+ return result ?? defaultValue ?? (key as string)
76
+ }, [t, state])
77
+
78
+ const tn = useMemo(() => (value: number, options?: Intl.NumberFormatOptions): string => {
79
+ return formatter.formatNumber(value, state.locale, options)
80
+ }, [state.locale])
81
+
82
+ const td = useMemo(() => (value: Date | number | string, options?: Intl.DateTimeFormatOptions): string => {
83
+ return formatter.formatDate(value, state.locale, options)
84
+ }, [state.locale])
85
+
86
+ const tdr = useMemo(() => (value: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string => {
87
+ return formatter.formatRelativeTime(value, state.locale, options)
88
+ }, [state.locale])
89
+
90
+ const has = useMemo(() => (key: TranslationKey, routeName?: string): boolean => {
91
+ return hasTranslation(state, key as string, routeName)
92
+ }, [state])
93
+
94
+ return {
95
+ // Translation methods
96
+ t,
97
+ ts,
98
+ tc,
99
+ tn,
100
+ td,
101
+ tdr,
102
+ has,
103
+
104
+ // Locale state
105
+ locale: state.locale,
106
+ fallbackLocale: state.fallbackLocale,
107
+ currentRoute: state.currentRoute,
108
+
109
+ // Route management (read-only в клиентских островах)
110
+ getRoute: (): string => {
111
+ return state.currentRoute
112
+ },
113
+ }
114
+ }
@@ -0,0 +1,111 @@
1
+ import React, { createContext, useContext, useState, useMemo } from 'react'
2
+ import type { I18nClientProps } from '../utils'
3
+ import { translate, hasTranslation, type I18nState } from './core'
4
+ import { defaultPlural, FormatService } from '@i18n-micro/core'
5
+ import type { Params, TranslationKey, CleanTranslation } from '@i18n-micro/types'
6
+
7
+ const formatter = new FormatService()
8
+
9
+ const I18nContext = createContext<I18nState | null>(null)
10
+
11
+ /**
12
+ * Провайдер для i18n в React островах
13
+ */
14
+ export function I18nProvider({ children, value }: { children: React.ReactNode, value: I18nClientProps }): React.ReactElement {
15
+ const [state] = useState<I18nState>(() => ({
16
+ locale: value.locale,
17
+ fallbackLocale: value.fallbackLocale,
18
+ translations: value.translations,
19
+ currentRoute: value.currentRoute,
20
+ }))
21
+
22
+ return React.createElement(I18nContext.Provider, { value: state }, children)
23
+ }
24
+
25
+ /**
26
+ * Хук для использования i18n в React компонентах
27
+ */
28
+ export function useAstroI18n() {
29
+ const state = useContext(I18nContext)
30
+ if (!state) {
31
+ throw new Error('useAstroI18n must be used within an I18nProvider')
32
+ }
33
+
34
+ const t = useMemo(() => (
35
+ key: TranslationKey,
36
+ params?: Params,
37
+ defaultValue?: string | null,
38
+ routeName?: string,
39
+ ): CleanTranslation => {
40
+ return translate(state, key as string, params, defaultValue, routeName)
41
+ }, [state])
42
+
43
+ const ts = useMemo(() => (
44
+ key: TranslationKey,
45
+ params?: Params,
46
+ defaultValue?: string,
47
+ routeName?: string,
48
+ ): string => {
49
+ const value = t(key, params, defaultValue, routeName)
50
+ return value?.toString() ?? defaultValue ?? (key as string)
51
+ }, [t])
52
+
53
+ const tc = useMemo(() => (key: TranslationKey, count: number | Params, defaultValue?: string): string => {
54
+ const { count: countValue, ...params } = typeof count === 'number' ? { count } : count
55
+
56
+ if (countValue === undefined) {
57
+ return defaultValue ?? (key as string)
58
+ }
59
+
60
+ const getter = (k: TranslationKey, p?: Params, dv?: string) => {
61
+ return t(k, p, dv)
62
+ }
63
+
64
+ const result = defaultPlural(
65
+ key,
66
+ Number.parseInt(countValue.toString(), 10),
67
+ params,
68
+ state.locale,
69
+ getter,
70
+ )
71
+
72
+ return result ?? defaultValue ?? (key as string)
73
+ }, [t, state])
74
+
75
+ const tn = useMemo(() => (value: number, options?: Intl.NumberFormatOptions): string => {
76
+ return formatter.formatNumber(value, state.locale, options)
77
+ }, [state.locale])
78
+
79
+ const td = useMemo(() => (value: Date | number | string, options?: Intl.DateTimeFormatOptions): string => {
80
+ return formatter.formatDate(value, state.locale, options)
81
+ }, [state.locale])
82
+
83
+ const tdr = useMemo(() => (value: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string => {
84
+ return formatter.formatRelativeTime(value, state.locale, options)
85
+ }, [state.locale])
86
+
87
+ const has = useMemo(() => (key: TranslationKey, routeName?: string): boolean => {
88
+ return hasTranslation(state, key as string, routeName)
89
+ }, [state])
90
+
91
+ return {
92
+ // Translation methods
93
+ t,
94
+ ts,
95
+ tc,
96
+ tn,
97
+ td,
98
+ tdr,
99
+ has,
100
+
101
+ // Locale state
102
+ locale: state.locale,
103
+ fallbackLocale: state.fallbackLocale,
104
+ currentRoute: state.currentRoute,
105
+
106
+ // Route management (read-only в клиентских островах)
107
+ getRoute: (): string => {
108
+ return state.currentRoute
109
+ },
110
+ }
111
+ }