@inlang/paraglide-js 2.3.2 → 2.4.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/dist/bundler-plugins/vite.d.ts +1 -1
- package/dist/bundler-plugins/vite.d.ts.map +1 -1
- package/dist/compiler/compile-project.js +1 -1
- package/dist/compiler/compile-project.test.js +13 -13
- package/dist/compiler/compile.test.js +20 -7
- package/dist/compiler/runtime/create-runtime.d.ts.map +1 -1
- package/dist/compiler/runtime/create-runtime.js +2 -0
- package/dist/compiler/runtime/set-locale.d.ts +8 -4
- package/dist/compiler/runtime/set-locale.d.ts.map +1 -1
- package/dist/compiler/runtime/set-locale.js +19 -18
- package/dist/compiler/runtime/set-locale.test.js +25 -0
- package/dist/compiler/runtime/should-redirect.d.ts +80 -0
- package/dist/compiler/runtime/should-redirect.d.ts.map +1 -0
- package/dist/compiler/runtime/should-redirect.js +119 -0
- package/dist/compiler/runtime/should-redirect.test.d.ts +2 -0
- package/dist/compiler/runtime/should-redirect.test.d.ts.map +1 -0
- package/dist/compiler/runtime/should-redirect.test.js +119 -0
- package/dist/compiler/runtime/type.d.ts +1 -0
- package/dist/compiler/runtime/type.d.ts.map +1 -1
- package/dist/compiler/server/middleware.d.ts.map +1 -1
- package/dist/compiler/server/middleware.js +18 -31
- package/dist/services/env-variables/index.js +1 -1
- package/dist/services/file-handling/write-output.test.js +7 -10
- package/package.json +5 -7
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const paraglideVitePlugin: (options: import("../index.js").CompilerOptions) => import("
|
|
1
|
+
export declare const paraglideVitePlugin: (options: import("../index.js").CompilerOptions) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
|
|
2
2
|
//# sourceMappingURL=vite.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/vite.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,+
|
|
1
|
+
{"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/vite.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,+GAAoC,CAAC"}
|
|
@@ -69,7 +69,7 @@ export const compileProject = async (args) => {
|
|
|
69
69
|
for (const [filename, content] of Object.entries(output)) {
|
|
70
70
|
if (optionsWithDefaults.includeEslintDisableComment) {
|
|
71
71
|
if (filename.endsWith(".js")) {
|
|
72
|
-
output[filename] =
|
|
72
|
+
output[filename] = `/* eslint-disable */\n${content}`;
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
75
|
}
|
|
@@ -70,7 +70,7 @@ test("handles message bundles with a : in the id", async () => {
|
|
|
70
70
|
},
|
|
71
71
|
}),
|
|
72
72
|
});
|
|
73
|
-
await insertBundleNested(project
|
|
73
|
+
await insertBundleNested(project, createBundleNested({
|
|
74
74
|
id: "hello:world",
|
|
75
75
|
messages: [
|
|
76
76
|
{
|
|
@@ -97,7 +97,7 @@ test("can emit message bundles with more than 255 characters", async () => {
|
|
|
97
97
|
},
|
|
98
98
|
}),
|
|
99
99
|
});
|
|
100
|
-
await insertBundleNested(project
|
|
100
|
+
await insertBundleNested(project, createBundleNested({
|
|
101
101
|
// 300 characters long id
|
|
102
102
|
id: "a".repeat(300),
|
|
103
103
|
messages: [
|
|
@@ -199,7 +199,7 @@ describe.each([
|
|
|
199
199
|
settings: { locales: ["en", "de"], baseLocale: "en" },
|
|
200
200
|
}),
|
|
201
201
|
});
|
|
202
|
-
await insertBundleNested(project
|
|
202
|
+
await insertBundleNested(project, createBundleNested({
|
|
203
203
|
id: "plural_test",
|
|
204
204
|
declarations: [
|
|
205
205
|
{ type: "input-variable", name: "count" },
|
|
@@ -333,7 +333,7 @@ describe.each([
|
|
|
333
333
|
settings: { locales: ["en", "de", "en-US"], baseLocale: "en" },
|
|
334
334
|
}),
|
|
335
335
|
});
|
|
336
|
-
await insertBundleNested(project
|
|
336
|
+
await insertBundleNested(project, createBundleNested({
|
|
337
337
|
id: "missingInGerman",
|
|
338
338
|
messages: [
|
|
339
339
|
{
|
|
@@ -363,7 +363,7 @@ describe.each([
|
|
|
363
363
|
}),
|
|
364
364
|
});
|
|
365
365
|
// Add test messages
|
|
366
|
-
await insertBundleNested(project
|
|
366
|
+
await insertBundleNested(project, createBundleNested({
|
|
367
367
|
id: "greeting",
|
|
368
368
|
messages: [
|
|
369
369
|
{
|
|
@@ -380,7 +380,7 @@ describe.each([
|
|
|
380
380
|
},
|
|
381
381
|
],
|
|
382
382
|
}));
|
|
383
|
-
await insertBundleNested(project
|
|
383
|
+
await insertBundleNested(project, createBundleNested({
|
|
384
384
|
id: "farewell",
|
|
385
385
|
messages: [
|
|
386
386
|
{
|
|
@@ -473,7 +473,7 @@ describe.each([
|
|
|
473
473
|
settings: { locales: ["en", "de"], baseLocale: "en" },
|
|
474
474
|
}),
|
|
475
475
|
});
|
|
476
|
-
await insertBundleNested(project
|
|
476
|
+
await insertBundleNested(project, createBundleNested({
|
|
477
477
|
id: "$502.23-hello_world",
|
|
478
478
|
messages: [
|
|
479
479
|
{
|
|
@@ -499,7 +499,7 @@ describe.each([
|
|
|
499
499
|
settings: { locales: ["en", "en-US"], baseLocale: "en" },
|
|
500
500
|
}),
|
|
501
501
|
});
|
|
502
|
-
await insertBundleNested(project
|
|
502
|
+
await insertBundleNested(project, createBundleNested({
|
|
503
503
|
id: "exists_in_both",
|
|
504
504
|
messages: [
|
|
505
505
|
{
|
|
@@ -523,7 +523,7 @@ describe.each([
|
|
|
523
523
|
},
|
|
524
524
|
],
|
|
525
525
|
}));
|
|
526
|
-
await insertBundleNested(project
|
|
526
|
+
await insertBundleNested(project, createBundleNested({
|
|
527
527
|
id: "missing_in_en_US",
|
|
528
528
|
messages: [
|
|
529
529
|
{
|
|
@@ -552,7 +552,7 @@ describe.each([
|
|
|
552
552
|
settings: { locales: ["en"], baseLocale: "en" },
|
|
553
553
|
}),
|
|
554
554
|
});
|
|
555
|
-
await insertBundleNested(project
|
|
555
|
+
await insertBundleNested(project, createBundleNested({
|
|
556
556
|
id: "happy🍌",
|
|
557
557
|
messages: [
|
|
558
558
|
{
|
|
@@ -578,7 +578,7 @@ describe.each([
|
|
|
578
578
|
}),
|
|
579
579
|
});
|
|
580
580
|
// Create two bundles with the same name but different case
|
|
581
|
-
await insertBundleNested(project
|
|
581
|
+
await insertBundleNested(project, createBundleNested({
|
|
582
582
|
id: "Helloworld",
|
|
583
583
|
messages: [
|
|
584
584
|
{
|
|
@@ -593,7 +593,7 @@ describe.each([
|
|
|
593
593
|
},
|
|
594
594
|
],
|
|
595
595
|
}));
|
|
596
|
-
await insertBundleNested(project
|
|
596
|
+
await insertBundleNested(project, createBundleNested({
|
|
597
597
|
id: "helloworld",
|
|
598
598
|
messages: [
|
|
599
599
|
{
|
|
@@ -883,7 +883,7 @@ const mockBundles = [
|
|
|
883
883
|
},
|
|
884
884
|
];
|
|
885
885
|
for (const bundle of mockBundles) {
|
|
886
|
-
await insertBundleNested(project
|
|
886
|
+
await insertBundleNested(project, bundle);
|
|
887
887
|
}
|
|
888
888
|
function createBundleNested(args) {
|
|
889
889
|
return {
|
|
@@ -237,7 +237,7 @@ test("includes eslint-disable comment", async () => {
|
|
|
237
237
|
fs: fs,
|
|
238
238
|
});
|
|
239
239
|
const messages = await fs.promises.readFile("/output/messages.js", "utf8");
|
|
240
|
-
expect(messages).toContain("
|
|
240
|
+
expect(messages).toContain("/* eslint-disable */");
|
|
241
241
|
await compile({
|
|
242
242
|
project: "/project.inlang",
|
|
243
243
|
outdir: "/output",
|
|
@@ -245,7 +245,7 @@ test("includes eslint-disable comment", async () => {
|
|
|
245
245
|
fs: fs,
|
|
246
246
|
});
|
|
247
247
|
const messagesWithoutComment = await fs.promises.readFile("/output/messages.js", "utf8");
|
|
248
|
-
expect(messagesWithoutComment).not.toContain("
|
|
248
|
+
expect(messagesWithoutComment).not.toContain("/* eslint-disable */");
|
|
249
249
|
});
|
|
250
250
|
test("default compiler options should include cookied, variable and baseLocale to ensure easy try out of paraglide js, working both in server and browser environemnts", () => {
|
|
251
251
|
// someone trying out paraglide js should be able to call `getLocale()` and `setLocale()`
|
|
@@ -298,16 +298,29 @@ test("emits warnings for modules that couldn't be imported via http", async () =
|
|
|
298
298
|
const mock = vi.fn();
|
|
299
299
|
consola.mockTypes(() => mock);
|
|
300
300
|
const fs = memfs().fs;
|
|
301
|
+
const fetchMock = vi.fn().mockRejectedValue(new TypeError("network error"));
|
|
302
|
+
vi.stubGlobal("fetch", fetchMock);
|
|
303
|
+
const errorsSpy = vi.spyOn(project.errors, "get").mockResolvedValue([
|
|
304
|
+
{
|
|
305
|
+
message: "Failed to import module https://example.com/non-existent-paraglide-plugin.js",
|
|
306
|
+
},
|
|
307
|
+
]);
|
|
301
308
|
// save project to directory to test loading
|
|
302
309
|
await saveProjectToDirectory({
|
|
303
310
|
project,
|
|
304
311
|
path: "/project.inlang",
|
|
305
312
|
fs: fs.promises,
|
|
306
313
|
});
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
314
|
+
try {
|
|
315
|
+
await compile({
|
|
316
|
+
project: "/project.inlang",
|
|
317
|
+
outdir: "/output",
|
|
318
|
+
fs: fs,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
finally {
|
|
322
|
+
vi.unstubAllGlobals();
|
|
323
|
+
errorsSpy.mockRestore();
|
|
324
|
+
}
|
|
312
325
|
expect(mock).toHaveBeenCalled();
|
|
313
326
|
});
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"create-runtime.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/create-runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,EAAE;QAChB,QAAQ,EAAE,WAAW,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC;QACnD,UAAU,EAAE,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC;QACvD,YAAY,EAAE,WAAW,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC;QAC3D,YAAY,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;QAC9C,WAAW,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,CAAC;QAC7C,qCAAqC,EAAE,eAAe,CAAC,uCAAuC,CAAC,CAAC;QAChG,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;QACtC,eAAe,EAAE,eAAe,CAAC,iBAAiB,CAAC,CAAC;QACpD,wBAAwB,EAAE,WAAW,CACpC,eAAe,CAAC,0BAA0B,CAAC,CAC3C,CAAC;KACF,CAAC;CACF,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"create-runtime.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/create-runtime.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE;IACvC,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,eAAe,EAAE;QAChB,QAAQ,EAAE,WAAW,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,CAAC;QACnD,UAAU,EAAE,WAAW,CAAC,eAAe,CAAC,YAAY,CAAC,CAAC,CAAC;QACvD,YAAY,EAAE,WAAW,CAAC,eAAe,CAAC,cAAc,CAAC,CAAC,CAAC;QAC3D,YAAY,EAAE,eAAe,CAAC,cAAc,CAAC,CAAC;QAC9C,WAAW,CAAC,EAAE,eAAe,CAAC,aAAa,CAAC,CAAC;QAC7C,qCAAqC,EAAE,eAAe,CAAC,uCAAuC,CAAC,CAAC;QAChG,QAAQ,EAAE,eAAe,CAAC,UAAU,CAAC,CAAC;QACtC,eAAe,EAAE,eAAe,CAAC,iBAAiB,CAAC,CAAC;QACpD,wBAAwB,EAAE,WAAW,CACpC,eAAe,CAAC,0BAA0B,CAAC,CAC3C,CAAC;KACF,CAAC;CACF,GAAG,MAAM,CA4IT"}
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {(newLocale: Locale, options?: { reload?: boolean }) => void | Promise<void>} SetLocaleFn
|
|
3
|
+
*/
|
|
1
4
|
/**
|
|
2
5
|
* Set the locale.
|
|
3
6
|
*
|
|
@@ -15,10 +18,11 @@
|
|
|
15
18
|
* @example
|
|
16
19
|
* setLocale('en', { reload: false });
|
|
17
20
|
*
|
|
18
|
-
* @type {
|
|
21
|
+
* @type {SetLocaleFn}
|
|
19
22
|
*/
|
|
20
|
-
export let setLocale:
|
|
23
|
+
export let setLocale: SetLocaleFn;
|
|
24
|
+
export function overwriteSetLocale(fn: SetLocaleFn): void;
|
|
25
|
+
export type SetLocaleFn = (newLocale: Locale, options?: {
|
|
21
26
|
reload?: boolean;
|
|
22
|
-
}) =>
|
|
23
|
-
export function overwriteSetLocale(fn: (newLocale: Locale) => void): void;
|
|
27
|
+
}) => void | Promise<void>;
|
|
24
28
|
//# sourceMappingURL=set-locale.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"set-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/set-locale.js"],"names":[],"mappings":"AAgCA;;;;;;;;;;;;;;;;;;GAkBG;AACH,sBAFU,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,
|
|
1
|
+
{"version":3,"file":"set-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/set-locale.js"],"names":[],"mappings":"AAgCA;;GAEG;AAEH;;;;;;;;;;;;;;;;;;GAkBG;AACH,sBAFU,WAAW,CAyGnB;AAgBK,uCAFI,WAAW,QAIrB;0BA/IY,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAE,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC"}
|
|
@@ -18,6 +18,9 @@ const navigateOrReload = (newLocation) => {
|
|
|
18
18
|
window.location.reload();
|
|
19
19
|
}
|
|
20
20
|
};
|
|
21
|
+
/**
|
|
22
|
+
* @typedef {(newLocale: Locale, options?: { reload?: boolean }) => void | Promise<void>} SetLocaleFn
|
|
23
|
+
*/
|
|
21
24
|
/**
|
|
22
25
|
* Set the locale.
|
|
23
26
|
*
|
|
@@ -35,7 +38,7 @@ const navigateOrReload = (newLocation) => {
|
|
|
35
38
|
* @example
|
|
36
39
|
* setLocale('en', { reload: false });
|
|
37
40
|
*
|
|
38
|
-
* @type {
|
|
41
|
+
* @type {SetLocaleFn}
|
|
39
42
|
*/
|
|
40
43
|
export let setLocale = (newLocale, options) => {
|
|
41
44
|
const optionsWithDefaults = {
|
|
@@ -44,6 +47,7 @@ export let setLocale = (newLocale, options) => {
|
|
|
44
47
|
};
|
|
45
48
|
// locale is already set
|
|
46
49
|
// https://github.com/opral/inlang-paraglide-js/issues/430
|
|
50
|
+
/** @type {Locale | undefined} */
|
|
47
51
|
let currentLocale;
|
|
48
52
|
try {
|
|
49
53
|
currentLocale = getLocale();
|
|
@@ -103,7 +107,7 @@ export let setLocale = (newLocale, options) => {
|
|
|
103
107
|
const handler = customClientStrategies.get(strat);
|
|
104
108
|
if (handler) {
|
|
105
109
|
let result = handler.setLocale(newLocale);
|
|
106
|
-
// Handle async setLocale
|
|
110
|
+
// Handle async setLocale
|
|
107
111
|
if (result instanceof Promise) {
|
|
108
112
|
result = result.catch((error) => {
|
|
109
113
|
throw new Error(`Custom strategy "${strat}" setLocale failed.`, {
|
|
@@ -115,23 +119,20 @@ export let setLocale = (newLocale, options) => {
|
|
|
115
119
|
}
|
|
116
120
|
}
|
|
117
121
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Wait for any async custom setLocale functions
|
|
124
|
-
return Promise.all(customSetLocalePromises).then(() => {
|
|
125
|
-
navigateOrReload(newLocation);
|
|
126
|
-
});
|
|
127
|
-
}
|
|
128
|
-
else {
|
|
122
|
+
const runReload = () => {
|
|
123
|
+
if (!isServer &&
|
|
124
|
+
optionsWithDefaults.reload &&
|
|
125
|
+
window.location &&
|
|
126
|
+
newLocale !== currentLocale) {
|
|
129
127
|
navigateOrReload(newLocation);
|
|
130
128
|
}
|
|
129
|
+
};
|
|
130
|
+
if (customSetLocalePromises.length) {
|
|
131
|
+
return Promise.all(customSetLocalePromises).then(() => {
|
|
132
|
+
runReload();
|
|
133
|
+
});
|
|
131
134
|
}
|
|
132
|
-
|
|
133
|
-
return Promise.all(customSetLocalePromises);
|
|
134
|
-
}
|
|
135
|
+
runReload();
|
|
135
136
|
return;
|
|
136
137
|
};
|
|
137
138
|
/**
|
|
@@ -146,8 +147,8 @@ export let setLocale = (newLocale, options) => {
|
|
|
146
147
|
* return Cookies.set('locale', newLocale)
|
|
147
148
|
* });
|
|
148
149
|
*
|
|
149
|
-
* @param {
|
|
150
|
+
* @param {SetLocaleFn} fn
|
|
150
151
|
*/
|
|
151
152
|
export const overwriteSetLocale = (fn) => {
|
|
152
|
-
setLocale = fn;
|
|
153
|
+
setLocale = /** @type {SetLocaleFn} */ (fn);
|
|
153
154
|
};
|
|
@@ -155,6 +155,31 @@ test("when strategy precedes URL, it should set the locale and re-direct to the
|
|
|
155
155
|
expect(globalThis.document.cookie).toBe("PARAGLIDE_LOCALE=en; path=/; max-age=34560000");
|
|
156
156
|
expect(globalThis.window.location.href).toBe("https://example.com/en/some-path");
|
|
157
157
|
});
|
|
158
|
+
test("overwriteSetLocale receives the options object", async () => {
|
|
159
|
+
const runtime = await createParaglide({
|
|
160
|
+
blob: await newProject({
|
|
161
|
+
settings: {
|
|
162
|
+
baseLocale: "en",
|
|
163
|
+
locales: ["en", "fr"],
|
|
164
|
+
},
|
|
165
|
+
}),
|
|
166
|
+
strategy: ["cookie"],
|
|
167
|
+
cookieName: "PARAGLIDE_LOCALE",
|
|
168
|
+
});
|
|
169
|
+
// Provide minimal browser globals to avoid strategy branches failing.
|
|
170
|
+
/** @ts-expect-error - browser shim for tests */
|
|
171
|
+
globalThis.document = { cookie: "" };
|
|
172
|
+
globalThis.window = {
|
|
173
|
+
location: {
|
|
174
|
+
href: "https://example.com/en",
|
|
175
|
+
reload: vi.fn(),
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
const spy = vi.fn();
|
|
179
|
+
runtime.overwriteSetLocale(spy);
|
|
180
|
+
runtime.setLocale("fr", { reload: false });
|
|
181
|
+
expect(spy).toHaveBeenCalledWith("fr", { reload: false });
|
|
182
|
+
});
|
|
158
183
|
// https://github.com/opral/inlang-paraglide-js/issues/430
|
|
159
184
|
test("should not reload when setting locale to current locale", async () => {
|
|
160
185
|
// @ts-expect-error - global variable definition
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @typedef {object} ShouldRedirectServerInput
|
|
3
|
+
* @property {Request} request
|
|
4
|
+
* @property {string | URL} [url]
|
|
5
|
+
* @property {ReturnType<typeof assertIsLocale>} [locale]
|
|
6
|
+
*
|
|
7
|
+
* @typedef {object} ShouldRedirectClientInput
|
|
8
|
+
* @property {undefined} [request]
|
|
9
|
+
* @property {string | URL} [url]
|
|
10
|
+
* @property {ReturnType<typeof assertIsLocale>} [locale]
|
|
11
|
+
*
|
|
12
|
+
* @typedef {ShouldRedirectServerInput | ShouldRedirectClientInput} ShouldRedirectInput
|
|
13
|
+
*
|
|
14
|
+
* @typedef {object} ShouldRedirectResult
|
|
15
|
+
* @property {boolean} shouldRedirect - Indicates whether the consumer should perform a redirect.
|
|
16
|
+
* @property {ReturnType<typeof assertIsLocale>} locale - Locale resolved using the configured strategies.
|
|
17
|
+
* @property {URL | undefined} redirectUrl - Destination URL when a redirect is required.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Determines whether a redirect is required to align the current URL with the active locale.
|
|
21
|
+
*
|
|
22
|
+
* This helper mirrors the logic that powers `paraglideMiddleware`, but works in both server
|
|
23
|
+
* and client environments. It evaluates the configured strategies in order, computes the
|
|
24
|
+
* canonical localized URL, and reports when the current URL does not match.
|
|
25
|
+
*
|
|
26
|
+
* When called in the browser without arguments, the current `window.location.href` is used.
|
|
27
|
+
*
|
|
28
|
+
* @example
|
|
29
|
+
* // Client side usage (e.g. TanStack Router beforeLoad hook)
|
|
30
|
+
* async function beforeLoad({ location }) {
|
|
31
|
+
* const decision = await shouldRedirect({ url: location.href });
|
|
32
|
+
*
|
|
33
|
+
* if (decision.shouldRedirect) {
|
|
34
|
+
* throw redirect({ to: decision.redirectUrl.href });
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* // Server side usage with a Request
|
|
40
|
+
* export async function handle(request) {
|
|
41
|
+
* const decision = await shouldRedirect({ request });
|
|
42
|
+
*
|
|
43
|
+
* if (decision.shouldRedirect) {
|
|
44
|
+
* return Response.redirect(decision.redirectUrl, 307);
|
|
45
|
+
* }
|
|
46
|
+
*
|
|
47
|
+
* return render(request, decision.locale);
|
|
48
|
+
* }
|
|
49
|
+
*
|
|
50
|
+
* @param {ShouldRedirectInput} [input]
|
|
51
|
+
* @returns {Promise<ShouldRedirectResult>}
|
|
52
|
+
*/
|
|
53
|
+
export function shouldRedirect(input?: ShouldRedirectInput): Promise<ShouldRedirectResult>;
|
|
54
|
+
export type ShouldRedirectServerInput = {
|
|
55
|
+
request: Request;
|
|
56
|
+
url?: string | URL | undefined;
|
|
57
|
+
locale?: ReturnType<typeof assertIsLocale>;
|
|
58
|
+
};
|
|
59
|
+
export type ShouldRedirectClientInput = {
|
|
60
|
+
request?: undefined;
|
|
61
|
+
url?: string | URL | undefined;
|
|
62
|
+
locale?: ReturnType<typeof assertIsLocale>;
|
|
63
|
+
};
|
|
64
|
+
export type ShouldRedirectInput = ShouldRedirectServerInput | ShouldRedirectClientInput;
|
|
65
|
+
export type ShouldRedirectResult = {
|
|
66
|
+
/**
|
|
67
|
+
* - Indicates whether the consumer should perform a redirect.
|
|
68
|
+
*/
|
|
69
|
+
shouldRedirect: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* - Locale resolved using the configured strategies.
|
|
72
|
+
*/
|
|
73
|
+
locale: ReturnType<typeof assertIsLocale>;
|
|
74
|
+
/**
|
|
75
|
+
* - Destination URL when a redirect is required.
|
|
76
|
+
*/
|
|
77
|
+
redirectUrl: URL | undefined;
|
|
78
|
+
};
|
|
79
|
+
import { assertIsLocale } from "./assert-is-locale.js";
|
|
80
|
+
//# sourceMappingURL=should-redirect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"should-redirect.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/should-redirect.js"],"names":[],"mappings":"AAOA;;;;;;;;;;;;;;;;;GAiBG;AAEH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,uCAHW,mBAAmB,GACjB,OAAO,CAAC,oBAAoB,CAAC,CAsBzC;;aAvEa,OAAO;;aAEP,UAAU,CAAC,OAAO,cAAc,CAAC;;;cAGjC,SAAS;;aAET,UAAU,CAAC,OAAO,cAAc,CAAC;;kCAElC,yBAAyB,GAAG,yBAAyB;;;;;oBAGpD,OAAO;;;;YACP,UAAU,CAAC,OAAO,cAAc,CAAC;;;;iBACjC,GAAG,GAAG,SAAS;;+BAnBE,uBAAuB"}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { localizeUrl } from "./localize-url.js";
|
|
2
|
+
import { getLocale } from "./get-locale.js";
|
|
3
|
+
import { getUrlOrigin } from "./get-url-origin.js";
|
|
4
|
+
import { extractLocaleFromRequestAsync } from "./extract-locale-from-request-async.js";
|
|
5
|
+
import { assertIsLocale } from "./assert-is-locale.js";
|
|
6
|
+
import { strategy } from "./variables.js";
|
|
7
|
+
/**
|
|
8
|
+
* @typedef {object} ShouldRedirectServerInput
|
|
9
|
+
* @property {Request} request
|
|
10
|
+
* @property {string | URL} [url]
|
|
11
|
+
* @property {ReturnType<typeof assertIsLocale>} [locale]
|
|
12
|
+
*
|
|
13
|
+
* @typedef {object} ShouldRedirectClientInput
|
|
14
|
+
* @property {undefined} [request]
|
|
15
|
+
* @property {string | URL} [url]
|
|
16
|
+
* @property {ReturnType<typeof assertIsLocale>} [locale]
|
|
17
|
+
*
|
|
18
|
+
* @typedef {ShouldRedirectServerInput | ShouldRedirectClientInput} ShouldRedirectInput
|
|
19
|
+
*
|
|
20
|
+
* @typedef {object} ShouldRedirectResult
|
|
21
|
+
* @property {boolean} shouldRedirect - Indicates whether the consumer should perform a redirect.
|
|
22
|
+
* @property {ReturnType<typeof assertIsLocale>} locale - Locale resolved using the configured strategies.
|
|
23
|
+
* @property {URL | undefined} redirectUrl - Destination URL when a redirect is required.
|
|
24
|
+
*/
|
|
25
|
+
/**
|
|
26
|
+
* Determines whether a redirect is required to align the current URL with the active locale.
|
|
27
|
+
*
|
|
28
|
+
* This helper mirrors the logic that powers `paraglideMiddleware`, but works in both server
|
|
29
|
+
* and client environments. It evaluates the configured strategies in order, computes the
|
|
30
|
+
* canonical localized URL, and reports when the current URL does not match.
|
|
31
|
+
*
|
|
32
|
+
* When called in the browser without arguments, the current `window.location.href` is used.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* // Client side usage (e.g. TanStack Router beforeLoad hook)
|
|
36
|
+
* async function beforeLoad({ location }) {
|
|
37
|
+
* const decision = await shouldRedirect({ url: location.href });
|
|
38
|
+
*
|
|
39
|
+
* if (decision.shouldRedirect) {
|
|
40
|
+
* throw redirect({ to: decision.redirectUrl.href });
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* // Server side usage with a Request
|
|
46
|
+
* export async function handle(request) {
|
|
47
|
+
* const decision = await shouldRedirect({ request });
|
|
48
|
+
*
|
|
49
|
+
* if (decision.shouldRedirect) {
|
|
50
|
+
* return Response.redirect(decision.redirectUrl, 307);
|
|
51
|
+
* }
|
|
52
|
+
*
|
|
53
|
+
* return render(request, decision.locale);
|
|
54
|
+
* }
|
|
55
|
+
*
|
|
56
|
+
* @param {ShouldRedirectInput} [input]
|
|
57
|
+
* @returns {Promise<ShouldRedirectResult>}
|
|
58
|
+
*/
|
|
59
|
+
export async function shouldRedirect(input = {}) {
|
|
60
|
+
const locale = /** @type {ReturnType<typeof assertIsLocale>} */ (await resolveLocale(input));
|
|
61
|
+
if (!strategy.includes("url")) {
|
|
62
|
+
return { shouldRedirect: false, locale, redirectUrl: undefined };
|
|
63
|
+
}
|
|
64
|
+
const currentUrl = resolveUrl(input);
|
|
65
|
+
const localizedUrl = localizeUrl(currentUrl.href, { locale });
|
|
66
|
+
const shouldRedirectToLocalizedUrl = normalizeUrl(localizedUrl.href) !== normalizeUrl(currentUrl.href);
|
|
67
|
+
return {
|
|
68
|
+
shouldRedirect: shouldRedirectToLocalizedUrl,
|
|
69
|
+
locale,
|
|
70
|
+
redirectUrl: shouldRedirectToLocalizedUrl ? localizedUrl : undefined,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Resolves the locale either from the provided input or by using the configured strategies.
|
|
75
|
+
*
|
|
76
|
+
* @param {ShouldRedirectInput} input
|
|
77
|
+
* @returns {Promise<ReturnType<typeof assertIsLocale>>}
|
|
78
|
+
*/
|
|
79
|
+
async function resolveLocale(input) {
|
|
80
|
+
if (input.locale) {
|
|
81
|
+
return assertIsLocale(input.locale);
|
|
82
|
+
}
|
|
83
|
+
if (input.request) {
|
|
84
|
+
return extractLocaleFromRequestAsync(input.request);
|
|
85
|
+
}
|
|
86
|
+
return getLocale();
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Resolves the current URL from the provided input or runtime context.
|
|
90
|
+
*
|
|
91
|
+
* @param {ShouldRedirectInput} input
|
|
92
|
+
* @returns {URL}
|
|
93
|
+
*/
|
|
94
|
+
function resolveUrl(input) {
|
|
95
|
+
if (input.request) {
|
|
96
|
+
return new URL(input.request.url);
|
|
97
|
+
}
|
|
98
|
+
if (input.url instanceof URL) {
|
|
99
|
+
return new URL(input.url.href);
|
|
100
|
+
}
|
|
101
|
+
if (typeof input.url === "string") {
|
|
102
|
+
return new URL(input.url, getUrlOrigin());
|
|
103
|
+
}
|
|
104
|
+
if (typeof window !== "undefined" && window?.location?.href) {
|
|
105
|
+
return new URL(window.location.href);
|
|
106
|
+
}
|
|
107
|
+
throw new Error("shouldRedirect() requires either a request, an absolute URL, or must run in a browser environment.");
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Normalize url for comparison by stripping the trailing slash.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} url
|
|
113
|
+
* @returns {string}
|
|
114
|
+
*/
|
|
115
|
+
function normalizeUrl(url) {
|
|
116
|
+
const urlObj = new URL(url);
|
|
117
|
+
urlObj.pathname = urlObj.pathname.replace(/\/$/, "");
|
|
118
|
+
return urlObj.href;
|
|
119
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"should-redirect.test.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/should-redirect.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { createParaglide } from "../create-paraglide.js";
|
|
3
|
+
import { newProject } from "@inlang/sdk";
|
|
4
|
+
test("shouldRedirect redirects to the strategy-preferred locale on the server", async () => {
|
|
5
|
+
const runtime = await createParaglide({
|
|
6
|
+
blob: await newProject({
|
|
7
|
+
settings: {
|
|
8
|
+
baseLocale: "en",
|
|
9
|
+
locales: ["en", "fr"],
|
|
10
|
+
},
|
|
11
|
+
}),
|
|
12
|
+
strategy: ["cookie", "url"],
|
|
13
|
+
cookieName: "PARAGLIDE_LOCALE",
|
|
14
|
+
urlPatterns: [
|
|
15
|
+
{
|
|
16
|
+
pattern: "https://example.com/:path(.*)?",
|
|
17
|
+
localized: [
|
|
18
|
+
["en", "https://example.com/en/:path(.*)?"],
|
|
19
|
+
["fr", "https://example.com/fr/:path(.*)?"],
|
|
20
|
+
],
|
|
21
|
+
},
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
const request = new Request("https://example.com/en/dashboard", {
|
|
25
|
+
headers: {
|
|
26
|
+
cookie: "PARAGLIDE_LOCALE=fr",
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
const decision = await runtime.shouldRedirect({ request });
|
|
30
|
+
expect(decision.shouldRedirect).toBe(true);
|
|
31
|
+
expect(decision.redirectUrl?.href).toBe("https://example.com/fr/dashboard");
|
|
32
|
+
expect(decision.locale).toBe("fr");
|
|
33
|
+
});
|
|
34
|
+
test("shouldRedirect does nothing when the URL already matches", async () => {
|
|
35
|
+
const runtime = await createParaglide({
|
|
36
|
+
blob: await newProject({
|
|
37
|
+
settings: {
|
|
38
|
+
baseLocale: "en",
|
|
39
|
+
locales: ["en", "fr"],
|
|
40
|
+
},
|
|
41
|
+
}),
|
|
42
|
+
strategy: ["cookie", "url"],
|
|
43
|
+
cookieName: "PARAGLIDE_LOCALE",
|
|
44
|
+
urlPatterns: [
|
|
45
|
+
{
|
|
46
|
+
pattern: "https://example.com/:path(.*)?",
|
|
47
|
+
localized: [
|
|
48
|
+
["en", "https://example.com/en/:path(.*)?"],
|
|
49
|
+
["fr", "https://example.com/fr/:path(.*)?"],
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
});
|
|
54
|
+
const request = new Request("https://example.com/fr/dashboard", {
|
|
55
|
+
headers: {
|
|
56
|
+
cookie: "PARAGLIDE_LOCALE=fr",
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
const decision = await runtime.shouldRedirect({ request });
|
|
60
|
+
expect(decision.shouldRedirect).toBe(false);
|
|
61
|
+
expect(decision.redirectUrl).toBeUndefined();
|
|
62
|
+
expect(decision.locale).toBe("fr");
|
|
63
|
+
});
|
|
64
|
+
test("shouldRedirect falls back to the browser URL when no input is provided", async () => {
|
|
65
|
+
const runtime = await createParaglide({
|
|
66
|
+
blob: await newProject({
|
|
67
|
+
settings: {
|
|
68
|
+
baseLocale: "en",
|
|
69
|
+
locales: ["en", "de"],
|
|
70
|
+
},
|
|
71
|
+
}),
|
|
72
|
+
strategy: ["url", "globalVariable"],
|
|
73
|
+
isServer: "false",
|
|
74
|
+
urlPatterns: undefined,
|
|
75
|
+
});
|
|
76
|
+
const originalWindow = globalThis.window;
|
|
77
|
+
try {
|
|
78
|
+
globalThis.window = {
|
|
79
|
+
location: {
|
|
80
|
+
href: "https://example.com/en/profile",
|
|
81
|
+
origin: "https://example.com",
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
runtime.overwriteGetLocale(() => "de");
|
|
85
|
+
const decision = await runtime.shouldRedirect();
|
|
86
|
+
expect(decision.shouldRedirect).toBe(true);
|
|
87
|
+
expect(decision.redirectUrl?.href).toBe("https://example.com/de/profile");
|
|
88
|
+
expect(decision.locale).toBe("de");
|
|
89
|
+
}
|
|
90
|
+
finally {
|
|
91
|
+
if (originalWindow === undefined) {
|
|
92
|
+
Reflect.deleteProperty(globalThis, "window");
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
globalThis.window = originalWindow;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
test("shouldRedirect never suggests a redirect without the url strategy", async () => {
|
|
100
|
+
const runtime = await createParaglide({
|
|
101
|
+
blob: await newProject({
|
|
102
|
+
settings: {
|
|
103
|
+
baseLocale: "en",
|
|
104
|
+
locales: ["en", "fr"],
|
|
105
|
+
},
|
|
106
|
+
}),
|
|
107
|
+
strategy: ["cookie"],
|
|
108
|
+
cookieName: "PARAGLIDE_LOCALE",
|
|
109
|
+
});
|
|
110
|
+
const request = new Request("https://example.com/en/dashboard", {
|
|
111
|
+
headers: {
|
|
112
|
+
cookie: "PARAGLIDE_LOCALE=fr",
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
const decision = await runtime.shouldRedirect({ request });
|
|
116
|
+
expect(decision.shouldRedirect).toBe(false);
|
|
117
|
+
expect(decision.redirectUrl).toBeUndefined();
|
|
118
|
+
expect(decision.locale).toBe("fr");
|
|
119
|
+
});
|
|
@@ -25,6 +25,7 @@ export type Runtime = {
|
|
|
25
25
|
deLocalizeHref: typeof import("./localize-href.js").deLocalizeHref;
|
|
26
26
|
localizeUrl: typeof import("./localize-url.js").localizeUrl;
|
|
27
27
|
deLocalizeUrl: typeof import("./localize-url.js").deLocalizeUrl;
|
|
28
|
+
shouldRedirect: typeof import("./should-redirect.js").shouldRedirect;
|
|
28
29
|
extractLocaleFromUrl: typeof import("./extract-locale-from-url.js").extractLocaleFromUrl;
|
|
29
30
|
extractLocaleFromRequest: typeof import("./extract-locale-from-request.js").extractLocaleFromRequest;
|
|
30
31
|
extractLocaleFromRequestAsync: typeof import("./extract-locale-from-request-async.js").extractLocaleFromRequestAsync;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/type.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG;IACrB,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,OAAO,EAAE,cAAc,gBAAgB,EAAE,OAAO,CAAC;IACjD,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,YAAY,EAAE,cAAc,gBAAgB,EAAE,YAAY,CAAC;IAC3D,WAAW,EAAE,cAAc,gBAAgB,EAAE,WAAW,CAAC;IACzD,wBAAwB,EAAE,cAAc,gBAAgB,EAAE,wBAAwB,CAAC;IACnF,uBAAuB,EAAE,cAAc,gBAAgB,EAAE,uBAAuB,CAAC;IACjF,qCAAqC,EAAE,cAAc,gBAAgB,EAAE,qCAAqC,CAAC;IAC7G,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,YAAY,EAAE,cAAc,qBAAqB,EAAE,YAAY,CAAC;IAChE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,qBAAqB,EAAE,cAAc,qBAAqB,EAAE,qBAAqB,CAAC;IAClF,gCAAgC,EAAE,cAAc,gBAAgB,EAAE,gCAAgC,CAAC;IACnG,cAAc,EAAE,cAAc,uBAAuB,EAAE,cAAc,CAAC;IACtE,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,YAAY,EAAE,cAAc,oBAAoB,EAAE,YAAY,CAAC;IAC/D,cAAc,EAAE,cAAc,oBAAoB,EAAE,cAAc,CAAC;IACnE,WAAW,EAAE,cAAc,mBAAmB,EAAE,WAAW,CAAC;IAC5D,aAAa,EAAE,cAAc,mBAAmB,EAAE,aAAa,CAAC;IAChE,oBAAoB,EAAE,cAAc,8BAA8B,EAAE,oBAAoB,CAAC;IACzF,wBAAwB,EAAE,cAAc,kCAAkC,EAAE,wBAAwB,CAAC;IACrG,6BAA6B,EAAE,cAAc,wCAAwC,EAAE,6BAA6B,CAAC;IACrH,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,0BAA0B,EAAE,cAAc,oCAAoC,EAAE,0BAA0B,CAAC;IAC3G,2BAA2B,EAAE,cAAc,qCAAqC,EAAE,2BAA2B,CAAC;IAC9G,gBAAgB,EAAE,cAAc,yBAAyB,EAAE,gBAAgB,CAAC;IAC5E,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;IACtF,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;CACtF,CAAC"}
|
|
1
|
+
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/type.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,MAAM,MAAM,OAAO,GAAG;IACrB,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,OAAO,EAAE,cAAc,gBAAgB,EAAE,OAAO,CAAC;IACjD,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,UAAU,EAAE,cAAc,gBAAgB,EAAE,UAAU,CAAC;IACvD,YAAY,EAAE,cAAc,gBAAgB,EAAE,YAAY,CAAC;IAC3D,WAAW,EAAE,cAAc,gBAAgB,EAAE,WAAW,CAAC;IACzD,wBAAwB,EAAE,cAAc,gBAAgB,EAAE,wBAAwB,CAAC;IACnF,uBAAuB,EAAE,cAAc,gBAAgB,EAAE,uBAAuB,CAAC;IACjF,qCAAqC,EAAE,cAAc,gBAAgB,EAAE,qCAAqC,CAAC;IAC7G,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,SAAS,EAAE,cAAc,iBAAiB,EAAE,SAAS,CAAC;IACtD,YAAY,EAAE,cAAc,qBAAqB,EAAE,YAAY,CAAC;IAChE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,kBAAkB,EAAE,cAAc,iBAAiB,EAAE,kBAAkB,CAAC;IACxE,qBAAqB,EAAE,cAAc,qBAAqB,EAAE,qBAAqB,CAAC;IAClF,gCAAgC,EAAE,cAAc,gBAAgB,EAAE,gCAAgC,CAAC;IACnG,cAAc,EAAE,cAAc,uBAAuB,EAAE,cAAc,CAAC;IACtE,QAAQ,EAAE,cAAc,gBAAgB,EAAE,QAAQ,CAAC;IACnD,YAAY,EAAE,cAAc,oBAAoB,EAAE,YAAY,CAAC;IAC/D,cAAc,EAAE,cAAc,oBAAoB,EAAE,cAAc,CAAC;IACnE,WAAW,EAAE,cAAc,mBAAmB,EAAE,WAAW,CAAC;IAC5D,aAAa,EAAE,cAAc,mBAAmB,EAAE,aAAa,CAAC;IAChE,cAAc,EAAE,cAAc,sBAAsB,EAAE,cAAc,CAAC;IACrE,oBAAoB,EAAE,cAAc,8BAA8B,EAAE,oBAAoB,CAAC;IACzF,wBAAwB,EAAE,cAAc,kCAAkC,EAAE,wBAAwB,CAAC;IACrG,6BAA6B,EAAE,cAAc,wCAAwC,EAAE,6BAA6B,CAAC;IACrH,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,uBAAuB,EAAE,cAAc,iCAAiC,EAAE,uBAAuB,CAAC;IAClG,0BAA0B,EAAE,cAAc,oCAAoC,EAAE,0BAA0B,CAAC;IAC3G,2BAA2B,EAAE,cAAc,qCAAqC,EAAE,2BAA2B,CAAC;IAC9G,gBAAgB,EAAE,cAAc,yBAAyB,EAAE,gBAAgB,CAAC;IAC5E,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;IACtF,0BAA0B,EAAE,cAAc,eAAe,EAAE,0BAA0B,CAAC;CACtF,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/compiler/server/middleware.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,oCA9Ca,CAAC,WAEH,OAAO,WACP,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,cAAc,EAAE,MAAM,CAAA;CAAE,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,cACrF;IAAE,UAAU,EAAC,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE,GACzC,OAAO,CAAC,QAAQ,CAAC,
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../../../src/compiler/server/middleware.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4DG;AACH,oCA9Ca,CAAC,WAEH,OAAO,WACP,CAAC,IAAI,EAAE;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,MAAM,EAAE,OAAO,cAAc,EAAE,MAAM,CAAA;CAAE,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,cACrF;IAAE,UAAU,EAAC,CAAC,QAAQ,EAAE,QAAQ,KAAK,IAAI,CAAA;CAAE,GACzC,OAAO,CAAC,QAAQ,CAAC,CAyI7B"}
|
|
@@ -68,30 +68,29 @@ export async function paraglideMiddleware(request, resolve, callbacks) {
|
|
|
68
68
|
else if (!runtime.serverAsyncLocalStorage) {
|
|
69
69
|
runtime.overwriteServerAsyncLocalStorage(createMockAsyncLocalStorage());
|
|
70
70
|
}
|
|
71
|
-
const
|
|
71
|
+
const decision = await runtime.shouldRedirect({ request });
|
|
72
|
+
const locale = decision.locale;
|
|
72
73
|
const origin = new URL(request.url).origin;
|
|
73
74
|
// if the client makes a request to a URL that doesn't match
|
|
74
75
|
// the localizedUrl, redirect the client to the localized URL
|
|
75
76
|
if (request.headers.get("Sec-Fetch-Dest") === "document" &&
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
headers["Vary"] = "Accept-Language";
|
|
84
|
-
}
|
|
85
|
-
const response = new Response(null, {
|
|
86
|
-
status: 307,
|
|
87
|
-
headers: {
|
|
88
|
-
Location: localizedUrl.href,
|
|
89
|
-
...headers,
|
|
90
|
-
},
|
|
91
|
-
});
|
|
92
|
-
callbacks?.onRedirect(response);
|
|
93
|
-
return response;
|
|
77
|
+
decision.shouldRedirect &&
|
|
78
|
+
decision.redirectUrl) {
|
|
79
|
+
// Create headers object with Vary header if preferredLanguage strategy is used
|
|
80
|
+
/** @type {Record<string, string>} */
|
|
81
|
+
const headers = {};
|
|
82
|
+
if (runtime.strategy.includes("preferredLanguage")) {
|
|
83
|
+
headers["Vary"] = "Accept-Language";
|
|
94
84
|
}
|
|
85
|
+
const response = new Response(null, {
|
|
86
|
+
status: 307,
|
|
87
|
+
headers: {
|
|
88
|
+
Location: decision.redirectUrl.href,
|
|
89
|
+
...headers,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
callbacks?.onRedirect(response);
|
|
93
|
+
return response;
|
|
95
94
|
}
|
|
96
95
|
// If the strategy includes "url", we need to de-localize the URL
|
|
97
96
|
// before passing it to the server middleware.
|
|
@@ -135,18 +134,6 @@ export async function paraglideMiddleware(request, resolve, callbacks) {
|
|
|
135
134
|
}
|
|
136
135
|
return response;
|
|
137
136
|
}
|
|
138
|
-
/**
|
|
139
|
-
* Normalize url for comparison.
|
|
140
|
-
* Strips trailing slash
|
|
141
|
-
* @param {string} url
|
|
142
|
-
* @returns {string} normalized url string
|
|
143
|
-
*/
|
|
144
|
-
function normalizeURL(url) {
|
|
145
|
-
const urlObj = new URL(url);
|
|
146
|
-
// // strip trailing slash from pathname
|
|
147
|
-
urlObj.pathname = urlObj.pathname.replace(/\/$/, "");
|
|
148
|
-
return urlObj.href;
|
|
149
|
-
}
|
|
150
137
|
/**
|
|
151
138
|
* Creates a mock AsyncLocalStorage implementation for environments where
|
|
152
139
|
* native AsyncLocalStorage is not available or disabled.
|
|
@@ -45,8 +45,7 @@ test("should create any missing directories", async () => {
|
|
|
45
45
|
test("should only write once if the output hasn't changed", async () => {
|
|
46
46
|
const { writeOutput } = await import("./write-output.js");
|
|
47
47
|
const fs = mockFs({});
|
|
48
|
-
|
|
49
|
-
fs.writeFile = vi.spyOn(fs, "writeFile");
|
|
48
|
+
const writeFileSpy = vi.spyOn(fs, "writeFile");
|
|
50
49
|
const hashes = await writeOutput({
|
|
51
50
|
directory: "/output",
|
|
52
51
|
output: { "test.txt": "test" },
|
|
@@ -60,13 +59,12 @@ test("should only write once if the output hasn't changed", async () => {
|
|
|
60
59
|
});
|
|
61
60
|
expect(hashes).toEqual(hashes2);
|
|
62
61
|
expect(await fs.readFile("/output/test.txt", { encoding: "utf-8" })).toBe("test");
|
|
63
|
-
expect(
|
|
62
|
+
expect(writeFileSpy).toHaveBeenCalledTimes(1);
|
|
64
63
|
});
|
|
65
64
|
test("should write again if the output has changed", async () => {
|
|
66
65
|
const { writeOutput } = await import("./write-output.js");
|
|
67
66
|
const fs = mockFs({});
|
|
68
|
-
|
|
69
|
-
fs.writeFile = vi.spyOn(fs, "writeFile");
|
|
67
|
+
const writeFileSpy = vi.spyOn(fs, "writeFile");
|
|
70
68
|
const hashes = await writeOutput({
|
|
71
69
|
directory: "/output",
|
|
72
70
|
output: { "test.txt": "test" },
|
|
@@ -79,13 +77,12 @@ test("should write again if the output has changed", async () => {
|
|
|
79
77
|
previousOutputHashes: hashes,
|
|
80
78
|
});
|
|
81
79
|
expect(await fs.readFile("/output/test.txt", { encoding: "utf-8" })).toBe("test2");
|
|
82
|
-
expect(
|
|
80
|
+
expect(writeFileSpy).toHaveBeenCalledTimes(2);
|
|
83
81
|
});
|
|
84
82
|
test("should write files if output has partially changed", async () => {
|
|
85
83
|
const { writeOutput } = await import("./write-output.js");
|
|
86
84
|
const fs = mockFs({});
|
|
87
|
-
|
|
88
|
-
fs.writeFile = vi.spyOn(fs, "writeFile");
|
|
85
|
+
const writeFileSpy = vi.spyOn(fs, "writeFile");
|
|
89
86
|
const hashes = await writeOutput({
|
|
90
87
|
directory: "/output",
|
|
91
88
|
output: { "file1.txt": "test", "file2.txt": "test" },
|
|
@@ -97,8 +94,8 @@ test("should write files if output has partially changed", async () => {
|
|
|
97
94
|
fs,
|
|
98
95
|
previousOutputHashes: hashes,
|
|
99
96
|
});
|
|
100
|
-
expect(
|
|
101
|
-
expect(
|
|
97
|
+
expect(writeFileSpy).toHaveBeenCalledWith("/output/file2.txt", "test2");
|
|
98
|
+
expect(writeFileSpy).toHaveBeenCalledTimes(3);
|
|
102
99
|
});
|
|
103
100
|
test("should delete files that have been removed from the output", async () => {
|
|
104
101
|
const { writeOutput } = await import("./write-output.js");
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@inlang/paraglide-js",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.0",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public",
|
|
@@ -35,21 +35,19 @@
|
|
|
35
35
|
"@inlang/recommend-sherlock": "0.2.1"
|
|
36
36
|
},
|
|
37
37
|
"devDependencies": {
|
|
38
|
-
"@eslint/js": "^9.18.0",
|
|
39
38
|
"@rollup/plugin-virtual": "3.0.2",
|
|
40
39
|
"@ts-morph/bootstrap": "0.26.0",
|
|
41
40
|
"@types/node": "^22.10.6",
|
|
42
|
-
"@vitest/coverage-v8": "
|
|
43
|
-
"eslint": "^9.18.0",
|
|
41
|
+
"@vitest/coverage-v8": "3.1.4",
|
|
44
42
|
"memfs": "4.17.0",
|
|
43
|
+
"oxlint": "^1.14.0",
|
|
45
44
|
"prettier": "^3.4.2",
|
|
46
45
|
"rolldown": "1.0.0-beta.1",
|
|
47
46
|
"typedoc": "0.28.12",
|
|
48
47
|
"typedoc-plugin-markdown": "4.7.0",
|
|
49
48
|
"typedoc-plugin-missing-exports": "4.0.0",
|
|
50
49
|
"typescript": "5.8.3",
|
|
51
|
-
"
|
|
52
|
-
"vitest": "2.1.8",
|
|
50
|
+
"vitest": "3.1.4",
|
|
53
51
|
"@inlang/plugin-message-format": "4.0.0",
|
|
54
52
|
"@opral/tsconfig": "1.1.0"
|
|
55
53
|
},
|
|
@@ -85,7 +83,7 @@
|
|
|
85
83
|
"test": "npm run env-variables && tsc --noEmit && vitest run --coverage ./src/**/*",
|
|
86
84
|
"test:watch": "npm run env-variables && vitest --watch ./src/**/*",
|
|
87
85
|
"env-variables": "node ./src/services/env-variables/create-index-file.js",
|
|
88
|
-
"lint": "
|
|
86
|
+
"lint": "oxlint --config .oxlintrc.json --fix",
|
|
89
87
|
"format": "prettier ./src --write",
|
|
90
88
|
"clean": "rm -rf ./dist ./node_modules"
|
|
91
89
|
}
|