@turnipxenon/pineapple 4.4.2 → 4.5.0-alpha.2
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/dist/ui/elements/pinya-combobox/PinyaCombobox.svelte.d.ts +10 -3
- package/dist/ui/modules/projects/Hepcat.svelte.d.ts +1 -1
- package/dist/ui/modules/projects/Pengi.svelte.d.ts +1 -1
- package/dist/ui/modules/projects/Soulwork.svelte.d.ts +1 -1
- package/dist/ui/modules/projects/ThisWebpage.svelte.d.ts +1 -1
- package/package.json +24 -26
- package/dist/.gitignore +0 -1
- package/dist/paraglide/messages/_index.d.ts +0 -2
- package/dist/paraglide/messages/_index.js +0 -3
- package/dist/paraglide/messages/example_message.d.ts +0 -5
- package/dist/paraglide/messages/example_message.js +0 -39
- package/dist/paraglide/messages/settings.d.ts +0 -3
- package/dist/paraglide/messages/settings.js +0 -38
- package/dist/paraglide/messages.d.ts +0 -2
- package/dist/paraglide/messages.js +0 -4
- package/dist/paraglide/registry.d.ts +0 -21
- package/dist/paraglide/registry.js +0 -31
- package/dist/paraglide/runtime.d.ts +0 -400
- package/dist/paraglide/runtime.js +0 -1060
- package/dist/paraglide/server.d.ts +0 -64
- package/dist/paraglide/server.js +0 -161
- package/dist/server-temp/prisma.d.ts +0 -2
- package/dist/server-temp/prisma.js +0 -2
|
@@ -1,1060 +0,0 @@
|
|
|
1
|
-
// eslint-disable
|
|
2
|
-
|
|
3
|
-
/** @type {any} */
|
|
4
|
-
const URLPattern = {}
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* The project's base locale.
|
|
8
|
-
*
|
|
9
|
-
* @example
|
|
10
|
-
* if (locale === baseLocale) {
|
|
11
|
-
* // do something
|
|
12
|
-
* }
|
|
13
|
-
*/
|
|
14
|
-
export const baseLocale = "en";
|
|
15
|
-
/**
|
|
16
|
-
* The project's locales that have been specified in the settings.
|
|
17
|
-
*
|
|
18
|
-
* @example
|
|
19
|
-
* if (locales.includes(userSelectedLocale) === false) {
|
|
20
|
-
* throw new Error('Locale is not available');
|
|
21
|
-
* }
|
|
22
|
-
*/
|
|
23
|
-
export const locales = /** @type {const} */ (["en", "fr", "tl"]);
|
|
24
|
-
/** @type {string} */
|
|
25
|
-
export const cookieName = "PARAGLIDE_LOCALE";
|
|
26
|
-
/** @type {string} */
|
|
27
|
-
export const localStorageKey = "PARAGLIDE_LOCALE";
|
|
28
|
-
/**
|
|
29
|
-
* @type {Array<"cookie" | "baseLocale" | "globalVariable" | "url" | "preferredLanguage" | "localStorage">}
|
|
30
|
-
*/
|
|
31
|
-
export const strategy = [
|
|
32
|
-
"url",
|
|
33
|
-
"cookie",
|
|
34
|
-
"baseLocale"
|
|
35
|
-
];
|
|
36
|
-
/**
|
|
37
|
-
* The used URL patterns.
|
|
38
|
-
*
|
|
39
|
-
* @type {Array<{ pattern: string, localized: Array<[Locale, string]> }> }
|
|
40
|
-
*/
|
|
41
|
-
export const urlPatterns = [
|
|
42
|
-
{
|
|
43
|
-
"pattern": ":protocol://:domain(.*)::port?/:path(.*)?",
|
|
44
|
-
"localized": [
|
|
45
|
-
[
|
|
46
|
-
"fr",
|
|
47
|
-
":protocol://:domain(.*)::port?/fr/:path(.*)?"
|
|
48
|
-
],
|
|
49
|
-
[
|
|
50
|
-
"tl",
|
|
51
|
-
":protocol://:domain(.*)::port?/tl/:path(.*)?"
|
|
52
|
-
],
|
|
53
|
-
[
|
|
54
|
-
"en",
|
|
55
|
-
":protocol://:domain(.*)::port?/:path(.*)?"
|
|
56
|
-
]
|
|
57
|
-
]
|
|
58
|
-
}
|
|
59
|
-
];
|
|
60
|
-
/**
|
|
61
|
-
* @typedef {{
|
|
62
|
-
* getStore(): {
|
|
63
|
-
* locale?: Locale,
|
|
64
|
-
* origin?: string,
|
|
65
|
-
* messageCalls?: Set<string>
|
|
66
|
-
* } | undefined,
|
|
67
|
-
* run: (store: { locale?: Locale, origin?: string, messageCalls?: Set<string>},
|
|
68
|
-
* cb: any) => any
|
|
69
|
-
* }} ParaglideAsyncLocalStorage
|
|
70
|
-
*/
|
|
71
|
-
/**
|
|
72
|
-
* Server side async local storage that is set by `serverMiddleware()`.
|
|
73
|
-
*
|
|
74
|
-
* The variable is used to retrieve the locale and origin in a server-side
|
|
75
|
-
* rendering context without effecting other requests.
|
|
76
|
-
*
|
|
77
|
-
* @type {ParaglideAsyncLocalStorage | undefined}
|
|
78
|
-
*/
|
|
79
|
-
export let serverAsyncLocalStorage = undefined;
|
|
80
|
-
export const disableAsyncLocalStorage = false;
|
|
81
|
-
export const experimentalMiddlewareLocaleSplitting = false;
|
|
82
|
-
export const isServer = import.meta.env?.SSR ?? typeof window === 'undefined';
|
|
83
|
-
/**
|
|
84
|
-
* Sets the server side async local storage.
|
|
85
|
-
*
|
|
86
|
-
* The function is needed because the `runtime.js` file
|
|
87
|
-
* must define the `serverAsyncLocalStorage` variable to
|
|
88
|
-
* avoid a circular import between `runtime.js` and
|
|
89
|
-
* `server.js` files.
|
|
90
|
-
*
|
|
91
|
-
* @param {ParaglideAsyncLocalStorage | undefined} value
|
|
92
|
-
*/
|
|
93
|
-
export function overwriteServerAsyncLocalStorage(value) {
|
|
94
|
-
serverAsyncLocalStorage = value;
|
|
95
|
-
}
|
|
96
|
-
const TREE_SHAKE_COOKIE_STRATEGY_USED = true;
|
|
97
|
-
const TREE_SHAKE_URL_STRATEGY_USED = true;
|
|
98
|
-
const TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED = false;
|
|
99
|
-
const TREE_SHAKE_PREFERRED_LANGUAGE_STRATEGY_USED = false;
|
|
100
|
-
const TREE_SHAKE_DEFAULT_URL_PATTERN_USED = true;
|
|
101
|
-
const TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED = false;
|
|
102
|
-
|
|
103
|
-
globalThis.__paraglide = {}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* This is a fallback to get started with a custom
|
|
107
|
-
* strategy and avoid type errors.
|
|
108
|
-
*
|
|
109
|
-
* The implementation is overwritten
|
|
110
|
-
* by \`overwriteGetLocale()\` and \`defineSetLocale()\`.
|
|
111
|
-
*
|
|
112
|
-
* @type {Locale|undefined}
|
|
113
|
-
*/
|
|
114
|
-
let _locale;
|
|
115
|
-
let localeInitiallySet = false;
|
|
116
|
-
/**
|
|
117
|
-
* Get the current locale.
|
|
118
|
-
*
|
|
119
|
-
* @example
|
|
120
|
-
* if (getLocale() === 'de') {
|
|
121
|
-
* console.log('Germany 🇩🇪');
|
|
122
|
-
* } else if (getLocale() === 'nl') {
|
|
123
|
-
* console.log('Netherlands 🇳🇱');
|
|
124
|
-
* }
|
|
125
|
-
*
|
|
126
|
-
* @type {() => Locale}
|
|
127
|
-
*/
|
|
128
|
-
export let getLocale = () => {
|
|
129
|
-
/** @type {string | undefined} */
|
|
130
|
-
let locale;
|
|
131
|
-
// if running in a server-side rendering context
|
|
132
|
-
// retrieve the locale from the async local storage
|
|
133
|
-
if (serverAsyncLocalStorage) {
|
|
134
|
-
const locale = serverAsyncLocalStorage?.getStore()?.locale;
|
|
135
|
-
if (locale) {
|
|
136
|
-
return locale;
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
for (const strat of strategy) {
|
|
140
|
-
if (TREE_SHAKE_COOKIE_STRATEGY_USED && strat === "cookie") {
|
|
141
|
-
locale = extractLocaleFromCookie();
|
|
142
|
-
}
|
|
143
|
-
else if (strat === "baseLocale") {
|
|
144
|
-
locale = baseLocale;
|
|
145
|
-
}
|
|
146
|
-
else if (TREE_SHAKE_URL_STRATEGY_USED && strat === "url" && !isServer) {
|
|
147
|
-
locale = extractLocaleFromUrl(window.location.href);
|
|
148
|
-
}
|
|
149
|
-
else if (TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED &&
|
|
150
|
-
strat === "globalVariable" &&
|
|
151
|
-
_locale !== undefined) {
|
|
152
|
-
locale = _locale;
|
|
153
|
-
}
|
|
154
|
-
else if (TREE_SHAKE_PREFERRED_LANGUAGE_STRATEGY_USED &&
|
|
155
|
-
strat === "preferredLanguage" &&
|
|
156
|
-
!isServer) {
|
|
157
|
-
locale = negotiatePreferredLanguageFromNavigator();
|
|
158
|
-
}
|
|
159
|
-
else if (TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED &&
|
|
160
|
-
strat === "localStorage" &&
|
|
161
|
-
!isServer) {
|
|
162
|
-
locale = localStorage.getItem(localStorageKey) ?? undefined;
|
|
163
|
-
}
|
|
164
|
-
// check if match, else continue loop
|
|
165
|
-
if (locale !== undefined) {
|
|
166
|
-
const asserted = assertIsLocale(locale);
|
|
167
|
-
if (!localeInitiallySet) {
|
|
168
|
-
_locale = asserted;
|
|
169
|
-
// https://github.com/opral/inlang-paraglide-js/issues/455
|
|
170
|
-
localeInitiallySet = true;
|
|
171
|
-
setLocale(asserted, { reload: false });
|
|
172
|
-
}
|
|
173
|
-
return asserted;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
throw new Error("No locale found. Read the docs https://inlang.com/m/gerre34r/library-inlang-paraglideJs/errors#no-locale-found");
|
|
177
|
-
};
|
|
178
|
-
/**
|
|
179
|
-
* Negotiates a preferred language from navigator.languages.
|
|
180
|
-
*
|
|
181
|
-
* @returns {string|undefined} The negotiated preferred language.
|
|
182
|
-
*/
|
|
183
|
-
function negotiatePreferredLanguageFromNavigator() {
|
|
184
|
-
if (!navigator?.languages?.length) {
|
|
185
|
-
return undefined;
|
|
186
|
-
}
|
|
187
|
-
const languages = navigator.languages.map((lang) => ({
|
|
188
|
-
fullTag: lang.toLowerCase(),
|
|
189
|
-
baseTag: lang.split("-")[0]?.toLowerCase(),
|
|
190
|
-
}));
|
|
191
|
-
for (const lang of languages) {
|
|
192
|
-
if (isLocale(lang.fullTag)) {
|
|
193
|
-
return lang.fullTag;
|
|
194
|
-
}
|
|
195
|
-
else if (isLocale(lang.baseTag)) {
|
|
196
|
-
return lang.baseTag;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return undefined;
|
|
200
|
-
}
|
|
201
|
-
/**
|
|
202
|
-
* Overwrite the \`getLocale()\` function.
|
|
203
|
-
*
|
|
204
|
-
* Use this function to overwrite how the locale is resolved. For example,
|
|
205
|
-
* you can resolve the locale from the browser's preferred language,
|
|
206
|
-
* a cookie, env variable, or a user's preference.
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* overwriteGetLocale(() => {
|
|
210
|
-
* // resolve the locale from a cookie. fallback to the base locale.
|
|
211
|
-
* return Cookies.get('locale') ?? baseLocale
|
|
212
|
-
* }
|
|
213
|
-
*
|
|
214
|
-
* @type {(fn: () => Locale) => void}
|
|
215
|
-
*/
|
|
216
|
-
export const overwriteGetLocale = (fn) => {
|
|
217
|
-
getLocale = fn;
|
|
218
|
-
};
|
|
219
|
-
|
|
220
|
-
/**
|
|
221
|
-
* Set the locale.
|
|
222
|
-
*
|
|
223
|
-
* Set locale reloads the site by default on the client. Reloading
|
|
224
|
-
* can be disabled by passing \`reload: false\` as an option. If
|
|
225
|
-
* reloading is disbaled, you need to ensure that the UI is updated
|
|
226
|
-
* to reflect the new locale.
|
|
227
|
-
*
|
|
228
|
-
* @example
|
|
229
|
-
* setLocale('en');
|
|
230
|
-
*
|
|
231
|
-
* @example
|
|
232
|
-
* setLocale('en', { reload: false });
|
|
233
|
-
*
|
|
234
|
-
* @type {(newLocale: Locale, options?: { reload?: boolean }) => void}
|
|
235
|
-
*/
|
|
236
|
-
export let setLocale = (newLocale, options) => {
|
|
237
|
-
const optionsWithDefaults = {
|
|
238
|
-
reload: true,
|
|
239
|
-
...options,
|
|
240
|
-
};
|
|
241
|
-
// locale is already set
|
|
242
|
-
// https://github.com/opral/inlang-paraglide-js/issues/430
|
|
243
|
-
let currentLocale;
|
|
244
|
-
try {
|
|
245
|
-
currentLocale = getLocale();
|
|
246
|
-
}
|
|
247
|
-
catch {
|
|
248
|
-
// do nothing, no locale has been set yet.
|
|
249
|
-
}
|
|
250
|
-
/** @type {string | undefined} */
|
|
251
|
-
let newLocation = undefined;
|
|
252
|
-
for (const strat of strategy) {
|
|
253
|
-
if (TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED &&
|
|
254
|
-
strat === "globalVariable") {
|
|
255
|
-
// a default for a custom strategy to get started quickly
|
|
256
|
-
// is likely overwritten by `defineSetLocale()`
|
|
257
|
-
_locale = newLocale;
|
|
258
|
-
}
|
|
259
|
-
else if (TREE_SHAKE_COOKIE_STRATEGY_USED && strat === "cookie") {
|
|
260
|
-
if (isServer) {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
// set the cookie
|
|
264
|
-
document.cookie = `${cookieName}=${newLocale}; path=/`;
|
|
265
|
-
}
|
|
266
|
-
else if (strat === "baseLocale") {
|
|
267
|
-
// nothing to be set here. baseLocale is only a fallback
|
|
268
|
-
continue;
|
|
269
|
-
}
|
|
270
|
-
else if (TREE_SHAKE_URL_STRATEGY_USED &&
|
|
271
|
-
strat === "url" &&
|
|
272
|
-
typeof window !== "undefined") {
|
|
273
|
-
// route to the new url
|
|
274
|
-
//
|
|
275
|
-
// this triggers a page reload but a user rarely
|
|
276
|
-
// switches locales, so this should be fine.
|
|
277
|
-
//
|
|
278
|
-
// if the behavior is not desired, the implementation
|
|
279
|
-
// can be overwritten by `defineSetLocale()` to avoid
|
|
280
|
-
// a full page reload.
|
|
281
|
-
newLocation = localizeUrl(window.location.href, {
|
|
282
|
-
locale: newLocale,
|
|
283
|
-
}).href;
|
|
284
|
-
}
|
|
285
|
-
else if (TREE_SHAKE_LOCAL_STORAGE_STRATEGY_USED &&
|
|
286
|
-
strat === "localStorage" &&
|
|
287
|
-
typeof window !== "undefined") {
|
|
288
|
-
// set the localStorage
|
|
289
|
-
localStorage.setItem(localStorageKey, newLocale);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
if (!isServer &&
|
|
293
|
-
optionsWithDefaults.reload &&
|
|
294
|
-
window.location &&
|
|
295
|
-
newLocale !== currentLocale) {
|
|
296
|
-
if (newLocation) {
|
|
297
|
-
// reload the page by navigating to the new url
|
|
298
|
-
window.location.href = newLocation;
|
|
299
|
-
}
|
|
300
|
-
else {
|
|
301
|
-
// reload the page to reflect the new locale
|
|
302
|
-
window.location.reload();
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
return;
|
|
306
|
-
};
|
|
307
|
-
/**
|
|
308
|
-
* Overwrite the \`setLocale()\` function.
|
|
309
|
-
*
|
|
310
|
-
* Use this function to overwrite how the locale is set. For example,
|
|
311
|
-
* modify a cookie, env variable, or a user's preference.
|
|
312
|
-
*
|
|
313
|
-
* @example
|
|
314
|
-
* overwriteSetLocale((newLocale) => {
|
|
315
|
-
* // set the locale in a cookie
|
|
316
|
-
* return Cookies.set('locale', newLocale)
|
|
317
|
-
* });
|
|
318
|
-
*
|
|
319
|
-
* @param {(newLocale: Locale) => void} fn
|
|
320
|
-
*/
|
|
321
|
-
export const overwriteSetLocale = (fn) => {
|
|
322
|
-
setLocale = fn;
|
|
323
|
-
};
|
|
324
|
-
|
|
325
|
-
/**
|
|
326
|
-
* The origin of the current URL.
|
|
327
|
-
*
|
|
328
|
-
* Defaults to "http://y.com" in non-browser environments. If this
|
|
329
|
-
* behavior is not desired, the implementation can be overwritten
|
|
330
|
-
* by `overwriteGetUrlOrigin()`.
|
|
331
|
-
*
|
|
332
|
-
* @type {() => string}
|
|
333
|
-
*/
|
|
334
|
-
export let getUrlOrigin = () => {
|
|
335
|
-
if (serverAsyncLocalStorage) {
|
|
336
|
-
return serverAsyncLocalStorage.getStore()?.origin ?? "http://fallback.com";
|
|
337
|
-
}
|
|
338
|
-
else if (typeof window !== "undefined") {
|
|
339
|
-
return window.location.origin;
|
|
340
|
-
}
|
|
341
|
-
return "http://fallback.com";
|
|
342
|
-
};
|
|
343
|
-
/**
|
|
344
|
-
* Overwrite the getUrlOrigin function.
|
|
345
|
-
*
|
|
346
|
-
* Use this function in server environments to
|
|
347
|
-
* define how the URL origin is resolved.
|
|
348
|
-
*
|
|
349
|
-
* @type {(fn: () => string) => void}
|
|
350
|
-
*/
|
|
351
|
-
export let overwriteGetUrlOrigin = (fn) => {
|
|
352
|
-
getUrlOrigin = fn;
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Check if something is an available locale.
|
|
357
|
-
*
|
|
358
|
-
* @example
|
|
359
|
-
* if (isLocale(params.locale)) {
|
|
360
|
-
* setLocale(params.locale);
|
|
361
|
-
* } else {
|
|
362
|
-
* setLocale('en');
|
|
363
|
-
* }
|
|
364
|
-
*
|
|
365
|
-
* @param {any} locale
|
|
366
|
-
* @returns {locale is Locale}
|
|
367
|
-
*/
|
|
368
|
-
export function isLocale(locale) {
|
|
369
|
-
return !locale ? false : locales.includes(locale);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
/**
|
|
373
|
-
* Asserts that the input is a locale.
|
|
374
|
-
*
|
|
375
|
-
* @param {any} input - The input to check.
|
|
376
|
-
* @returns {Locale} The input if it is a locale.
|
|
377
|
-
* @throws {Error} If the input is not a locale.
|
|
378
|
-
*/
|
|
379
|
-
export function assertIsLocale(input) {
|
|
380
|
-
if (isLocale(input) === false) {
|
|
381
|
-
throw new Error(`Invalid locale: ${input}. Expected one of: ${locales.join(", ")}`);
|
|
382
|
-
}
|
|
383
|
-
return input;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
/**
|
|
387
|
-
* Extracts a locale from a request.
|
|
388
|
-
*
|
|
389
|
-
* Use the function on the server to extract the locale
|
|
390
|
-
* from a request.
|
|
391
|
-
*
|
|
392
|
-
* The function goes through the strategies in the order
|
|
393
|
-
* they are defined. If a strategy returns an invalid locale,
|
|
394
|
-
* it will fall back to the next strategy.
|
|
395
|
-
*
|
|
396
|
-
* @example
|
|
397
|
-
* const locale = extractLocaleFromRequest(request);
|
|
398
|
-
*
|
|
399
|
-
* @type {(request: Request) => Locale}
|
|
400
|
-
*/
|
|
401
|
-
export const extractLocaleFromRequest = (request) => {
|
|
402
|
-
/** @type {string|undefined} */
|
|
403
|
-
let locale;
|
|
404
|
-
for (const strat of strategy) {
|
|
405
|
-
if (TREE_SHAKE_COOKIE_STRATEGY_USED && strat === "cookie") {
|
|
406
|
-
locale = request.headers
|
|
407
|
-
.get("cookie")
|
|
408
|
-
?.split("; ")
|
|
409
|
-
.find((c) => c.startsWith(cookieName + "="))
|
|
410
|
-
?.split("=")[1];
|
|
411
|
-
}
|
|
412
|
-
else if (TREE_SHAKE_URL_STRATEGY_USED &&
|
|
413
|
-
strat === "url" &&
|
|
414
|
-
// only process url strategy if request is a document
|
|
415
|
-
// else it's api requests, etc.
|
|
416
|
-
request.headers.get("Sec-Fetch-Dest") === "document") {
|
|
417
|
-
locale = extractLocaleFromUrl(request.url);
|
|
418
|
-
}
|
|
419
|
-
else if (TREE_SHAKE_PREFERRED_LANGUAGE_STRATEGY_USED &&
|
|
420
|
-
strat === "preferredLanguage") {
|
|
421
|
-
const acceptLanguageHeader = request.headers.get("accept-language");
|
|
422
|
-
if (acceptLanguageHeader) {
|
|
423
|
-
locale = negotiatePreferredLanguageFromHeader(acceptLanguageHeader);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
else if (strat === "globalVariable") {
|
|
427
|
-
locale = _locale;
|
|
428
|
-
}
|
|
429
|
-
else if (strat === "baseLocale") {
|
|
430
|
-
return baseLocale;
|
|
431
|
-
}
|
|
432
|
-
else if (strat === "localStorage") {
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
if (locale !== undefined) {
|
|
436
|
-
if (!isLocale(locale)) {
|
|
437
|
-
locale = undefined;
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
return assertIsLocale(locale);
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
}
|
|
444
|
-
throw new Error("No locale found. There is an error in your strategy. Try adding 'baseLocale' as the very last strategy. Read more here https://inlang.com/m/gerre34r/library-inlang-paraglideJs/errors#no-locale-found");
|
|
445
|
-
};
|
|
446
|
-
/**
|
|
447
|
-
* Negotiates a preferred language from a header.
|
|
448
|
-
*
|
|
449
|
-
* @param {string} header - The header to negotiate from.
|
|
450
|
-
* @returns {string|undefined} The negotiated preferred language.
|
|
451
|
-
*/
|
|
452
|
-
function negotiatePreferredLanguageFromHeader(header) {
|
|
453
|
-
// Parse language preferences with their q-values and base language codes
|
|
454
|
-
const languages = header
|
|
455
|
-
.split(",")
|
|
456
|
-
.map((lang) => {
|
|
457
|
-
const [tag, q = "1"] = lang.trim().split(";q=");
|
|
458
|
-
// Get both the full tag and base language code
|
|
459
|
-
const baseTag = tag?.split("-")[0]?.toLowerCase();
|
|
460
|
-
return {
|
|
461
|
-
fullTag: tag?.toLowerCase(),
|
|
462
|
-
baseTag,
|
|
463
|
-
q: Number(q),
|
|
464
|
-
};
|
|
465
|
-
})
|
|
466
|
-
.sort((a, b) => b.q - a.q);
|
|
467
|
-
for (const lang of languages) {
|
|
468
|
-
if (isLocale(lang.fullTag)) {
|
|
469
|
-
return lang.fullTag;
|
|
470
|
-
}
|
|
471
|
-
else if (isLocale(lang.baseTag)) {
|
|
472
|
-
return lang.baseTag;
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
return undefined;
|
|
476
|
-
}
|
|
477
|
-
|
|
478
|
-
/**
|
|
479
|
-
* Extracts a cookie from the document.
|
|
480
|
-
*
|
|
481
|
-
* Will return undefined if the docuement is not available or if the cookie is not set.
|
|
482
|
-
* The `document` object is not available in server-side rendering, so this function should not be called in that context.
|
|
483
|
-
*
|
|
484
|
-
* @returns {string | undefined}
|
|
485
|
-
*/
|
|
486
|
-
export function extractLocaleFromCookie() {
|
|
487
|
-
if (typeof document === "undefined" || !document.cookie) {
|
|
488
|
-
return;
|
|
489
|
-
}
|
|
490
|
-
const match = document.cookie.match(new RegExp(`(^| )${cookieName}=([^;]+)`));
|
|
491
|
-
const locale = match?.[2];
|
|
492
|
-
if (isLocale(locale)) {
|
|
493
|
-
return locale;
|
|
494
|
-
}
|
|
495
|
-
return undefined;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
/**
|
|
499
|
-
* Extracts the locale from a given URL using native URLPattern.
|
|
500
|
-
*
|
|
501
|
-
* @param {URL|string} url - The full URL from which to extract the locale.
|
|
502
|
-
* @returns {Locale|undefined} The extracted locale, or undefined if no locale is found.
|
|
503
|
-
*/
|
|
504
|
-
export function extractLocaleFromUrl(url) {
|
|
505
|
-
if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
|
|
506
|
-
return defaultUrlPatternExtractLocale(url);
|
|
507
|
-
}
|
|
508
|
-
const urlObj = typeof url === "string" ? new URL(url) : url;
|
|
509
|
-
// Iterate over URL patterns
|
|
510
|
-
for (const element of urlPatterns) {
|
|
511
|
-
for (const [locale, localizedPattern] of element.localized) {
|
|
512
|
-
const match = new URLPattern(localizedPattern, urlObj.href).exec(urlObj.href);
|
|
513
|
-
if (!match) {
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
// Check if the locale is valid
|
|
517
|
-
if (assertIsLocale(locale)) {
|
|
518
|
-
return locale;
|
|
519
|
-
}
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
return undefined;
|
|
523
|
-
}
|
|
524
|
-
/**
|
|
525
|
-
* https://github.com/opral/inlang-paraglide-js/issues/381
|
|
526
|
-
*
|
|
527
|
-
* @param {URL|string} url - The full URL from which to extract the locale.
|
|
528
|
-
* @returns {Locale|undefined} The extracted locale, or undefined if no locale is found.
|
|
529
|
-
*/
|
|
530
|
-
function defaultUrlPatternExtractLocale(url) {
|
|
531
|
-
const urlObj = new URL(url, "http://dummy.com");
|
|
532
|
-
const pathSegments = urlObj.pathname.split("/").filter(Boolean);
|
|
533
|
-
if (pathSegments.length > 0) {
|
|
534
|
-
const potentialLocale = pathSegments[0];
|
|
535
|
-
if (isLocale(potentialLocale)) {
|
|
536
|
-
return potentialLocale;
|
|
537
|
-
}
|
|
538
|
-
}
|
|
539
|
-
// everything else has to be the base locale
|
|
540
|
-
return baseLocale;
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
/**
|
|
544
|
-
* Lower-level URL localization function, primarily used in server contexts.
|
|
545
|
-
*
|
|
546
|
-
* This function is designed for server-side usage where you need precise control
|
|
547
|
-
* over URL localization, such as in middleware or request handlers. It works with
|
|
548
|
-
* URL objects and always returns absolute URLs.
|
|
549
|
-
*
|
|
550
|
-
* For client-side UI components, use `localizeHref()` instead, which provides
|
|
551
|
-
* a more convenient API with relative paths and automatic locale detection.
|
|
552
|
-
*
|
|
553
|
-
* @example
|
|
554
|
-
* ```typescript
|
|
555
|
-
* // Server middleware example
|
|
556
|
-
* app.use((req, res, next) => {
|
|
557
|
-
* const url = new URL(req.url, `${req.protocol}://${req.headers.host}`);
|
|
558
|
-
* const localized = localizeUrl(url, { locale: "de" });
|
|
559
|
-
*
|
|
560
|
-
* if (localized.href !== url.href) {
|
|
561
|
-
* return res.redirect(localized.href);
|
|
562
|
-
* }
|
|
563
|
-
* next();
|
|
564
|
-
* });
|
|
565
|
-
* ```
|
|
566
|
-
*
|
|
567
|
-
* @example
|
|
568
|
-
* ```typescript
|
|
569
|
-
* // Using with URL patterns
|
|
570
|
-
* const url = new URL("https://example.com/about");
|
|
571
|
-
* localizeUrl(url, { locale: "de" });
|
|
572
|
-
* // => URL("https://example.com/de/about")
|
|
573
|
-
*
|
|
574
|
-
* // Using with domain-based localization
|
|
575
|
-
* const url = new URL("https://example.com/store");
|
|
576
|
-
* localizeUrl(url, { locale: "de" });
|
|
577
|
-
* // => URL("https://de.example.com/store")
|
|
578
|
-
* ```
|
|
579
|
-
*
|
|
580
|
-
* @param {string | URL} url - The URL to localize. If string, must be absolute.
|
|
581
|
-
* @param {Object} [options] - Options for localization
|
|
582
|
-
* @param {string} [options.locale] - Target locale. If not provided, uses getLocale()
|
|
583
|
-
* @returns {URL} The localized URL, always absolute
|
|
584
|
-
*/
|
|
585
|
-
export function localizeUrl(url, options) {
|
|
586
|
-
if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
|
|
587
|
-
return localizeUrlDefaultPattern(url, options);
|
|
588
|
-
}
|
|
589
|
-
const targetLocale = options?.locale ?? getLocale();
|
|
590
|
-
const urlObj = typeof url === "string" ? new URL(url) : url;
|
|
591
|
-
// Iterate over URL patterns
|
|
592
|
-
for (const element of urlPatterns) {
|
|
593
|
-
// match localized patterns
|
|
594
|
-
for (const [, localizedPattern] of element.localized) {
|
|
595
|
-
const match = new URLPattern(localizedPattern, urlObj.href).exec(urlObj.href);
|
|
596
|
-
if (!match) {
|
|
597
|
-
continue;
|
|
598
|
-
}
|
|
599
|
-
const targetPattern = element.localized.find(([locale]) => locale === targetLocale)?.[1];
|
|
600
|
-
if (!targetPattern) {
|
|
601
|
-
continue;
|
|
602
|
-
}
|
|
603
|
-
const localizedUrl = fillPattern(targetPattern, aggregateGroups(match), urlObj.origin);
|
|
604
|
-
return fillMissingUrlParts(localizedUrl, match);
|
|
605
|
-
}
|
|
606
|
-
const unlocalizedMatch = new URLPattern(element.pattern, urlObj.href).exec(urlObj.href);
|
|
607
|
-
if (unlocalizedMatch) {
|
|
608
|
-
const targetPattern = element.localized.find(([locale]) => locale === targetLocale)?.[1];
|
|
609
|
-
if (targetPattern) {
|
|
610
|
-
const localizedUrl = fillPattern(targetPattern, aggregateGroups(unlocalizedMatch), urlObj.origin);
|
|
611
|
-
return fillMissingUrlParts(localizedUrl, unlocalizedMatch);
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
}
|
|
615
|
-
// If no match found, return the original URL
|
|
616
|
-
return urlObj;
|
|
617
|
-
}
|
|
618
|
-
/**
|
|
619
|
-
* https://github.com/opral/inlang-paraglide-js/issues/381
|
|
620
|
-
*
|
|
621
|
-
* @param {string | URL} url
|
|
622
|
-
* @param {Object} [options]
|
|
623
|
-
* @param {string} [options.locale]
|
|
624
|
-
* @returns {URL}
|
|
625
|
-
*/
|
|
626
|
-
function localizeUrlDefaultPattern(url, options) {
|
|
627
|
-
const urlObj = typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url);
|
|
628
|
-
const locale = options?.locale ?? getLocale();
|
|
629
|
-
const currentLocale = extractLocaleFromUrl(urlObj);
|
|
630
|
-
// If current locale matches target locale, no change needed
|
|
631
|
-
if (currentLocale === locale) {
|
|
632
|
-
return urlObj;
|
|
633
|
-
}
|
|
634
|
-
const pathSegments = urlObj.pathname.split("/").filter(Boolean);
|
|
635
|
-
// If current path starts with a locale, remove it
|
|
636
|
-
if (pathSegments.length > 0 && isLocale(pathSegments[0])) {
|
|
637
|
-
pathSegments.shift();
|
|
638
|
-
}
|
|
639
|
-
// For base locale, don't add prefix
|
|
640
|
-
if (locale === baseLocale) {
|
|
641
|
-
urlObj.pathname = "/" + pathSegments.join("/");
|
|
642
|
-
}
|
|
643
|
-
else {
|
|
644
|
-
// For other locales, add prefix
|
|
645
|
-
urlObj.pathname = "/" + locale + "/" + pathSegments.join("/");
|
|
646
|
-
}
|
|
647
|
-
return urlObj;
|
|
648
|
-
}
|
|
649
|
-
/**
|
|
650
|
-
* Low-level URL de-localization function, primarily used in server contexts.
|
|
651
|
-
*
|
|
652
|
-
* This function is designed for server-side usage where you need precise control
|
|
653
|
-
* over URL de-localization, such as in middleware or request handlers. It works with
|
|
654
|
-
* URL objects and always returns absolute URLs.
|
|
655
|
-
*
|
|
656
|
-
* For client-side UI components, use `deLocalizeHref()` instead, which provides
|
|
657
|
-
* a more convenient API with relative paths.
|
|
658
|
-
*
|
|
659
|
-
* @example
|
|
660
|
-
* ```typescript
|
|
661
|
-
* // Server middleware example
|
|
662
|
-
* app.use((req, res, next) => {
|
|
663
|
-
* const url = new URL(req.url, `${req.protocol}://${req.headers.host}`);
|
|
664
|
-
* const baseUrl = deLocalizeUrl(url);
|
|
665
|
-
*
|
|
666
|
-
* // Store the base URL for later use
|
|
667
|
-
* req.baseUrl = baseUrl;
|
|
668
|
-
* next();
|
|
669
|
-
* });
|
|
670
|
-
* ```
|
|
671
|
-
*
|
|
672
|
-
* @example
|
|
673
|
-
* ```typescript
|
|
674
|
-
* // Using with URL patterns
|
|
675
|
-
* const url = new URL("https://example.com/de/about");
|
|
676
|
-
* deLocalizeUrl(url); // => URL("https://example.com/about")
|
|
677
|
-
*
|
|
678
|
-
* // Using with domain-based localization
|
|
679
|
-
* const url = new URL("https://de.example.com/store");
|
|
680
|
-
* deLocalizeUrl(url); // => URL("https://example.com/store")
|
|
681
|
-
* ```
|
|
682
|
-
*
|
|
683
|
-
* @param {string | URL} url - The URL to de-localize. If string, must be absolute.
|
|
684
|
-
* @returns {URL} The de-localized URL, always absolute
|
|
685
|
-
*/
|
|
686
|
-
export function deLocalizeUrl(url) {
|
|
687
|
-
if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
|
|
688
|
-
return deLocalizeUrlDefaultPattern(url);
|
|
689
|
-
}
|
|
690
|
-
const urlObj = typeof url === "string" ? new URL(url) : url;
|
|
691
|
-
// Iterate over URL patterns
|
|
692
|
-
for (const element of urlPatterns) {
|
|
693
|
-
// Iterate over localized versions
|
|
694
|
-
for (const [, localizedPattern] of element.localized) {
|
|
695
|
-
const match = new URLPattern(localizedPattern, urlObj.href).exec(urlObj.href);
|
|
696
|
-
if (match) {
|
|
697
|
-
// Convert localized URL back to the base pattern
|
|
698
|
-
const groups = aggregateGroups(match);
|
|
699
|
-
const baseUrl = fillPattern(element.pattern, groups, urlObj.origin);
|
|
700
|
-
return fillMissingUrlParts(baseUrl, match);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
703
|
-
// match unlocalized pattern
|
|
704
|
-
const unlocalizedMatch = new URLPattern(element.pattern, urlObj.href).exec(urlObj.href);
|
|
705
|
-
if (unlocalizedMatch) {
|
|
706
|
-
const baseUrl = fillPattern(element.pattern, aggregateGroups(unlocalizedMatch), urlObj.origin);
|
|
707
|
-
return fillMissingUrlParts(baseUrl, unlocalizedMatch);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
// no match found return the original url
|
|
711
|
-
return urlObj;
|
|
712
|
-
}
|
|
713
|
-
/**
|
|
714
|
-
* De-localizes a URL using the default pattern (/:locale/*)
|
|
715
|
-
* @param {string|URL} url
|
|
716
|
-
* @returns {URL}
|
|
717
|
-
*/
|
|
718
|
-
function deLocalizeUrlDefaultPattern(url) {
|
|
719
|
-
const urlObj = typeof url === "string" ? new URL(url, getUrlOrigin()) : new URL(url);
|
|
720
|
-
const pathSegments = urlObj.pathname.split("/").filter(Boolean);
|
|
721
|
-
// If first segment is a locale, remove it
|
|
722
|
-
if (pathSegments.length > 0 && isLocale(pathSegments[0])) {
|
|
723
|
-
urlObj.pathname = "/" + pathSegments.slice(1).join("/");
|
|
724
|
-
}
|
|
725
|
-
return urlObj;
|
|
726
|
-
}
|
|
727
|
-
/**
|
|
728
|
-
* Takes matches of implicit wildcards in the UrlPattern (when a part is missing
|
|
729
|
-
* it is equal to '*') and adds them back to the result of fillPattern.
|
|
730
|
-
*
|
|
731
|
-
* At least protocol and hostname are required to create a valid URL inside fillPattern.
|
|
732
|
-
*
|
|
733
|
-
* @param {URL} url
|
|
734
|
-
* @param {any} match
|
|
735
|
-
* @returns {URL}
|
|
736
|
-
*/
|
|
737
|
-
function fillMissingUrlParts(url, match) {
|
|
738
|
-
if (match.protocol.groups["0"]) {
|
|
739
|
-
url.protocol = match.protocol.groups["0"] ?? "";
|
|
740
|
-
}
|
|
741
|
-
if (match.hostname.groups["0"]) {
|
|
742
|
-
url.hostname = match.hostname.groups["0"] ?? "";
|
|
743
|
-
}
|
|
744
|
-
if (match.username.groups["0"]) {
|
|
745
|
-
url.username = match.username.groups["0"] ?? "";
|
|
746
|
-
}
|
|
747
|
-
if (match.password.groups["0"]) {
|
|
748
|
-
url.password = match.password.groups["0"] ?? "";
|
|
749
|
-
}
|
|
750
|
-
if (match.port.groups["0"]) {
|
|
751
|
-
url.port = match.port.groups["0"] ?? "";
|
|
752
|
-
}
|
|
753
|
-
if (match.pathname.groups["0"]) {
|
|
754
|
-
url.pathname = match.pathname.groups["0"] ?? "";
|
|
755
|
-
}
|
|
756
|
-
if (match.search.groups["0"]) {
|
|
757
|
-
url.search = match.search.groups["0"] ?? "";
|
|
758
|
-
}
|
|
759
|
-
if (match.hash.groups["0"]) {
|
|
760
|
-
url.hash = match.hash.groups["0"] ?? "";
|
|
761
|
-
}
|
|
762
|
-
return url;
|
|
763
|
-
}
|
|
764
|
-
/**
|
|
765
|
-
* Fills a URL pattern with values for named groups, supporting all URLPattern-style modifiers.
|
|
766
|
-
*
|
|
767
|
-
* This function will eventually be replaced by https://github.com/whatwg/urlpattern/issues/73
|
|
768
|
-
*
|
|
769
|
-
* Matches:
|
|
770
|
-
* - :name -> Simple
|
|
771
|
-
* - :name? -> Optional
|
|
772
|
-
* - :name+ -> One or more
|
|
773
|
-
* - :name* -> Zero or more
|
|
774
|
-
* - :name(...) -> Regex group
|
|
775
|
-
* - {text} -> Group delimiter
|
|
776
|
-
* - {text}? -> Optional group delimiter
|
|
777
|
-
*
|
|
778
|
-
* If the value is `null`, the segment is removed.
|
|
779
|
-
*
|
|
780
|
-
* @param {string} pattern - The URL pattern containing named groups.
|
|
781
|
-
* @param {Record<string, string | null | undefined>} values - Object of values for named groups.
|
|
782
|
-
* @param {string} origin - Base URL to use for URL construction.
|
|
783
|
-
* @returns {URL} - The constructed URL with named groups filled.
|
|
784
|
-
*/
|
|
785
|
-
function fillPattern(pattern, values, origin) {
|
|
786
|
-
// First, handle group delimiters with curly braces
|
|
787
|
-
let processedGroupDelimiters = pattern.replace(/\{([^{}]*)\}([?+*]?)/g, (_, content, modifier) => {
|
|
788
|
-
// For optional group delimiters
|
|
789
|
-
if (modifier === "?") {
|
|
790
|
-
// For optional groups, we'll include the content
|
|
791
|
-
return content;
|
|
792
|
-
}
|
|
793
|
-
// For non-optional group delimiters, always include the content
|
|
794
|
-
return content;
|
|
795
|
-
});
|
|
796
|
-
// Then handle named groups
|
|
797
|
-
const filled = processedGroupDelimiters.replace(/(\/?):([a-zA-Z0-9_]+)(\([^)]*\))?([?+*]?)/g, (_, slash, name, __, modifier) => {
|
|
798
|
-
const value = values[name];
|
|
799
|
-
if (value === null) {
|
|
800
|
-
// If value is null, remove the entire segment including the preceding slash
|
|
801
|
-
return "";
|
|
802
|
-
}
|
|
803
|
-
if (modifier === "?") {
|
|
804
|
-
// Optional segment
|
|
805
|
-
return value !== undefined ? `${slash}${value}` : "";
|
|
806
|
-
}
|
|
807
|
-
if (modifier === "+" || modifier === "*") {
|
|
808
|
-
// Repeatable segments
|
|
809
|
-
if (value === undefined && modifier === "+") {
|
|
810
|
-
throw new Error(`Missing value for "${name}" (one or more required)`);
|
|
811
|
-
}
|
|
812
|
-
return value ? `${slash}${value}` : "";
|
|
813
|
-
}
|
|
814
|
-
// Simple named group (no modifier)
|
|
815
|
-
if (value === undefined) {
|
|
816
|
-
throw new Error(`Missing value for "${name}"`);
|
|
817
|
-
}
|
|
818
|
-
return `${slash}${value}`;
|
|
819
|
-
});
|
|
820
|
-
return new URL(filled, origin);
|
|
821
|
-
}
|
|
822
|
-
/**
|
|
823
|
-
* Aggregates named groups from various parts of the URLPattern match result.
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
* @type {(match: any) => Record<string, string | null | undefined>}
|
|
827
|
-
*/
|
|
828
|
-
export function aggregateGroups(match) {
|
|
829
|
-
return {
|
|
830
|
-
...match.hash.groups,
|
|
831
|
-
...match.hostname.groups,
|
|
832
|
-
...match.password.groups,
|
|
833
|
-
...match.pathname.groups,
|
|
834
|
-
...match.port.groups,
|
|
835
|
-
...match.protocol.groups,
|
|
836
|
-
...match.search.groups,
|
|
837
|
-
...match.username.groups,
|
|
838
|
-
};
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
/**
|
|
842
|
-
* High-level URL localization function optimized for client-side UI usage.
|
|
843
|
-
*
|
|
844
|
-
* This is a convenience wrapper around `localizeUrl()` that provides features
|
|
845
|
-
* needed in UI:
|
|
846
|
-
*
|
|
847
|
-
* - Accepts relative paths (e.g., "/about")
|
|
848
|
-
* - Returns relative paths when possible
|
|
849
|
-
* - Automatically detects current locale if not specified
|
|
850
|
-
* - Handles string input/output instead of URL objects
|
|
851
|
-
*
|
|
852
|
-
* @example
|
|
853
|
-
* ```typescript
|
|
854
|
-
* // In a React/Vue/Svelte component
|
|
855
|
-
* const NavLink = ({ href }) => {
|
|
856
|
-
* // Automatically uses current locale, keeps path relative
|
|
857
|
-
* return <a href={localizeHref(href)}>...</a>;
|
|
858
|
-
* };
|
|
859
|
-
*
|
|
860
|
-
* // Examples:
|
|
861
|
-
* localizeHref("/about")
|
|
862
|
-
* // => "/de/about" (if current locale is "de")
|
|
863
|
-
* localizeHref("/store", { locale: "fr" })
|
|
864
|
-
* // => "/fr/store" (explicit locale)
|
|
865
|
-
*
|
|
866
|
-
* // Cross-origin links remain absolute
|
|
867
|
-
* localizeHref("https://other-site.com/about")
|
|
868
|
-
* // => "https://other-site.com/de/about"
|
|
869
|
-
* ```
|
|
870
|
-
*
|
|
871
|
-
* For server-side URL localization (e.g., in middleware), use `localizeUrl()`
|
|
872
|
-
* which provides more precise control over URL handling.
|
|
873
|
-
*
|
|
874
|
-
* @param {string} href - The href to localize (can be relative or absolute)
|
|
875
|
-
* @param {Object} [options] - Options for localization
|
|
876
|
-
* @param {string} [options.locale] - Target locale. If not provided, uses `getLocale()`
|
|
877
|
-
* @returns {string} The localized href, relative if input was relative
|
|
878
|
-
*/
|
|
879
|
-
export function localizeHref(href, options) {
|
|
880
|
-
const locale = options?.locale ?? getLocale();
|
|
881
|
-
const url = new URL(href, getUrlOrigin());
|
|
882
|
-
const localized = localizeUrl(url, options);
|
|
883
|
-
// if the origin is identical and the href is relative,
|
|
884
|
-
// return the relative path
|
|
885
|
-
if (href.startsWith("/") && url.origin === localized.origin) {
|
|
886
|
-
// check for cross origin localization in which case an absolute URL must be returned.
|
|
887
|
-
if (locale !== getLocale()) {
|
|
888
|
-
const localizedCurrentLocale = localizeUrl(url, { locale: getLocale() });
|
|
889
|
-
if (localizedCurrentLocale.origin !== localized.origin) {
|
|
890
|
-
return localized.href;
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
return localized.pathname + localized.search + localized.hash;
|
|
894
|
-
}
|
|
895
|
-
return localized.href;
|
|
896
|
-
}
|
|
897
|
-
/**
|
|
898
|
-
* High-level URL de-localization function optimized for client-side UI usage.
|
|
899
|
-
*
|
|
900
|
-
* This is a convenience wrapper around `deLocalizeUrl()` that provides features
|
|
901
|
-
* needed in the UI:
|
|
902
|
-
*
|
|
903
|
-
* - Accepts relative paths (e.g., "/de/about")
|
|
904
|
-
* - Returns relative paths when possible
|
|
905
|
-
* - Handles string input/output instead of URL objects
|
|
906
|
-
*
|
|
907
|
-
* @example
|
|
908
|
-
* ```typescript
|
|
909
|
-
* // In a React/Vue/Svelte component
|
|
910
|
-
* const LocaleSwitcher = ({ href }) => {
|
|
911
|
-
* // Remove locale prefix before switching
|
|
912
|
-
* const baseHref = deLocalizeHref(href);
|
|
913
|
-
* return locales.map(locale =>
|
|
914
|
-
* <a href={localizeHref(baseHref, { locale })}>
|
|
915
|
-
* Switch to {locale}
|
|
916
|
-
* </a>
|
|
917
|
-
* );
|
|
918
|
-
* };
|
|
919
|
-
*
|
|
920
|
-
* // Examples:
|
|
921
|
-
* deLocalizeHref("/de/about") // => "/about"
|
|
922
|
-
* deLocalizeHref("/fr/store") // => "/store"
|
|
923
|
-
*
|
|
924
|
-
* // Cross-origin links remain absolute
|
|
925
|
-
* deLocalizeHref("https://example.com/de/about")
|
|
926
|
-
* // => "https://example.com/about"
|
|
927
|
-
* ```
|
|
928
|
-
*
|
|
929
|
-
* For server-side URL de-localization (e.g., in middleware), use `deLocalizeUrl()`
|
|
930
|
-
* which provides more precise control over URL handling.
|
|
931
|
-
*
|
|
932
|
-
* @param {string} href - The href to de-localize (can be relative or absolute)
|
|
933
|
-
* @returns {string} The de-localized href, relative if input was relative
|
|
934
|
-
* @see deLocalizeUrl - For low-level URL de-localization in server contexts
|
|
935
|
-
*/
|
|
936
|
-
export function deLocalizeHref(href) {
|
|
937
|
-
const url = new URL(href, getUrlOrigin());
|
|
938
|
-
const deLocalized = deLocalizeUrl(url);
|
|
939
|
-
// If the origin is identical and the href is relative,
|
|
940
|
-
// return the relative path instead of the full URL.
|
|
941
|
-
if (href.startsWith("/") && url.origin === deLocalized.origin) {
|
|
942
|
-
return deLocalized.pathname + deLocalized.search + deLocalized.hash;
|
|
943
|
-
}
|
|
944
|
-
return deLocalized.href;
|
|
945
|
-
}
|
|
946
|
-
|
|
947
|
-
/**
|
|
948
|
-
* @param {string} safeModuleId
|
|
949
|
-
* @param {Locale} locale
|
|
950
|
-
*/
|
|
951
|
-
export function trackMessageCall(safeModuleId, locale) {
|
|
952
|
-
if (isServer === false)
|
|
953
|
-
return;
|
|
954
|
-
const store = serverAsyncLocalStorage?.getStore();
|
|
955
|
-
if (store) {
|
|
956
|
-
store.messageCalls?.add(`${safeModuleId}:${locale}`);
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
|
|
960
|
-
/**
|
|
961
|
-
* Generates a list of localized URLs for all provided URLs.
|
|
962
|
-
*
|
|
963
|
-
* This is useful for SSG (Static Site Generation) and sitemap generation.
|
|
964
|
-
* NextJS and other frameworks use this function for SSG.
|
|
965
|
-
*
|
|
966
|
-
* @example
|
|
967
|
-
* ```typescript
|
|
968
|
-
* const urls = generateStaticLocalizedUrls([
|
|
969
|
-
* "https://example.com/about",
|
|
970
|
-
* "https://example.com/blog",
|
|
971
|
-
* ]);
|
|
972
|
-
* urls[0].href // => "https://example.com/about"
|
|
973
|
-
* urls[1].href // => "https://example.com/blog"
|
|
974
|
-
* urls[2].href // => "https://example.com/de/about"
|
|
975
|
-
* urls[3].href // => "https://example.com/de/blog"
|
|
976
|
-
* ...
|
|
977
|
-
* ```
|
|
978
|
-
*
|
|
979
|
-
* @param {(string | URL)[]} urls - List of URLs to generate localized versions for. Can be absolute URLs or paths.
|
|
980
|
-
* @returns {URL[]} List of localized URLs as URL objects
|
|
981
|
-
*/
|
|
982
|
-
export function generateStaticLocalizedUrls(urls) {
|
|
983
|
-
const localizedUrls = new Set();
|
|
984
|
-
// For default URL pattern, we can optimize the generation
|
|
985
|
-
if (TREE_SHAKE_DEFAULT_URL_PATTERN_USED) {
|
|
986
|
-
for (const urlInput of urls) {
|
|
987
|
-
const url = urlInput instanceof URL
|
|
988
|
-
? urlInput
|
|
989
|
-
: new URL(urlInput, "http://localhost");
|
|
990
|
-
// Base locale doesn't get a prefix
|
|
991
|
-
localizedUrls.add(url);
|
|
992
|
-
// Other locales get their code as prefix
|
|
993
|
-
for (const locale of locales) {
|
|
994
|
-
if (locale !== baseLocale) {
|
|
995
|
-
const localizedPath = `/${locale}${url.pathname}${url.search}${url.hash}`;
|
|
996
|
-
const localizedUrl = new URL(localizedPath, url.origin);
|
|
997
|
-
localizedUrls.add(localizedUrl);
|
|
998
|
-
}
|
|
999
|
-
}
|
|
1000
|
-
}
|
|
1001
|
-
return Array.from(localizedUrls);
|
|
1002
|
-
}
|
|
1003
|
-
// For custom URL patterns, we need to use localizeUrl for each URL and locale
|
|
1004
|
-
for (const urlInput of urls) {
|
|
1005
|
-
const url = urlInput instanceof URL
|
|
1006
|
-
? urlInput
|
|
1007
|
-
: new URL(urlInput, "http://localhost");
|
|
1008
|
-
// Try each URL pattern to find one that matches
|
|
1009
|
-
let patternFound = false;
|
|
1010
|
-
for (const pattern of urlPatterns) {
|
|
1011
|
-
try {
|
|
1012
|
-
// Try to match the unlocalized pattern
|
|
1013
|
-
const unlocalizedMatch = new URLPattern(pattern.pattern, url.href).exec(url.href);
|
|
1014
|
-
if (!unlocalizedMatch)
|
|
1015
|
-
continue;
|
|
1016
|
-
patternFound = true;
|
|
1017
|
-
// Track unique localized URLs to avoid duplicates when patterns are the same
|
|
1018
|
-
const seenUrls = new Set();
|
|
1019
|
-
// Generate localized URL for each locale
|
|
1020
|
-
for (const [locale] of pattern.localized) {
|
|
1021
|
-
try {
|
|
1022
|
-
const localizedUrl = localizeUrl(url, { locale });
|
|
1023
|
-
const urlString = localizedUrl.href;
|
|
1024
|
-
// Only add if we haven't seen this exact URL before
|
|
1025
|
-
if (!seenUrls.has(urlString)) {
|
|
1026
|
-
seenUrls.add(urlString);
|
|
1027
|
-
localizedUrls.add(localizedUrl);
|
|
1028
|
-
}
|
|
1029
|
-
}
|
|
1030
|
-
catch {
|
|
1031
|
-
// Skip if localization fails for this locale
|
|
1032
|
-
continue;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
break;
|
|
1036
|
-
}
|
|
1037
|
-
catch {
|
|
1038
|
-
// Skip if pattern matching fails
|
|
1039
|
-
continue;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
// If no pattern matched, use the URL as is
|
|
1043
|
-
if (!patternFound) {
|
|
1044
|
-
localizedUrls.add(url);
|
|
1045
|
-
}
|
|
1046
|
-
}
|
|
1047
|
-
return Array.from(localizedUrls);
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
// ------ TYPES ------
|
|
1051
|
-
|
|
1052
|
-
/**
|
|
1053
|
-
* A locale that is available in the project.
|
|
1054
|
-
*
|
|
1055
|
-
* @example
|
|
1056
|
-
* setLocale(request.locale as Locale)
|
|
1057
|
-
*
|
|
1058
|
-
* @typedef {(typeof locales)[number]} Locale
|
|
1059
|
-
*/
|
|
1060
|
-
|