@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
package/src/utils.ts CHANGED
@@ -1,15 +1,21 @@
1
+ import { getByPath, interpolate, isProduction, ownValue, resolveText, setTranslationAtKey, type ResolveTextOptions } from '@i18n-micro/core'
1
2
  import type { CleanTranslation, Locale, Params, TranslationKey, Translations } from '@i18n-micro/types'
2
- import { resolveHreflangAlternates } from '@i18n-micro/utils/resolve-hreflang'
3
- import { resolveOgLocale, warnUnresolvedOgLocale } from '@i18n-micro/utils/resolve-og-locale'
3
+ import { getEnabledLocaleCodes, getEnabledLocales } from '@i18n-micro/utils/active-locales'
4
+ import { withoutAppBaseURL } from '@i18n-micro/utils/app-path'
5
+ import { buildLocaleHead, joinBase, type LocaleHead } from '@i18n-micro/utils/locale-head'
4
6
  import type { AstroGlobal } from 'astro'
5
7
  import type { AstroI18n } from './composer'
6
- import type { I18nRoutingStrategy } from './router/types'
8
+ import { localizeHref, withBase, type HrefContext } from './request'
9
+ import { createAstroRouterAdapter } from './router/adapter'
7
10
  import './env.d'
8
11
 
12
+ /** The subset of `AstroGlobal` the helpers read. */
13
+ type Astro = Pick<AstroGlobal, 'locals' | 'url'> & Partial<Pick<AstroGlobal, 'params' | 'site'>>
14
+
9
15
  /**
10
16
  * Get i18n instance from Astro context
11
17
  */
12
- export function getI18n(astro: AstroGlobal): AstroI18n {
18
+ export function getI18n(astro: Pick<AstroGlobal, 'locals'>): AstroI18n {
13
19
  const i18n = astro.locals.i18n
14
20
  if (!i18n) {
15
21
  throw new Error('i18n instance not found. Make sure i18n middleware is configured.')
@@ -21,42 +27,51 @@ export function getI18n(astro: AstroGlobal): AstroI18n {
21
27
  /**
22
28
  * Get current locale from Astro context
23
29
  */
24
- export function getLocale(astro: AstroGlobal): string {
25
- return astro.locals.locale || 'en'
30
+ export function getLocale(astro: Pick<AstroGlobal, 'locals'>): string {
31
+ return astro.locals.locale ?? getI18n(astro).getLocale()
26
32
  }
27
33
 
28
34
  /**
29
35
  * Get default locale from Astro context
30
36
  */
31
- export function getDefaultLocale(astro: AstroGlobal): string {
32
- return astro.locals.defaultLocale || 'en'
37
+ export function getDefaultLocale(astro: Pick<AstroGlobal, 'locals'>): string {
38
+ return astro.locals.defaultLocale ?? getI18n(astro).getFallbackLocale()
33
39
  }
34
40
 
35
41
  /**
36
42
  * Get all locales from Astro context
37
43
  */
38
- export function getLocales(astro: AstroGlobal): Locale[] {
44
+ export function getLocales(astro: Pick<AstroGlobal, 'locals'>): Locale[] {
39
45
  return astro.locals.locales || []
40
46
  }
41
47
 
42
- /**
43
- * Get routing strategy from Astro locals
44
- */
45
- function getRoutingStrategy(astro: AstroGlobal): I18nRoutingStrategy | null {
46
- return (astro.locals.routingStrategy as I18nRoutingStrategy | undefined) || null
48
+ /** How this request builds URLs. Without the middleware's strategy, the default one. */
49
+ function hrefContext(astro: Astro): HrefContext & { path: string } {
50
+ const { locals } = astro
51
+ const base = locals.i18nBase ?? ''
52
+ const defaultLocale = getDefaultLocale(astro)
53
+ const locales = getLocales(astro)
54
+ return {
55
+ base,
56
+ defaultLocale,
57
+ codes: getEnabledLocaleCodes(locales),
58
+ strategy: locals.routingStrategy ?? createAstroRouterAdapter(locales, defaultLocale),
59
+ path: withoutAppBaseURL((locals.currentUrl ?? astro.url).pathname, base),
60
+ }
47
61
  }
48
62
 
49
63
  /**
50
64
  * Use i18n in Astro pages/components
51
65
  * Returns helper functions for translations
52
66
  */
53
- export function useI18n(astro: AstroGlobal) {
67
+ export function useI18n(astro: Astro) {
54
68
  const i18n = getI18n(astro)
55
69
  const locale = getLocale(astro)
56
70
  const defaultLocale = getDefaultLocale(astro)
57
71
  const locales = getLocales(astro)
58
- const localeCodes = locales.map((l) => l.code)
59
- const routingStrategy = getRoutingStrategy(astro)
72
+ const links = hrefContext(astro)
73
+ const { strategy, codes, base, path: currentPath } = links
74
+ const bare = (path: string) => withoutAppBaseURL(path, base)
60
75
 
61
76
  return {
62
77
  // Current locale
@@ -65,139 +80,81 @@ export function useI18n(astro: AstroGlobal) {
65
80
  locales,
66
81
 
67
82
  // Translation methods
68
- t: (key: TranslationKey, params?: Params, defaultValue?: string | null, routeName?: string): CleanTranslation => {
69
- return i18n.t(key, params, defaultValue, routeName)
70
- },
71
- ts: (key: TranslationKey, params?: Params, defaultValue?: string, routeName?: string): string => {
72
- return i18n.ts(key, params, defaultValue, routeName)
73
- },
74
- tc: (key: TranslationKey, count: number | Params, defaultValue?: string): string => {
75
- return i18n.tc(key, count, defaultValue)
76
- },
83
+ t: i18n.t.bind(i18n),
84
+ ts: i18n.ts.bind(i18n),
85
+ tc: i18n.tc.bind(i18n),
77
86
  tn: i18n.tn.bind(i18n),
78
87
  td: i18n.td.bind(i18n),
79
88
  tdr: i18n.tdr.bind(i18n),
80
- has: (key: TranslationKey, routeName?: string): boolean => {
81
- return i18n.has(key, routeName)
82
- },
89
+ has: i18n.has.bind(i18n),
83
90
 
84
91
  // Route management
85
- getRoute: (): string => {
86
- return i18n.getRoute()
87
- },
88
- getRouteName: (path?: string): string => {
89
- const targetPath = path || astro.url.pathname
90
- if (routingStrategy?.getRouteName) {
91
- return routingStrategy.getRouteName(targetPath, localeCodes)
92
- }
93
- // Fallback: basic route name extraction
94
- const cleanPath = targetPath.replace(/^\//, '').replace(/\/$/, '')
95
- if (!cleanPath) return 'index'
96
- const segments = cleanPath.split('/').filter(Boolean)
97
- const firstSegment = segments[0]
98
- if (firstSegment && localeCodes.includes(firstSegment)) {
99
- segments.shift()
100
- }
101
- return segments.length === 0 ? 'index' : segments.join('-')
102
- },
103
- getLocaleFromPath: (path?: string): string => {
104
- const targetPath = path || astro.url.pathname
105
- if (routingStrategy?.getLocaleFromPath) {
106
- return routingStrategy.getLocaleFromPath(targetPath, defaultLocale, localeCodes)
107
- }
108
- // Fallback: check first segment
109
- const segments = targetPath.split('/').filter(Boolean)
110
- const firstSegment = segments[0]
111
- return firstSegment && localeCodes.includes(firstSegment) ? firstSegment : defaultLocale
112
- },
113
-
114
- // Path utilities
115
- switchLocalePath: (newLocale: string): string => {
116
- if (routingStrategy?.switchLocalePath) {
117
- return routingStrategy.switchLocalePath(astro.url.pathname, newLocale, localeCodes, defaultLocale)
118
- }
119
- // Fallback: basic locale switching
120
- const segments = astro.url.pathname.split('/').filter(Boolean)
121
- const firstSegment = segments[0]
122
- if (firstSegment && localeCodes.includes(firstSegment)) {
123
- segments.shift()
124
- }
125
- if (newLocale !== defaultLocale) {
126
- segments.unshift(newLocale)
127
- }
128
- return `/${segments.join('/')}`
129
- },
130
- localizePath: (path: string, targetLocale?: string): string => {
131
- if (routingStrategy?.localizePath) {
132
- return routingStrategy.localizePath(path, targetLocale || locale, localeCodes, defaultLocale)
133
- }
134
- // Fallback: basic localization
135
- const cleanPath = path.replace(/^\//, '').replace(/\/$/, '') || ''
136
- const segments = cleanPath.split('/').filter(Boolean)
137
- const firstSegment = segments[0]
138
- if (firstSegment && localeCodes.includes(firstSegment)) {
139
- segments.shift()
140
- }
141
- if (targetLocale && targetLocale !== defaultLocale) {
142
- segments.unshift(targetLocale)
143
- }
144
- return `/${segments.join('/')}`
145
- },
92
+ getRoute: i18n.getRoute.bind(i18n),
93
+ getRouteName: (path?: string): string => (path === undefined ? i18n.getRoute() : strategy.getRouteName!(bare(path), codes)),
94
+ getLocaleFromPath: (path?: string): string => strategy.getLocaleFromPath!(path === undefined ? currentPath : bare(path), defaultLocale, codes),
95
+
96
+ // Path utilities: URLs of the site, `base` included
97
+ switchLocalePath: (newLocale: string): string => withBase(strategy.switchLocalePath!(currentPath, newLocale, codes, defaultLocale), base),
98
+ localizePath: (path: string, targetLocale?: string): string => localizeHref(path, targetLocale || locale, links),
146
99
 
147
100
  // Get i18n instance
148
- getI18n: (): AstroI18n => {
149
- return i18n
150
- },
151
-
152
- // Get base path without locale (for rewrite)
153
- getBasePath: (url?: URL): string => {
154
- const targetUrl = url || astro.url
155
- const pathname = targetUrl.pathname
156
- const segments = pathname.split('/').filter(Boolean)
157
-
158
- // Remove locale from path if present
159
- const firstSegment = segments[0]
160
- if (firstSegment && localeCodes.includes(firstSegment)) {
161
- segments.shift()
162
- }
163
-
164
- const basePath = segments.length > 0 ? `/${segments.join('/')}` : '/'
165
- return basePath
166
- },
101
+ getI18n: () => i18n,
102
+
103
+ // Path of a URL without locale and `base`: what `Astro.rewrite` takes
104
+ getBasePath: (url?: URL): string => strategy.removeLocaleFromPath!(url ? bare(url.pathname) : currentPath, codes),
167
105
 
168
106
  // Translation management
169
- addTranslations: (locale: string, translations: Record<string, unknown>, merge: boolean = true): void => {
170
- i18n.addTranslations(locale, translations, merge)
171
- },
172
- addRouteTranslations: (locale: string, routeName: string, translations: Record<string, unknown>, merge: boolean = true): void => {
173
- i18n.addRouteTranslations(locale, routeName, translations, merge)
174
- },
175
- mergeTranslations: (locale: string, routeName: string, translations: Record<string, unknown>): void => {
176
- i18n.mergeTranslations(locale, routeName, translations)
177
- },
178
- clearCache: (): void => {
179
- i18n.clearCache()
180
- },
107
+ addTranslations: i18n.addTranslations.bind(i18n),
108
+ addRouteTranslations: i18n.addRouteTranslations.bind(i18n),
109
+ mergeTranslations: i18n.mergeTranslations.bind(i18n),
110
+ clearCache: i18n.clearCache.bind(i18n),
181
111
  }
182
112
  }
183
113
 
114
+ /** What `<I18nSwitcher>` shows for a locale. A code is free-form: `constructor` must not read an inherited member. */
115
+ export function localeLabel(locale: Locale, customLabels: Record<string, string>): string {
116
+ return (ownValue(customLabels, locale.code) as string | undefined) || locale.displayName || locale.code
117
+ }
118
+
119
+ /** `href` of an `<I18nLink>`: relative to the page, in `locale` (default the current one). */
120
+ export function resolveLinkHref(astro: Astro, href: string, locale?: string): string {
121
+ const links = hrefContext(astro)
122
+ return localizeHref(href, locale || getLocale(astro), { ...links, from: links.path })
123
+ }
124
+
125
+ export interface I18nTProps extends ResolveTextOptions {
126
+ defaultValue?: string
127
+ }
128
+
129
+ /** The text of an `<I18nT>`, `null` when `hideIfEmpty` hides it. With `html`, params arrive escaped; the translation is trusted. */
130
+ export function resolveI18nT(astro: Astro, props: I18nTProps): string | null {
131
+ const i18n = getI18n(astro)
132
+ const { defaultValue } = props
133
+ // `t` with the default for `resolveText`, every other member from the instance
134
+ const host = defaultValue
135
+ ? (Object.create(i18n, {
136
+ t: { value: (key: string, params?: Params, fallback?: string | null) => i18n.t(key, params, fallback ?? defaultValue) },
137
+ }) as AstroI18n)
138
+ : i18n
139
+ const text = resolveText(host, props)
140
+ return text && interpolate(text.template, text.params)
141
+ }
142
+
184
143
  /**
185
144
  * Strip locale prefix from the current URL for `Astro.rewrite()` on `/[locale]/*` routes.
186
145
  */
187
- export function getLocaleRewritePath(astro: AstroGlobal): string {
188
- const url = astro.locals.currentUrl ?? astro.url
189
- return useI18n(astro).getBasePath(url)
146
+ export function getLocaleRewritePath(astro: Astro): string {
147
+ return useI18n(astro).getBasePath(astro.locals.currentUrl ?? astro.url)
190
148
  }
191
149
 
192
150
  /**
193
151
  * Validate `:locale`, sync i18n/locals, and return the base path for `Astro.rewrite()`.
194
152
  * Returns a `Response` when the locale param is invalid.
195
153
  */
196
- export function prepareLocaleRewrite(astro: AstroGlobal): Response | string {
154
+ export function prepareLocaleRewrite(astro: Astro & Pick<AstroGlobal, 'params'>): Response | string {
197
155
  const { locale } = astro.params
198
- const localeCodes = getLocales(astro).map((entry) => entry.code)
199
156
 
200
- if (!locale || !localeCodes.includes(locale)) {
157
+ if (!locale || !getEnabledLocaleCodes(getLocales(astro)).includes(locale)) {
201
158
  return new Response(null, { status: 404 })
202
159
  }
203
160
 
@@ -214,6 +171,7 @@ export function prepareLocaleRewrite(astro: AstroGlobal): Response | string {
214
171
  * Generate locale head meta tags for SEO
215
172
  */
216
173
  export interface LocaleHeadOptions {
174
+ /** Origin of the absolute URLs. Default: `site` from the Astro config, else root-relative URLs. */
217
175
  baseUrl?: string
218
176
  addDirAttribute?: boolean
219
177
  addSeoAttributes?: boolean
@@ -224,148 +182,29 @@ export interface LocaleHeadOptions {
224
182
  hreflangBaseLanguage?: boolean
225
183
  }
226
184
 
227
- export interface LocaleHeadResult {
228
- htmlAttrs: {
229
- lang?: string
230
- dir?: 'ltr' | 'rtl' | 'auto'
231
- }
232
- link: Array<{
233
- rel: string
234
- href: string
235
- hreflang?: string
236
- }>
237
- meta: Array<{
238
- property: string
239
- content: string
240
- }>
241
- }
242
-
243
- export function useLocaleHead(astro: AstroGlobal, options: LocaleHeadOptions = {}): LocaleHeadResult {
244
- const { baseUrl = '/', addDirAttribute = true, addSeoAttributes = true, hreflangBaseLanguage = false } = options
245
-
246
- const locale = getLocale(astro)
247
- const defaultLocale = getDefaultLocale(astro)
248
- const allLocales = getLocales(astro)
249
- const locales = allLocales.filter((l) => !l.disabled)
250
- const localesForSeo = locales.filter((l) => l.seo !== false)
251
- const currentLocaleObj = locales.find((l) => l.code === locale)
252
-
253
- if (!currentLocaleObj) {
254
- return { htmlAttrs: {}, link: [], meta: [] }
255
- }
256
-
257
- const currentIso = currentLocaleObj.iso || locale
258
- const currentOg = resolveOgLocale(currentLocaleObj)
259
- const currentDir = currentLocaleObj.dir || 'auto'
260
-
261
- const result: LocaleHeadResult = {
262
- htmlAttrs: {
263
- lang: currentIso,
264
- ...(addDirAttribute ? { dir: currentDir } : {}),
265
- },
266
- link: [],
267
- meta: [],
268
- }
269
-
270
- if (!addSeoAttributes) {
271
- return result
272
- }
273
-
274
- // Canonical URL (uses pathname only — query params are excluded)
275
- const canonicalUrl = `${baseUrl}${astro.url.pathname}`
276
- result.link.push({
277
- rel: 'canonical',
278
- href: canonicalUrl,
279
- })
280
-
281
- // Get routing strategy
282
- const routingStrategy = getRoutingStrategy(astro)
283
- const allLocaleCodes = locales.map((l) => l.code)
284
-
285
- // Alternate languages (includes current locale for self-referencing hreflang, per Google guidelines)
286
- const hrefByCode = new Map<string, string>()
287
- for (const loc of localesForSeo) {
288
- let alternatePath = astro.url.pathname
289
- if (routingStrategy?.switchLocalePath) {
290
- alternatePath = routingStrategy.switchLocalePath(astro.url.pathname, loc.code, allLocaleCodes, defaultLocale)
291
- } else {
292
- // Fallback: basic locale switching
293
- const segments = astro.url.pathname.split('/').filter(Boolean)
294
- const firstSegment = segments[0]
295
- if (firstSegment && allLocaleCodes.includes(firstSegment)) {
296
- segments.shift()
297
- }
298
- if (loc.code !== defaultLocale) {
299
- segments.unshift(loc.code)
300
- }
301
- alternatePath = `/${segments.join('/')}`
302
- }
303
- hrefByCode.set(String(loc.code), `${baseUrl}${alternatePath}`)
304
- }
185
+ export type LocaleHeadResult = LocaleHead
305
186
 
306
- for (const { hreflang, localeCode } of resolveHreflangAlternates(localesForSeo, { hreflangBaseLanguage })) {
307
- const alternateUrl = hrefByCode.get(localeCode)
308
- if (!alternateUrl) continue
309
- result.link.push({
310
- rel: 'alternate',
311
- href: alternateUrl,
312
- hreflang,
313
- })
314
- }
315
-
316
- // x-default hreflang — points to the default locale's URL.
317
- // Tells search engines which URL to show when none of the
318
- // specified languages match the user's browser settings.
319
- const defaultLocaleObj = locales.find((l) => l.code === defaultLocale)
320
- if (defaultLocaleObj?.seo !== false) {
321
- let xDefaultPath = astro.url.pathname
322
- if (routingStrategy?.switchLocalePath) {
323
- xDefaultPath = routingStrategy.switchLocalePath(astro.url.pathname, defaultLocale, allLocaleCodes, defaultLocale)
324
- } else {
325
- const segments = astro.url.pathname.split('/').filter(Boolean)
326
- const firstSegment = segments[0]
327
- if (firstSegment && allLocaleCodes.includes(firstSegment)) {
328
- segments.shift()
329
- }
330
- xDefaultPath = `/${segments.join('/')}`
331
- }
332
- result.link.push({
333
- rel: 'alternate',
334
- href: `${baseUrl}${xDefaultPath}`,
335
- hreflang: 'x-default',
336
- })
337
- }
187
+ export function useLocaleHead(astro: Astro, options: LocaleHeadOptions = {}): LocaleHeadResult {
188
+ const { addDirAttribute, addSeoAttributes, hreflangBaseLanguage } = options
189
+ const origin = options.baseUrl ?? astro.site?.origin ?? ''
190
+ const { base, defaultLocale, codes, strategy, path } = hrefContext(astro)
191
+ const code = getLocale(astro)
192
+ const enabled = getEnabledLocales(getLocales(astro))
193
+ const locale = enabled.find((entry) => entry.code === code)
194
+ if (!locale) return { htmlAttrs: {}, link: [], meta: [] }
338
195
 
339
- // Open Graph locale
340
- if (currentOg) {
341
- result.meta.push({
342
- property: 'og:locale',
343
- content: currentOg,
344
- })
345
- } else {
346
- warnUnresolvedOgLocale(currentLocaleObj, { tag: 'og:locale' })
347
- }
348
-
349
- result.meta.push({
350
- property: 'og:url',
351
- content: canonicalUrl,
196
+ const hrefOf = (target: string) => joinBase(origin, withBase(strategy.switchLocalePath!(path, target, codes, defaultLocale), base))
197
+ const defaultEntry = enabled.find((entry) => entry.code === defaultLocale)
198
+ return buildLocaleHead({
199
+ locale,
200
+ seoLocales: enabled.filter((entry) => entry.seo !== false),
201
+ hrefOf,
202
+ canonical: joinBase(origin, withBase(path, base)),
203
+ xDefault: defaultEntry && defaultEntry.seo !== false ? hrefOf(defaultLocale) : undefined,
204
+ addDirAttribute,
205
+ addSeoAttributes,
206
+ hreflangBaseLanguage,
352
207
  })
353
-
354
- // Alternate OG locales
355
- for (const loc of localesForSeo) {
356
- if (loc.code === locale) continue
357
- const ogAlt = resolveOgLocale(loc)
358
- if (!ogAlt) {
359
- warnUnresolvedOgLocale(loc, { tag: 'og:locale:alternate' })
360
- continue
361
- }
362
- result.meta.push({
363
- property: 'og:locale:alternate',
364
- content: ogAlt,
365
- })
366
- }
367
-
368
- return result
369
208
  }
370
209
 
371
210
  /**
@@ -373,77 +212,64 @@ export function useLocaleHead(astro: AstroGlobal, options: LocaleHeadOptions = {
373
212
  */
374
213
  export interface I18nClientProps {
375
214
  locale: string
215
+ /** The tag plurals go by: the locale's `iso` when it has one. */
216
+ pluralLocale?: string
217
+ /** Tags of the codes that have one, for `setLocale`. Absent when there are none. */
218
+ localeIso?: Record<string, string>
376
219
  fallbackLocale: string
377
220
  translations: Record<string, Translations> // routeName -> translations
378
221
  currentRoute: string
379
222
  }
380
223
 
381
- /**
382
- * Creates a nested structure from a key (e.g. 'islands.vue.title' -> { islands: { vue: { title: value } } })
383
- */
384
- function setNestedValue(obj: Translations, key: string, value: unknown): void {
385
- const parts = key.split('.')
386
- let current: Translations = obj
387
- for (let i = 0; i < parts.length - 1; i++) {
388
- const part = parts[i]!
389
- if (!current[part]) {
390
- current[part] = {}
391
- }
392
- current = current[part] as Translations
224
+ /** `keys` of `tree`, nested back into objects. Only the copy is written: a value may be a stored subtree. */
225
+ function pickKeys(tree: Translations, keys: string[]): Translations {
226
+ let picked: Translations = {}
227
+ for (const key of keys) {
228
+ const value = getByPath(tree, key)
229
+ if (value !== undefined && value !== null) picked = setTranslationAtKey(picked, key, value) as Translations
393
230
  }
394
- const last = parts[parts.length - 1]
395
- if (last !== undefined) {
396
- current[last] = value
231
+ return picked
232
+ }
233
+
234
+ /** Above this, a dictionary in every island's props is worth a word to the developer. */
235
+ const PROPS_WARN_SIZE = 20_000
236
+ const warned = new WeakSet<object>()
237
+
238
+ /** A cyclic tree has no JSON, and no size to warn about. */
239
+ function sizeOf(tree: Translations): number {
240
+ try {
241
+ return JSON.stringify(tree).length
242
+ } catch {
243
+ return 0
397
244
  }
398
245
  }
399
246
 
247
+ /** Once per dictionary: the merged tree is the same object until a write. */
248
+ function warnOversizedProps(tree: Translations): void {
249
+ if (isProduction() || warned.has(tree)) return
250
+ warned.add(tree)
251
+ const size = sizeOf(tree)
252
+ if (size > PROPS_WARN_SIZE)
253
+ console.warn(`[i18n-micro] getI18nProps() without keys puts ${size} bytes in every island's props: pass the keys the island uses.`)
254
+ }
255
+
400
256
  /**
401
- * Prepares props to pass to a client island.
402
- * Accepts a list of keys to pass to the island.
403
- * Uses i18n methods for proper routesLocaleLinks support.
404
- * currentRoute is already normalized via middleware (getRouteName), so we use it directly.
257
+ * Props for a client island: what `t()` can answer on this page (route, root and fallback keys),
258
+ * or only `keys` of that, which is the way to keep the page small. Stored trees are shared, an
259
+ * island must not write into them.
405
260
  */
406
- export function getI18nProps(astro: AstroGlobal, keys?: string[]): I18nClientProps {
261
+ export function getI18nProps(astro: Pick<AstroGlobal, 'locals'>, keys?: string[]): I18nClientProps {
407
262
  const i18n = getI18n(astro)
408
- const locale = getLocale(astro)
409
- const fallbackLocale = getDefaultLocale(astro)
410
- // currentRoute is already normalized via middleware.setRoute(getRouteName(...))
411
- // getRouteName takes routesLocaleLinks into account if configured
412
263
  const currentRoute = i18n.getRoute()
413
-
414
- const translations: Record<string, Translations> = {}
415
-
416
- // If keys are specified, extract only those from cache
417
- if (keys && keys.length > 0) {
418
- const extracted: Translations = {}
419
-
420
- // Use i18n methods to get translations
421
- // This guarantees proper routesLocaleLinks support
422
- for (const key of keys) {
423
- // Use i18n.t() which internally uses helper.getTranslation
424
- // and properly resolves the cache key considering routesLocaleLinks
425
- const value = i18n.t(key, undefined, undefined, currentRoute)
426
- if (value !== null && value !== undefined && value !== key) {
427
- setNestedValue(extracted, key, value)
428
- }
429
- }
430
-
431
- if (Object.keys(extracted).length > 0) {
432
- translations[currentRoute] = extracted
433
- }
434
- } else {
435
- // If no keys specified, get route-specific translations
436
- // Use the public getRouteTranslations method for safe access
437
- const routeTrans = i18n.getRouteTranslations(locale, currentRoute)
438
- if (routeTrans) {
439
- translations[currentRoute] = routeTrans
440
- }
441
- }
442
-
264
+ const all = i18n.resolveTranslations(currentRoute)
265
+ if (!keys?.length) warnOversizedProps(all)
266
+ const localeIso = i18n.getLocaleIso()
443
267
  return {
444
- locale,
445
- fallbackLocale,
268
+ locale: getLocale(astro),
269
+ pluralLocale: i18n.pluralLocale(),
270
+ ...(Object.keys(localeIso).length > 0 && { localeIso }),
271
+ fallbackLocale: i18n.getFallbackLocale(),
446
272
  currentRoute,
447
- translations,
273
+ translations: { [currentRoute]: keys && keys.length > 0 ? pickKeys(all, keys) : all },
448
274
  }
449
275
  }
@@ -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
+ }
File without changes
File without changes
@@ -1 +0,0 @@
1
- let e=require("@i18n-micro/core");function t(e,t){if(e===null||typeof t!=`string`)return null;let n=e;if(e[t])n=e[t];else{let e=t.toString().split(`.`);for(let t of e)if(n&&typeof n==`object`&&n&&t in n)n=n[t];else return null}return n??null}function n(n,r,i,a,o){if(!r)return a||r||``;let s=o||n.currentRoute,c=null;return n.translations[s]&&(c=t(n.translations[s],r)),c||=a===void 0?r:a||r,typeof c==`string`&&i?(0,e.interpolate)(c,i):c}function r(e,n,r){let i=r||e.currentRoute,a=e.translations[i];if(a){let e=t(a,n);if(e!==null&&typeof e!=`object`)return!0}return!1}Object.defineProperty(exports,"n",{enumerable:!0,get:function(){return n}}),Object.defineProperty(exports,"t",{enumerable:!0,get:function(){return r}});
@@ -1,28 +0,0 @@
1
- import { interpolate as e } from "@i18n-micro/core";
2
- //#region src/client/core.ts
3
- function t(e, t) {
4
- if (e === null || typeof t != "string") return null;
5
- let n = e;
6
- if (e[t]) n = e[t];
7
- else {
8
- let e = t.toString().split(".");
9
- for (let t of e) if (n && typeof n == "object" && n && t in n) n = n[t];
10
- else return null;
11
- }
12
- return n ?? null;
13
- }
14
- function n(n, r, i, a, o) {
15
- if (!r) return a || r || "";
16
- let s = o || n.currentRoute, c = null;
17
- return n.translations[s] && (c = t(n.translations[s], r)), c ||= a === void 0 ? r : a || r, typeof c == "string" && i ? e(c, i) : c;
18
- }
19
- function r(e, n, r) {
20
- let i = r || e.currentRoute, a = e.translations[i];
21
- if (a) {
22
- let e = t(a, n);
23
- if (e !== null && typeof e != "object") return !0;
24
- }
25
- return !1;
26
- }
27
- //#endregion
28
- export { n, r as t };