@i18n-micro/astro 1.3.24 → 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.
- package/LICENSE +1 -1
- package/README.md +8 -64
- package/dist/auto-middleware.d.ts +1 -0
- package/dist/client/core.d.cts +86 -0
- package/dist/client/core.d.ts +71 -8
- package/dist/client/index.d.cts +2 -6
- package/dist/client/index.d.ts +2 -6
- package/dist/client/index.js +1 -6
- package/dist/client/preact.d.ts +12 -23
- package/dist/client/preact.js +13 -37
- package/dist/client/react.d.ts +14 -21
- package/dist/client/react.js +12 -37
- package/dist/client/svelte.d.ts +14 -14
- package/dist/client/svelte.js +17 -47
- package/dist/client/vue.d.ts +15 -15
- package/dist/client/vue.js +37 -2
- package/dist/composer.d.cts +16 -0
- package/dist/composer.d.ts +10 -30
- package/dist/core-CgrgdmGy.cjs +1 -0
- package/dist/core-DTAcn2yF.js +72 -0
- package/dist/env.d.ts +8 -2
- package/dist/index.cjs +11 -3
- package/dist/index.d.cts +10 -13
- package/dist/index.d.ts +10 -13
- package/dist/index.mjs +171 -482
- package/dist/integration.d.cts +26 -0
- package/dist/integration.d.ts +14 -15
- package/dist/middleware-BT3OA5YL.cjs +1 -0
- package/dist/middleware-Ch8E0iV_.js +257 -0
- package/dist/middleware.d.cts +34 -0
- package/dist/middleware.d.ts +17 -8
- package/dist/middleware.js +23 -0
- package/dist/node.d.ts +26 -0
- package/dist/node.js +22 -0
- package/dist/request.d.ts +69 -0
- package/dist/router/adapter.d.cts +9 -0
- package/dist/router/adapter.d.ts +5 -4
- package/dist/router/types.d.cts +66 -0
- package/dist/router/types.d.ts +3 -2
- package/dist/utils.d.cts +112 -0
- package/dist/utils.d.ts +39 -35
- package/dist/virtual.d.ts +21 -0
- package/package.json +28 -15
- package/src/auto-middleware.ts +26 -0
- package/src/client/core.ts +124 -59
- package/src/client/index.ts +2 -11
- package/src/client/preact.tsx +12 -127
- package/src/client/react.tsx +11 -123
- package/src/client/svelte.ts +6 -69
- package/src/client/vue.ts +22 -77
- package/src/components/i18n-link.astro +2 -59
- package/src/components/i18n-switcher.astro +46 -149
- package/src/components/i18n-t.astro +6 -54
- package/src/composer.ts +25 -115
- package/src/env.d.ts +7 -0
- package/src/index.ts +1 -6
- package/src/integration.ts +113 -52
- package/src/middleware.ts +174 -120
- package/src/node.ts +51 -0
- package/src/request.ts +177 -0
- package/src/router/adapter.ts +47 -149
- package/src/router/types.ts +3 -2
- package/src/utils.ts +149 -323
- package/src/virtual.d.ts +21 -0
- package/dist/components/index.d.cts +0 -0
- package/dist/components/index.d.ts +0 -0
- package/dist/core-DwkpOV-H.cjs +0 -1
- package/dist/core-VSnkGRWR.js +0 -28
- package/dist/load-translations.d.ts +0 -43
- package/dist/routing-context.d.ts +0 -4
- package/dist/vue-BNiEwZr2.cjs +0 -5
- package/dist/vue-CWHjIOiS.js +0 -1724
- package/src/components/index.ts +0 -11
- package/src/load-translations.ts +0 -106
- 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 {
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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:
|
|
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
|
|
59
|
-
const
|
|
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: (
|
|
69
|
-
|
|
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: (
|
|
81
|
-
return i18n.has(key, routeName)
|
|
82
|
-
},
|
|
89
|
+
has: i18n.has.bind(i18n),
|
|
83
90
|
|
|
84
91
|
// Route management
|
|
85
|
-
getRoute: ()
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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: ()
|
|
149
|
-
|
|
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: (
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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:
|
|
188
|
-
|
|
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 || !
|
|
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
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
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
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
395
|
-
|
|
396
|
-
|
|
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
|
-
*
|
|
402
|
-
*
|
|
403
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/virtual.d.ts
ADDED
|
@@ -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
|
package/dist/core-DwkpOV-H.cjs
DELETED
|
@@ -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}});
|
package/dist/core-VSnkGRWR.js
DELETED
|
@@ -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 };
|