@i18n-micro/astro 1.3.25 → 2.0.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 (75) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +8 -64
  3. package/dist/auto-middleware.d.ts +1 -0
  4. package/dist/client/core.d.cts +86 -0
  5. package/dist/client/core.d.ts +71 -8
  6. package/dist/client/index.d.cts +2 -6
  7. package/dist/client/index.d.ts +2 -6
  8. package/dist/client/index.js +1 -6
  9. package/dist/client/preact.d.ts +12 -23
  10. package/dist/client/preact.js +13 -37
  11. package/dist/client/react.d.ts +14 -21
  12. package/dist/client/react.js +12 -37
  13. package/dist/client/svelte.d.ts +14 -14
  14. package/dist/client/svelte.js +17 -47
  15. package/dist/client/vue.d.ts +15 -15
  16. package/dist/client/vue.js +37 -2
  17. package/dist/composer.d.cts +16 -0
  18. package/dist/composer.d.ts +10 -30
  19. package/dist/core-CgrgdmGy.cjs +1 -0
  20. package/dist/core-DTAcn2yF.js +72 -0
  21. package/dist/env.d.ts +8 -2
  22. package/dist/index.cjs +11 -3
  23. package/dist/index.d.cts +10 -13
  24. package/dist/index.d.ts +10 -13
  25. package/dist/index.mjs +171 -482
  26. package/dist/integration.d.cts +26 -0
  27. package/dist/integration.d.ts +14 -15
  28. package/dist/middleware-BT3OA5YL.cjs +1 -0
  29. package/dist/middleware-Ch8E0iV_.js +257 -0
  30. package/dist/middleware.d.cts +34 -0
  31. package/dist/middleware.d.ts +17 -8
  32. package/dist/middleware.js +23 -0
  33. package/dist/node.d.ts +26 -0
  34. package/dist/node.js +22 -0
  35. package/dist/request.d.ts +69 -0
  36. package/dist/router/adapter.d.cts +9 -0
  37. package/dist/router/adapter.d.ts +5 -4
  38. package/dist/router/types.d.cts +66 -0
  39. package/dist/router/types.d.ts +3 -2
  40. package/dist/utils.d.cts +112 -0
  41. package/dist/utils.d.ts +39 -35
  42. package/dist/virtual.d.ts +21 -0
  43. package/package.json +28 -15
  44. package/src/auto-middleware.ts +26 -0
  45. package/src/client/core.ts +124 -59
  46. package/src/client/index.ts +2 -11
  47. package/src/client/preact.tsx +12 -127
  48. package/src/client/react.tsx +11 -123
  49. package/src/client/svelte.ts +6 -69
  50. package/src/client/vue.ts +22 -77
  51. package/src/components/i18n-link.astro +2 -59
  52. package/src/components/i18n-switcher.astro +46 -149
  53. package/src/components/i18n-t.astro +6 -54
  54. package/src/composer.ts +25 -115
  55. package/src/env.d.ts +7 -0
  56. package/src/index.ts +1 -6
  57. package/src/integration.ts +113 -52
  58. package/src/middleware.ts +174 -120
  59. package/src/node.ts +51 -0
  60. package/src/request.ts +177 -0
  61. package/src/router/adapter.ts +47 -149
  62. package/src/router/types.ts +3 -2
  63. package/src/utils.ts +149 -323
  64. package/src/virtual.d.ts +21 -0
  65. package/dist/components/index.d.cts +0 -0
  66. package/dist/components/index.d.ts +0 -0
  67. package/dist/core-DwkpOV-H.cjs +0 -1
  68. package/dist/core-VSnkGRWR.js +0 -28
  69. package/dist/load-translations.d.ts +0 -43
  70. package/dist/routing-context.d.ts +0 -4
  71. package/dist/vue-BNiEwZr2.cjs +0 -5
  72. package/dist/vue-CWHjIOiS.js +0 -1724
  73. package/src/components/index.ts +0 -11
  74. package/src/load-translations.ts +0 -106
  75. package/src/routing-context.ts +0 -23
@@ -20,9 +20,10 @@ export interface I18nRoutingStrategy {
20
20
  };
21
21
  /**
22
22
  * Get route name from path (e.g., /en/about -> about)
23
- * Used in middleware to set route name
23
+ * Used in middleware to set route name. `routePattern` is the page's pattern (`/blog/[slug]`), with
24
+ * a `[locale]` param already replaced by its value: use it to give every URL of a page one name.
24
25
  */
25
- getRouteName?: (path: string, locales: string[]) => string;
26
+ getRouteName?: (path: string, locales: string[], routePattern?: string) => string;
26
27
  /**
27
28
  * Get locale from path
28
29
  * Checks if first segment is a locale code
@@ -0,0 +1,112 @@
1
+ import { ResolveTextOptions } from '@i18n-micro/core';
2
+ import { CleanTranslation, Locale, Params, TranslationKey, Translations } from '@i18n-micro/types';
3
+ import { LocaleHead } from '@i18n-micro/utils/locale-head';
4
+ import { AstroGlobal } from 'astro';
5
+ import { AstroI18n } from './composer.cjs';
6
+ /** The subset of `AstroGlobal` the helpers read. */
7
+ type Astro = Pick<AstroGlobal, 'locals' | 'url'> & Partial<Pick<AstroGlobal, 'params' | 'site'>>;
8
+ /**
9
+ * Get i18n instance from Astro context
10
+ */
11
+ export declare function getI18n(astro: Pick<AstroGlobal, 'locals'>): AstroI18n;
12
+ /**
13
+ * Get current locale from Astro context
14
+ */
15
+ export declare function getLocale(astro: Pick<AstroGlobal, 'locals'>): string;
16
+ /**
17
+ * Get default locale from Astro context
18
+ */
19
+ export declare function getDefaultLocale(astro: Pick<AstroGlobal, 'locals'>): string;
20
+ /**
21
+ * Get all locales from Astro context
22
+ */
23
+ export declare function getLocales(astro: Pick<AstroGlobal, 'locals'>): Locale[];
24
+ /**
25
+ * Use i18n in Astro pages/components
26
+ * Returns helper functions for translations
27
+ */
28
+ export declare function useI18n(astro: Astro): {
29
+ locale: string;
30
+ defaultLocale: string;
31
+ locales: Locale[];
32
+ t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
33
+ ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeContext?: unknown) => string;
34
+ tc: (key: TranslationKey, count: number | Params, defaultValue?: string) => string;
35
+ tn: {
36
+ (value: number, options?: Intl.NumberFormatOptions): string;
37
+ (value: number, key: string, overrides?: Intl.NumberFormatOptions): string;
38
+ (value: number, key: string, locale: string, overrides?: Intl.NumberFormatOptions): string;
39
+ };
40
+ td: {
41
+ (value: Date | number | string, options?: Intl.DateTimeFormatOptions): string;
42
+ (value: Date | number | string, key: string, overrides?: Intl.DateTimeFormatOptions): string;
43
+ (value: Date | number | string, key: string, locale: string, overrides?: Intl.DateTimeFormatOptions): string;
44
+ };
45
+ tdr: (value: Date | number | string, options?: Intl.RelativeTimeFormatOptions) => string;
46
+ has: (key: TranslationKey, routeContext?: unknown) => boolean;
47
+ getRoute: () => string;
48
+ getRouteName: (path?: string) => string;
49
+ getLocaleFromPath: (path?: string) => string;
50
+ switchLocalePath: (newLocale: string) => string;
51
+ localizePath: (path: string, targetLocale?: string) => string;
52
+ getI18n: () => AstroI18n;
53
+ getBasePath: (url?: URL) => string;
54
+ addTranslations: (locale: string, translations: Translations, merge?: boolean) => void;
55
+ addRouteTranslations: (locale: string, routeName: string, translations: Translations, merge?: boolean) => void;
56
+ mergeTranslations: (locale: string, routeName: string, translations: Translations) => void;
57
+ clearCache: () => void;
58
+ };
59
+ /** What `<I18nSwitcher>` shows for a locale. A code is free-form: `constructor` must not read an inherited member. */
60
+ export declare function localeLabel(locale: Locale, customLabels: Record<string, string>): string;
61
+ /** `href` of an `<I18nLink>`: relative to the page, in `locale` (default the current one). */
62
+ export declare function resolveLinkHref(astro: Astro, href: string, locale?: string): string;
63
+ export interface I18nTProps extends ResolveTextOptions {
64
+ defaultValue?: string;
65
+ }
66
+ /** The text of an `<I18nT>`, `null` when `hideIfEmpty` hides it. With `html`, params arrive escaped; the translation is trusted. */
67
+ export declare function resolveI18nT(astro: Astro, props: I18nTProps): string | null;
68
+ /**
69
+ * Strip locale prefix from the current URL for `Astro.rewrite()` on `/[locale]/*` routes.
70
+ */
71
+ export declare function getLocaleRewritePath(astro: Astro): string;
72
+ /**
73
+ * Validate `:locale`, sync i18n/locals, and return the base path for `Astro.rewrite()`.
74
+ * Returns a `Response` when the locale param is invalid.
75
+ */
76
+ export declare function prepareLocaleRewrite(astro: Astro & Pick<AstroGlobal, 'params'>): Response | string;
77
+ /**
78
+ * Generate locale head meta tags for SEO
79
+ */
80
+ export interface LocaleHeadOptions {
81
+ /** Origin of the absolute URLs. Default: `site` from the Astro config, else root-relative URLs. */
82
+ baseUrl?: string;
83
+ addDirAttribute?: boolean;
84
+ addSeoAttributes?: boolean;
85
+ /**
86
+ * Also emit bare-language `hreflang` tags derived from `iso` (never from routing `code`).
87
+ * @default false
88
+ */
89
+ hreflangBaseLanguage?: boolean;
90
+ }
91
+ export type LocaleHeadResult = LocaleHead;
92
+ export declare function useLocaleHead(astro: Astro, options?: LocaleHeadOptions): LocaleHeadResult;
93
+ /**
94
+ * Props to pass to client islands (Vue, React, Svelte, Preact)
95
+ */
96
+ export interface I18nClientProps {
97
+ locale: string;
98
+ /** The tag plurals go by: the locale's `iso` when it has one. */
99
+ pluralLocale?: string;
100
+ /** Tags of the codes that have one, for `setLocale`. Absent when there are none. */
101
+ localeIso?: Record<string, string>;
102
+ fallbackLocale: string;
103
+ translations: Record<string, Translations>;
104
+ currentRoute: string;
105
+ }
106
+ /**
107
+ * Props for a client island: what `t()` can answer on this page (route, root and fallback keys),
108
+ * or only `keys` of that, which is the way to keep the page small. Stored trees are shared, an
109
+ * island must not write into them.
110
+ */
111
+ export declare function getI18nProps(astro: Pick<AstroGlobal, 'locals'>, keys?: string[]): I18nClientProps;
112
+ export {};
package/dist/utils.d.ts CHANGED
@@ -1,32 +1,36 @@
1
+ import { ResolveTextOptions } from '@i18n-micro/core';
1
2
  import { CleanTranslation, Locale, Params, TranslationKey, Translations } from '@i18n-micro/types';
3
+ import { LocaleHead } from '@i18n-micro/utils/locale-head';
2
4
  import { AstroGlobal } from 'astro';
3
- import { AstroI18n } from './composer';
5
+ import { AstroI18n } from './composer.js';
6
+ /** The subset of `AstroGlobal` the helpers read. */
7
+ type Astro = Pick<AstroGlobal, 'locals' | 'url'> & Partial<Pick<AstroGlobal, 'params' | 'site'>>;
4
8
  /**
5
9
  * Get i18n instance from Astro context
6
10
  */
7
- export declare function getI18n(astro: AstroGlobal): AstroI18n;
11
+ export declare function getI18n(astro: Pick<AstroGlobal, 'locals'>): AstroI18n;
8
12
  /**
9
13
  * Get current locale from Astro context
10
14
  */
11
- export declare function getLocale(astro: AstroGlobal): string;
15
+ export declare function getLocale(astro: Pick<AstroGlobal, 'locals'>): string;
12
16
  /**
13
17
  * Get default locale from Astro context
14
18
  */
15
- export declare function getDefaultLocale(astro: AstroGlobal): string;
19
+ export declare function getDefaultLocale(astro: Pick<AstroGlobal, 'locals'>): string;
16
20
  /**
17
21
  * Get all locales from Astro context
18
22
  */
19
- export declare function getLocales(astro: AstroGlobal): Locale[];
23
+ export declare function getLocales(astro: Pick<AstroGlobal, 'locals'>): Locale[];
20
24
  /**
21
25
  * Use i18n in Astro pages/components
22
26
  * Returns helper functions for translations
23
27
  */
24
- export declare function useI18n(astro: AstroGlobal): {
28
+ export declare function useI18n(astro: Astro): {
25
29
  locale: string;
26
30
  defaultLocale: string;
27
31
  locales: Locale[];
28
- t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeName?: string) => CleanTranslation;
29
- ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeName?: string) => string;
32
+ t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeContext?: unknown) => CleanTranslation;
33
+ ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeContext?: unknown) => string;
30
34
  tc: (key: TranslationKey, count: number | Params, defaultValue?: string) => string;
31
35
  tn: {
32
36
  (value: number, options?: Intl.NumberFormatOptions): string;
@@ -39,7 +43,7 @@ export declare function useI18n(astro: AstroGlobal): {
39
43
  (value: Date | number | string, key: string, locale: string, overrides?: Intl.DateTimeFormatOptions): string;
40
44
  };
41
45
  tdr: (value: Date | number | string, options?: Intl.RelativeTimeFormatOptions) => string;
42
- has: (key: TranslationKey, routeName?: string) => boolean;
46
+ has: (key: TranslationKey, routeContext?: unknown) => boolean;
43
47
  getRoute: () => string;
44
48
  getRouteName: (path?: string) => string;
45
49
  getLocaleFromPath: (path?: string) => string;
@@ -47,24 +51,34 @@ export declare function useI18n(astro: AstroGlobal): {
47
51
  localizePath: (path: string, targetLocale?: string) => string;
48
52
  getI18n: () => AstroI18n;
49
53
  getBasePath: (url?: URL) => string;
50
- addTranslations: (locale: string, translations: Record<string, unknown>, merge?: boolean) => void;
51
- addRouteTranslations: (locale: string, routeName: string, translations: Record<string, unknown>, merge?: boolean) => void;
52
- mergeTranslations: (locale: string, routeName: string, translations: Record<string, unknown>) => void;
54
+ addTranslations: (locale: string, translations: Translations, merge?: boolean) => void;
55
+ addRouteTranslations: (locale: string, routeName: string, translations: Translations, merge?: boolean) => void;
56
+ mergeTranslations: (locale: string, routeName: string, translations: Translations) => void;
53
57
  clearCache: () => void;
54
58
  };
59
+ /** What `<I18nSwitcher>` shows for a locale. A code is free-form: `constructor` must not read an inherited member. */
60
+ export declare function localeLabel(locale: Locale, customLabels: Record<string, string>): string;
61
+ /** `href` of an `<I18nLink>`: relative to the page, in `locale` (default the current one). */
62
+ export declare function resolveLinkHref(astro: Astro, href: string, locale?: string): string;
63
+ export interface I18nTProps extends ResolveTextOptions {
64
+ defaultValue?: string;
65
+ }
66
+ /** The text of an `<I18nT>`, `null` when `hideIfEmpty` hides it. With `html`, params arrive escaped; the translation is trusted. */
67
+ export declare function resolveI18nT(astro: Astro, props: I18nTProps): string | null;
55
68
  /**
56
69
  * Strip locale prefix from the current URL for `Astro.rewrite()` on `/[locale]/*` routes.
57
70
  */
58
- export declare function getLocaleRewritePath(astro: AstroGlobal): string;
71
+ export declare function getLocaleRewritePath(astro: Astro): string;
59
72
  /**
60
73
  * Validate `:locale`, sync i18n/locals, and return the base path for `Astro.rewrite()`.
61
74
  * Returns a `Response` when the locale param is invalid.
62
75
  */
63
- export declare function prepareLocaleRewrite(astro: AstroGlobal): Response | string;
76
+ export declare function prepareLocaleRewrite(astro: Astro & Pick<AstroGlobal, 'params'>): Response | string;
64
77
  /**
65
78
  * Generate locale head meta tags for SEO
66
79
  */
67
80
  export interface LocaleHeadOptions {
81
+ /** Origin of the absolute URLs. Default: `site` from the Astro config, else root-relative URLs. */
68
82
  baseUrl?: string;
69
83
  addDirAttribute?: boolean;
70
84
  addSeoAttributes?: boolean;
@@ -74,35 +88,25 @@ export interface LocaleHeadOptions {
74
88
  */
75
89
  hreflangBaseLanguage?: boolean;
76
90
  }
77
- export interface LocaleHeadResult {
78
- htmlAttrs: {
79
- lang?: string;
80
- dir?: 'ltr' | 'rtl' | 'auto';
81
- };
82
- link: Array<{
83
- rel: string;
84
- href: string;
85
- hreflang?: string;
86
- }>;
87
- meta: Array<{
88
- property: string;
89
- content: string;
90
- }>;
91
- }
92
- export declare function useLocaleHead(astro: AstroGlobal, options?: LocaleHeadOptions): LocaleHeadResult;
91
+ export type LocaleHeadResult = LocaleHead;
92
+ export declare function useLocaleHead(astro: Astro, options?: LocaleHeadOptions): LocaleHeadResult;
93
93
  /**
94
94
  * Props to pass to client islands (Vue, React, Svelte, Preact)
95
95
  */
96
96
  export interface I18nClientProps {
97
97
  locale: string;
98
+ /** The tag plurals go by: the locale's `iso` when it has one. */
99
+ pluralLocale?: string;
100
+ /** Tags of the codes that have one, for `setLocale`. Absent when there are none. */
101
+ localeIso?: Record<string, string>;
98
102
  fallbackLocale: string;
99
103
  translations: Record<string, Translations>;
100
104
  currentRoute: string;
101
105
  }
102
106
  /**
103
- * Prepares props to pass to a client island.
104
- * Accepts a list of keys to pass to the island.
105
- * Uses i18n methods for proper routesLocaleLinks support.
106
- * currentRoute is already normalized via middleware (getRouteName), so we use it directly.
107
+ * Props for a client island: what `t()` can answer on this page (route, root and fallback keys),
108
+ * or only `keys` of that, which is the way to keep the page small. Stored trees are shared, an
109
+ * island must not write into them.
107
110
  */
108
- export declare function getI18nProps(astro: AstroGlobal, keys?: string[]): I18nClientProps;
111
+ export declare function getI18nProps(astro: Pick<AstroGlobal, 'locals'>, keys?: string[]): I18nClientProps;
112
+ export {};
@@ -0,0 +1,21 @@
1
+ declare module 'virtual:i18n-micro/config' {
2
+ export const config: {
3
+ defaultLocale: string
4
+ fallbackLocale: string
5
+ locales: import('@i18n-micro/types').Locale[]
6
+ localeCodes: string[]
7
+ translationDir: string | null
8
+ autoDetect: boolean
9
+ redirectToDefault: boolean
10
+ localeCookie: string | null
11
+ missingWarn: boolean
12
+ base: string
13
+ }
14
+ }
15
+
16
+ declare module 'virtual:i18n-micro/messages' {
17
+ export const messages: {
18
+ root: Record<string, import('@i18n-micro/types').Translations>
19
+ routes: Record<string, Record<string, import('@i18n-micro/types').Translations>>
20
+ }
21
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@i18n-micro/astro",
3
- "version": "1.3.25",
3
+ "version": "2.0.0",
4
4
  "description": "Astro integration for i18n-micro — translations, routing, and islands.",
5
5
  "keywords": [
6
6
  "astro",
@@ -13,6 +13,11 @@
13
13
  "url": "https://github.com/s00d/nuxt-i18n-micro/issues"
14
14
  },
15
15
  "license": "MIT",
16
+ "author": {
17
+ "name": "s00d",
18
+ "email": "Virus191288@gmail.com",
19
+ "url": "https://s00d.github.io/"
20
+ },
16
21
  "repository": {
17
22
  "type": "git",
18
23
  "url": "git+https://github.com/s00d/nuxt-i18n-micro.git",
@@ -40,6 +45,14 @@
40
45
  },
41
46
  "default": "./dist/index.mjs"
42
47
  },
48
+ "./middleware": {
49
+ "types": "./dist/auto-middleware.d.ts",
50
+ "import": "./dist/middleware.js"
51
+ },
52
+ "./node": {
53
+ "types": "./dist/node.d.ts",
54
+ "import": "./dist/node.js"
55
+ },
43
56
  "./env": {
44
57
  "types": "./dist/env.d.ts"
45
58
  },
@@ -80,28 +93,28 @@
80
93
  "access": "public"
81
94
  },
82
95
  "dependencies": {
83
- "@i18n-micro/core": "^1.3.22",
84
- "@i18n-micro/node": "^1.2.21",
85
- "@i18n-micro/types": "^1.2.18",
86
- "@i18n-micro/utils": "^1.0.27"
96
+ "@i18n-micro/core": "^2.0.0",
97
+ "@i18n-micro/types": "^2.0.0",
98
+ "@i18n-micro/utils": "^2.0.0",
99
+ "@i18n-micro/runtime": "^2.0.0"
87
100
  },
88
101
  "devDependencies": {
89
102
  "astro": "^5.0.0 || ^6.0.0 || ^7.0.0",
90
- "preact": "^10.27.2 || ^11.0.0-0",
91
- "publint": "^0.3.24",
92
- "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
103
+ "preact": "^10.0.0 || ^11.0.0",
104
+ "publint": "^0.3.25",
105
+ "react": "^18.0.0 || ^19.0.0",
93
106
  "svelte": "^4.0.0 || ^5.0.0",
94
- "vite": "^8.3.1",
95
- "vite-plugin-dts": "^5.1.1",
96
- "vitest": "^5.0.2",
97
- "vue": "^3.5.43"
107
+ "vite": "^8.3.5",
108
+ "vite-plugin-dts": "^5.1.2",
109
+ "vitest": "^5.1.0",
110
+ "vue": "^3.5.44"
98
111
  },
99
112
  "peerDependencies": {
100
113
  "astro": "^5.0.0 || ^6.0.0 || ^7.0.0",
101
- "preact": "^10.27.2 || ^11.0.0-0",
114
+ "preact": "^10.0.0 || ^11.0.0",
102
115
  "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
103
116
  "svelte": "^4.0.0 || ^5.0.0",
104
- "vue": "^3.5.43"
117
+ "vue": "^3.3.0"
105
118
  },
106
119
  "peerDependenciesMeta": {
107
120
  "svelte": {
@@ -118,7 +131,7 @@
118
131
  }
119
132
  },
120
133
  "engines": {
121
- "node": ">=18"
134
+ "node": ">=20"
122
135
  },
123
136
  "scripts": {
124
137
  "build": "vite build",
@@ -0,0 +1,26 @@
1
+ /** `@i18n-micro/astro/middleware`: what `i18nIntegration` installs. Nothing here reads the disk, so it runs on the edge too. */
2
+ import { config } from 'virtual:i18n-micro/config'
3
+ import { messages } from 'virtual:i18n-micro/messages'
4
+ import { localeIsoMap } from '@i18n-micro/core'
5
+ import { AstroI18n } from './composer'
6
+ import { createI18nMiddleware } from './middleware'
7
+
8
+ const i18n = new AstroI18n({
9
+ locale: config.defaultLocale,
10
+ fallbackLocale: config.fallbackLocale,
11
+ missingWarn: config.missingWarn,
12
+ localeIso: localeIsoMap(config.locales),
13
+ messages: messages.root,
14
+ routeMessages: messages.routes,
15
+ })
16
+
17
+ export const onRequest = createI18nMiddleware({
18
+ i18n,
19
+ defaultLocale: config.defaultLocale,
20
+ locales: config.localeCodes,
21
+ localeObjects: config.locales,
22
+ autoDetect: config.autoDetect,
23
+ redirectToDefault: config.redirectToDefault,
24
+ localeCookie: config.localeCookie,
25
+ base: config.base,
26
+ })
@@ -1,52 +1,37 @@
1
- import { interpolate } from '@i18n-micro/core'
2
- import type { Params, Translations } from '@i18n-micro/types'
1
+ import { convertible, defaultPlural, FormatService, interpolate, ownValue, resolveTranslation, textOf } from '@i18n-micro/core'
2
+ import type { CleanTranslation, Getter, Params, TranslationKey, Translations } from '@i18n-micro/types'
3
+ import type { I18nClientProps } from '../utils'
3
4
 
4
5
  /**
5
6
  * I18n state for client islands
6
7
  */
7
8
  export interface I18nState {
8
9
  locale: string
10
+ /** The tag plurals go by, when it is not `locale`. */
11
+ pluralLocale?: string
12
+ /** Tags of the codes that have one: `setLocale` finds the new locale's here. */
13
+ localeIso?: Record<string, string>
9
14
  fallbackLocale: string
10
15
  translations: Record<string, Translations> // routeName -> translations
11
16
  currentRoute: string
12
17
  }
13
18
 
14
- // Helper function to find a translation in a Translations object
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
- }
19
+ type Value = string | number | boolean | Translations
20
20
 
21
- let value: string | number | boolean | Translations | unknown | null = translations
22
-
23
- // Direct key access
24
- if (translations[key]) {
25
- value = translations[key]
26
- } else {
27
- // Search by nested keys (e.g. "nested.message")
28
- const parts = key.toString().split('.')
29
- for (const part of parts) {
30
- if (value && typeof value === 'object' && value !== null && part in value) {
31
- value = (value as Translations)[part]
32
- } else {
33
- return null
34
- }
35
- }
36
- }
37
-
38
- // Return value as-is (can be string, number, boolean, object, or null)
39
- // This matches CleanTranslation type which allows objects
40
- return (value as T) ?? null
21
+ /**
22
+ * The value at `key` in the route's dictionary, or null. Own keys only (core's `resolveTranslation`):
23
+ * `toString` or `__proto__` is a missing key, not `Object.prototype`'s.
24
+ */
25
+ function lookup(state: I18nState, key: string, routeName?: string): Value | null {
26
+ const route = routeName || state.currentRoute
27
+ const translations = Object.prototype.hasOwnProperty.call(state.translations, route) ? state.translations[route] : undefined
28
+ return (resolveTranslation(translations, key) as Value | null) ?? null
41
29
  }
42
30
 
43
31
  /**
44
- * Pure function to get a translation from state
45
- *
46
- * Note: This is a simplified version optimized for client-side islands.
47
- * It supports basic translation lookup and interpolation.
48
- * Returns CleanTranslation which can be string, number, boolean, object, or null.
49
- * For advanced features like Linked Messages (@:path.to.key), use the server-side i18n instance.
32
+ * Pure function to get a translation from state, answering as the server's `t()` does over the
33
+ * same keys. `getI18nProps` already merged the root and fallback dictionaries into the state.
34
+ * For Linked Messages (@:path.to.key), use the server-side i18n instance.
50
35
  */
51
36
  export function translate(
52
37
  state: I18nState,
@@ -55,41 +40,121 @@ export function translate(
55
40
  defaultValue?: string | null,
56
41
  routeName?: string,
57
42
  ): string | number | boolean | Translations | null {
58
- if (!key) {
59
- return defaultValue || key || ''
43
+ if (!key) return ''
44
+ // `??`: 0, false and '' are values, as on the server
45
+ const value = lookup(state, key, routeName) ?? (defaultValue || key)
46
+ if (typeof value === 'object') return convertible(value)
47
+ return typeof value === 'string' && params ? interpolate(value, params) : value
48
+ }
49
+
50
+ /**
51
+ * `tc` of every island adapter, as the server's `BaseI18n.tc`: a missing key gives `defaultValue`
52
+ * (else the key), because the getter reports the miss to `defaultPlural` instead of the key.
53
+ */
54
+ export function translatePlural(state: I18nState, key: TranslationKey, count: number | Params, defaultValue?: string): string {
55
+ const { count: countValue, ...params } = typeof count === 'number' ? { count } : count
56
+ if (countValue === undefined || countValue === null) return defaultValue ?? key
57
+ const getter: Getter = (k, p, dv) => {
58
+ const raw = lookup(state, k)
59
+ if (raw === null) return dv === undefined ? undefined : dv || k
60
+ return p && typeof raw === 'string' ? interpolate(raw, p) : raw
60
61
  }
62
+ return defaultPlural(key, Number(countValue), params, state.pluralLocale ?? state.locale, getter) ?? defaultValue ?? key
63
+ }
61
64
 
62
- const route = routeName || state.currentRoute
63
- let value: string | number | boolean | Translations | null = null
65
+ /**
66
+ * Checks if a translation exists in the state: a subtree counts, as on the server
67
+ */
68
+ export function hasTranslation(state: I18nState, key: string, routeName?: string): boolean {
69
+ return lookup(state, key, routeName) !== null
70
+ }
64
71
 
65
- if (state.translations[route]) {
66
- value = findTranslation<string | number | boolean | Translations>(state.translations[route], key)
67
- }
72
+ /** `state` in another locale, with that locale's plural tag. */
73
+ export const withLocale = (state: I18nState, locale: string): I18nState => ({
74
+ ...state,
75
+ locale,
76
+ pluralLocale: state.localeIso && (ownValue(state.localeIso, locale) as string | undefined),
77
+ })
68
78
 
69
- // If not found, use defaultValue or key
70
- if (!value) {
71
- value = defaultValue === undefined ? key : defaultValue || key
72
- }
79
+ const formatter = /*#__PURE__*/ new FormatService()
73
80
 
74
- if (typeof value === 'string' && params) {
75
- return interpolate(value, params)
81
+ const isoTag = (state: I18nState): string => state.pluralLocale ?? state.locale
82
+
83
+ /** What every island adapter returns, over the state it reads at each call. */
84
+ export function createIslandApi(getState: () => I18nState) {
85
+ const t = (key: TranslationKey, params?: Params, defaultValue?: string | null, routeName?: string): CleanTranslation =>
86
+ translate(getState(), key as string, params, defaultValue, routeName)
87
+
88
+ return {
89
+ t,
90
+ ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeName?: string): string =>
91
+ textOf(t(key, params, defaultValue, routeName)) ?? defaultValue ?? (key as string),
92
+ tc: (key: TranslationKey, count: number | Params, defaultValue?: string): string => translatePlural(getState(), key, count, defaultValue),
93
+ tn: (value: number, options?: Intl.NumberFormatOptions): string => formatter.formatNumber(value, isoTag(getState()), options),
94
+ td: (value: Date | number | string, options?: Intl.DateTimeFormatOptions): string => formatter.formatDate(value, isoTag(getState()), options),
95
+ tdr: (value: Date | number | string, options?: Intl.RelativeTimeFormatOptions): string =>
96
+ formatter.formatRelativeTime(value, isoTag(getState()), options),
97
+ has: (key: TranslationKey, routeName?: string): boolean => hasTranslation(getState(), key as string, routeName),
98
+ getRoute: (): string => getState().currentRoute,
76
99
  }
77
- return value
78
100
  }
79
101
 
80
- /**
81
- * Checks if a translation exists in the state
82
- */
83
- export function hasTranslation(state: I18nState, key: string, routeName?: string): boolean {
84
- const route = routeName || state.currentRoute
85
- const routeTranslations = state.translations[route]
102
+ type SetI18nState = (update: I18nState | ((current: I18nState) => I18nState)) => void
103
+
104
+ export interface I18nContextValue {
105
+ state: I18nState
106
+ setState: SetI18nState
107
+ }
108
+
109
+ /** What an island binding needs of its library: core stays free of framework imports. */
110
+ interface IslandLibrary<Children, Element, Context extends { Provider: unknown }> {
111
+ createContext: (value: I18nContextValue | null) => Context
112
+ createElement: (type: Context['Provider'], props: { value: I18nContextValue }, children: Children) => Element
113
+ useCallback: <T extends (arg: string) => void>(callback: T, deps: unknown[]) => T
114
+ useContext: (context: Context) => I18nContextValue | null
115
+ useMemo: <T>(factory: () => T, deps: unknown[]) => T
116
+ useState: <T>(initial: T) => [T, (update: T | ((current: T) => T)) => void]
117
+ }
118
+
119
+ /** The provider and hook of an island library (Preact and React have one API). */
120
+ export function createIslandBinding<Children, Element, Context extends { Provider: unknown }>(lib: IslandLibrary<Children, Element, Context>) {
121
+ const { createContext, createElement, useCallback, useContext, useMemo, useState } = lib
122
+ const I18nContext = createContext(null)
123
+
124
+ /** A new `value` (an island with `transition:persist`) replaces the state. */
125
+ function I18nProvider({ children, value }: { children: Children; value: I18nClientProps }): Element {
126
+ const [state, setState] = useState<I18nState>(value)
127
+ const [seen, setSeen] = useState(value)
128
+ if (seen !== value) {
129
+ setSeen(value)
130
+ setState(value)
131
+ }
132
+ const context = useMemo(() => ({ state, setState }), [state])
133
+ return createElement(I18nContext.Provider, { value: context }, children)
134
+ }
135
+
136
+ function useAstroI18n() {
137
+ const context = useContext(I18nContext)
138
+ if (!context) {
139
+ throw new Error('useAstroI18n must be used within an I18nProvider')
140
+ }
141
+ const { state, setState } = context
142
+ const api = useMemo(() => createIslandApi(() => state), [state])
143
+ const setLocale = useCallback((locale: string) => setState((current) => withLocale(current, locale)), [setState])
144
+ const setRoute = useCallback((currentRoute: string) => setState((current) => ({ ...current, currentRoute })), [setState])
145
+
146
+ return {
147
+ ...api,
148
+
149
+ // Locale state
150
+ locale: state.locale,
151
+ fallbackLocale: state.fallbackLocale,
152
+ currentRoute: state.currentRoute,
86
153
 
87
- if (routeTranslations) {
88
- const value = findTranslation(routeTranslations, key)
89
- if (value !== null && typeof value !== 'object') {
90
- return true
154
+ setLocale,
155
+ setRoute,
91
156
  }
92
157
  }
93
158
 
94
- return false
159
+ return { I18nProvider, useAstroI18n }
95
160
  }
@@ -1,13 +1,4 @@
1
- // Core utilities (pure functions)
2
-
1
+ // The pure core. Framework adapters live in their own entries (`./client/vue`, `./client/react`,
2
+ // `./client/preact`, `./client/svelte`): each needs its framework installed, and an island needs one.
3
3
  export type { I18nState } from './core'
4
4
  export { hasTranslation, translate } from './core'
5
- // Preact adapter
6
- export { I18nProvider as I18nProviderPreact, useAstroI18n as useAstroI18nPreact } from './preact'
7
-
8
- // React adapter
9
- export { I18nProvider, useAstroI18n as useAstroI18nReact } from './react'
10
- // Svelte adapter
11
- export { createI18nStore, useAstroI18n as useAstroI18nSvelte } from './svelte'
12
- // Vue adapter
13
- export { provideI18n, useAstroI18n as useAstroI18nVue } from './vue'