@inlang/paraglide-js 2.0.1 → 2.0.3

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.
@@ -95,3 +95,54 @@ test("compiles bundles with arbitrary module identifiers", async () => {
95
95
  });
96
96
  expect(result.bundle.code).includes(`export { ${toSafeModuleId("$p@44🍌")} as "$p@44🍌" }`);
97
97
  });
98
+ test("handles message pattern with duplicate variable references", async () => {
99
+ const mockBundle = {
100
+ id: "date_last_days",
101
+ declarations: [
102
+ { type: "input-variable", name: "days" },
103
+ { type: "input-variable", name: "days" },
104
+ ],
105
+ messages: [
106
+ {
107
+ id: "date_last_days",
108
+ bundleId: "date_last_days",
109
+ locale: "en",
110
+ selectors: [],
111
+ variants: [
112
+ {
113
+ id: "1",
114
+ messageId: "date_last_days",
115
+ matches: [],
116
+ pattern: [
117
+ { type: "text", value: "Last " },
118
+ {
119
+ type: "expression",
120
+ arg: { type: "variable-reference", name: "days" },
121
+ },
122
+ { type: "text", value: " days, showing " },
123
+ {
124
+ type: "expression",
125
+ arg: { type: "variable-reference", name: "days" },
126
+ },
127
+ { type: "text", value: " items" },
128
+ ],
129
+ },
130
+ ],
131
+ },
132
+ ],
133
+ };
134
+ const result = compileBundle({
135
+ fallbackMap: {
136
+ en: "en",
137
+ },
138
+ bundle: mockBundle,
139
+ messageReferenceExpression: (locale) => `${toSafeModuleId(locale)}.date_last_days`,
140
+ });
141
+ // The JSDoc should not have duplicate parameters
142
+ expect(result.bundle.code).toContain("@param {{ days: NonNullable<unknown> }} inputs");
143
+ expect(result.bundle.code).not.toContain("days: NonNullable<unknown>, days: NonNullable<unknown>");
144
+ // Check that the pattern is compiled correctly
145
+ const enMessage = result.messages?.en;
146
+ expect(enMessage).toBeDefined();
147
+ expect(enMessage?.code).toContain("Last ${i.days} days, showing ${i.days} items");
148
+ });
@@ -69,7 +69,7 @@ test("compiles a message with variants", async () => {
69
69
  ];
70
70
  const compiled = compileMessage(declarations, message, variants);
71
71
  const { some_message } = await import("data:text/javascript;base64," +
72
- btoa("export const some_message =" + compiled.code));
72
+ btoa("export const some_message = " + compiled.code));
73
73
  expect(some_message({ fistInput: 1, secondInput: 2 })).toBe("The inputs are 1 and 2");
74
74
  expect(some_message({ fistInput: 3, secondInput: 4 })).toBe("Catch all");
75
75
  expect(some_message({ fistInput: 1, secondInput: 5 })).toBe("Catch all");
@@ -1 +1 @@
1
- {"version":3,"file":"jsdoc-types.d.ts","sourceRoot":"","sources":["../../src/compiler/jsdoc-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC9C,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;CAClB,GAAG,MAAM,CAOT;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAU1D"}
1
+ {"version":3,"file":"jsdoc-types.d.ts","sourceRoot":"","sources":["../../src/compiler/jsdoc-types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAEjD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC9C,MAAM,EAAE,aAAa,EAAE,CAAC;IACxB,OAAO,EAAE,MAAM,EAAE,CAAC;CAClB,GAAG,MAAM,CAOT;AAED;;;;;;;GAOG;AACH,wBAAgB,UAAU,CAAC,MAAM,EAAE,aAAa,EAAE,GAAG,MAAM,CAoB1D"}
@@ -17,7 +17,13 @@ export function inputsType(inputs) {
17
17
  if (inputs.length === 0) {
18
18
  return "{}";
19
19
  }
20
- const inputParams = inputs
20
+ // Deduplicate inputs by name to avoid TypeScript errors with duplicate properties in JSDoc
21
+ const uniqueInputMap = new Map();
22
+ for (const input of inputs) {
23
+ uniqueInputMap.set(input.name, input);
24
+ }
25
+ const uniqueInputs = Array.from(uniqueInputMap.values());
26
+ const inputParams = uniqueInputs
21
27
  .map((input) => {
22
28
  return `${input.name}: NonNullable<unknown>`;
23
29
  })
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=jsdoc-types.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jsdoc-types.test.d.ts","sourceRoot":"","sources":["../../src/compiler/jsdoc-types.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,26 @@
1
+ import { test, expect } from "vitest";
2
+ import { inputsType, jsDocBundleFunctionTypes } from "./jsdoc-types.js";
3
+ test("inputsType generates unique parameter types even when the same input appears multiple times", () => {
4
+ // Simulate a case where the same input appears multiple times (like in a translation with placeholders)
5
+ const inputs = [
6
+ { name: "days", type: "input-variable" },
7
+ { name: "days", type: "input-variable" }, // Duplicated input
8
+ ];
9
+ // The generated input type should only include each parameter once
10
+ const result = inputsType(inputs);
11
+ expect(result).toBe("{ days: NonNullable<unknown> }");
12
+ // It should not generate a duplicate parameter
13
+ expect(result).not.toBe("{ days: NonNullable<unknown>, days: NonNullable<unknown> }");
14
+ });
15
+ test("jsDocBundleFunctionTypes correctly handles messages with duplicate inputs", () => {
16
+ const inputs = [
17
+ { name: "days", type: "input-variable" },
18
+ { name: "days", type: "input-variable" }, // Duplicated input
19
+ ];
20
+ const locales = ["en-us", "de-de"];
21
+ const result = jsDocBundleFunctionTypes({ inputs, locales });
22
+ // The JSDoc should only include each parameter once
23
+ expect(result).toContain("@param {{ days: NonNullable<unknown> }} inputs");
24
+ // It should not contain duplicated parameters
25
+ expect(result).not.toContain("@param {{ days: NonNullable<unknown>, days: NonNullable<unknown> }} inputs");
26
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"get-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/get-locale.js"],"names":[],"mappings":"AA+BA;;;;;;;;;;;GAWG;AACH,sBAFU,MAAM,MAAM,CAyDpB;AA4BF;;;;;;;;;;;;;;GAcG;AACH,iCAFU,CAAC,EAAE,EAAE,MAAM,MAAM,KAAK,IAAI,CAIlC"}
1
+ {"version":3,"file":"get-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/get-locale.js"],"names":[],"mappings":"AA+BA;;;;;;;;;;;GAWG;AACH,sBAFU,MAAM,MAAM,CA8DpB;AA4BF;;;;;;;;;;;;;;GAcG;AACH,iCAFU,CAAC,EAAE,EAAE,MAAM,MAAM,KAAK,IAAI,CAIlC"}
@@ -45,7 +45,10 @@ export let getLocale = () => {
45
45
  else if (strat === "baseLocale") {
46
46
  locale = baseLocale;
47
47
  }
48
- else if (TREE_SHAKE_URL_STRATEGY_USED && strat === "url" && !isServer && typeof window !== 'undefined') {
48
+ else if (TREE_SHAKE_URL_STRATEGY_USED &&
49
+ strat === "url" &&
50
+ !isServer &&
51
+ typeof window !== "undefined") {
49
52
  locale = extractLocaleFromUrl(window.location.href);
50
53
  }
51
54
  else if (TREE_SHAKE_GLOBAL_VARIABLE_STRATEGY_USED &&
@@ -41,7 +41,7 @@ export let setLocale = (newLocale, options) => {
41
41
  _locale = newLocale;
42
42
  }
43
43
  else if (TREE_SHAKE_COOKIE_STRATEGY_USED && strat === "cookie") {
44
- if (isServer || typeof document === 'undefined') {
44
+ if (isServer || typeof document === "undefined") {
45
45
  continue;
46
46
  }
47
47
  // set the cookie
@@ -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,GAEnF,OAAO,CAAC,QAAQ,CAAC,CA2I7B"}
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,GAEnF,OAAO,CAAC,QAAQ,CAAC,CA0H7B"}
@@ -61,79 +61,65 @@ import * as runtime from "./runtime.js";
61
61
  * ```
62
62
  */
63
63
  export async function paraglideMiddleware(request, resolve) {
64
- try {
65
- if (!runtime.disableAsyncLocalStorage && !runtime.serverAsyncLocalStorage) {
66
- const { AsyncLocalStorage } = await import("async_hooks");
67
- runtime.overwriteServerAsyncLocalStorage(new AsyncLocalStorage());
68
- }
69
- else if (!runtime.serverAsyncLocalStorage) {
70
- runtime.overwriteServerAsyncLocalStorage(createMockAsyncLocalStorage());
71
- }
72
- const locale = runtime.extractLocaleFromRequest(request);
73
- const origin = new URL(request.url).origin;
74
- // if the client makes a request to a URL that doesn't match
75
- // the localizedUrl, redirect the client to the localized URL
76
- if (request.headers.get("Sec-Fetch-Dest") === "document" &&
77
- runtime.strategy.includes("url")) {
78
- const localizedUrl = runtime.localizeUrl(request.url, { locale });
79
- if (normalizeURL(localizedUrl.href) !== normalizeURL(request.url)) {
80
- return Response.redirect(localizedUrl, 307);
81
- }
82
- }
83
- // If the strategy includes "url", we need to de-localize the URL
84
- // before passing it to the server middleware.
85
- //
86
- // The middleware is responsible for mapping a localized URL to the
87
- // de-localized URL e.g. `/en/about` to `/about`. Otherwise,
88
- // the server can't render the correct page.
89
- const newRequest = runtime.strategy.includes("url")
90
- ? new Request(runtime.deLocalizeUrl(request.url), request)
91
- : // need to create a new request object because some metaframeworks (nextjs!) throw otherwise
92
- // https://github.com/opral/inlang-paraglide-js/issues/411
93
- new Request(request);
94
- // the message functions that have been called in this request
95
- /** @type {Set<string>} */
96
- const messageCalls = new Set();
97
- const response = await runtime.serverAsyncLocalStorage?.run({ locale, origin, messageCalls }, () => resolve({ locale, request: newRequest }));
98
- // Only modify HTML responses
99
- if (runtime.experimentalMiddlewareLocaleSplitting &&
100
- response.headers.get("Content-Type")?.includes("html")) {
101
- const body = await response.text();
102
- const messages = [];
103
- // using .values() to avoid polyfilling in older projects. else the following error is thrown
104
- // Type 'Set<string>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.
105
- for (const messageCall of Array.from(messageCalls)) {
106
- const [id, locale] =
107
- /** @type {[string, import("./runtime.js").Locale]} */ (messageCall.split(":"));
108
- messages.push(`${id}: ${compiledBundles[id]?.[locale]}`);
109
- }
110
- const script = `<script>globalThis.__paraglide_ssr = { ${messages.join(",")} }</script>`;
111
- // Insert the script before the closing head tag
112
- const newBody = body.replace("</head>", `${script}</head>`);
113
- // Create a new response with the modified body
114
- // Clone all headers except Content-Length which will be set automatically
115
- const newHeaders = new Headers(response.headers);
116
- newHeaders.delete("Content-Length"); // Let the browser calculate the correct length
117
- return new Response(newBody, {
118
- status: response.status,
119
- statusText: response.statusText,
120
- headers: newHeaders,
121
- });
64
+ if (!runtime.disableAsyncLocalStorage && !runtime.serverAsyncLocalStorage) {
65
+ const { AsyncLocalStorage } = await import("async_hooks");
66
+ runtime.overwriteServerAsyncLocalStorage(new AsyncLocalStorage());
67
+ }
68
+ else if (!runtime.serverAsyncLocalStorage) {
69
+ runtime.overwriteServerAsyncLocalStorage(createMockAsyncLocalStorage());
70
+ }
71
+ const locale = runtime.extractLocaleFromRequest(request);
72
+ const origin = new URL(request.url).origin;
73
+ // if the client makes a request to a URL that doesn't match
74
+ // the localizedUrl, redirect the client to the localized URL
75
+ if (request.headers.get("Sec-Fetch-Dest") === "document" &&
76
+ runtime.strategy.includes("url")) {
77
+ const localizedUrl = runtime.localizeUrl(request.url, { locale });
78
+ if (normalizeURL(localizedUrl.href) !== normalizeURL(request.url)) {
79
+ return Response.redirect(localizedUrl, 307);
122
80
  }
123
- return response;
124
81
  }
125
- catch (error) {
126
- // Log the error with stack trace to help with debugging
127
- console.error("[Paraglide Middleware Error]", error);
128
- // Return a helpful error response
129
- // Use text/plain to ensure the error is readable in the browser
130
- return new Response(`Paraglide middleware error: ${ /** @type {any} */(error)?.message}\n`, {
131
- status: 500,
132
- headers: {
133
- "Content-Type": "text/plain; charset=utf-8",
134
- },
82
+ // If the strategy includes "url", we need to de-localize the URL
83
+ // before passing it to the server middleware.
84
+ //
85
+ // The middleware is responsible for mapping a localized URL to the
86
+ // de-localized URL e.g. `/en/about` to `/about`. Otherwise,
87
+ // the server can't render the correct page.
88
+ const newRequest = runtime.strategy.includes("url")
89
+ ? new Request(runtime.deLocalizeUrl(request.url), request)
90
+ : // need to create a new request object because some metaframeworks (nextjs!) throw otherwise
91
+ // https://github.com/opral/inlang-paraglide-js/issues/411
92
+ new Request(request);
93
+ // the message functions that have been called in this request
94
+ /** @type {Set<string>} */
95
+ const messageCalls = new Set();
96
+ const response = await runtime.serverAsyncLocalStorage?.run({ locale, origin, messageCalls }, () => resolve({ locale, request: newRequest }));
97
+ // Only modify HTML responses
98
+ if (runtime.experimentalMiddlewareLocaleSplitting &&
99
+ response.headers.get("Content-Type")?.includes("html")) {
100
+ const body = await response.text();
101
+ const messages = [];
102
+ // using .values() to avoid polyfilling in older projects. else the following error is thrown
103
+ // Type 'Set<string>' can only be iterated through when using the '--downlevelIteration' flag or with a '--target' of 'es2015' or higher.
104
+ for (const messageCall of Array.from(messageCalls)) {
105
+ const [id, locale] =
106
+ /** @type {[string, import("./runtime.js").Locale]} */ (messageCall.split(":"));
107
+ messages.push(`${id}: ${compiledBundles[id]?.[locale]}`);
108
+ }
109
+ const script = `<script>globalThis.__paraglide_ssr = { ${messages.join(",")} }</script>`;
110
+ // Insert the script before the closing head tag
111
+ const newBody = body.replace("</head>", `${script}</head>`);
112
+ // Create a new response with the modified body
113
+ // Clone all headers except Content-Length which will be set automatically
114
+ const newHeaders = new Headers(response.headers);
115
+ newHeaders.delete("Content-Length"); // Let the browser calculate the correct length
116
+ return new Response(newBody, {
117
+ status: response.status,
118
+ statusText: response.statusText,
119
+ headers: newHeaders,
135
120
  });
136
121
  }
122
+ return response;
137
123
  }
138
124
  /**
139
125
  * Normalize url for comparison.
@@ -433,3 +433,22 @@ test("only redirects if the request.headers.get('Sec-Fetch-Dest') === 'document'
433
433
  // Middleware should be called since no redirect for API requests
434
434
  expect(apiMiddlewareResolveWasCalled).toBe(true);
435
435
  });
436
+ // https://github.com/opral/inlang-paraglide-js/issues/477
437
+ test("does not catch errors thrown by downstream resolve call", async () => {
438
+ const runtime = await createParaglide({
439
+ project: await newProject({
440
+ settings: {
441
+ baseLocale: "en",
442
+ locales: ["en"],
443
+ },
444
+ }),
445
+ compilerOptions: {
446
+ strategy: ["url"],
447
+ },
448
+ });
449
+ await expect(() => runtime.paraglideMiddleware(new Request(new URL("https://example.com/page"), {
450
+ headers: { "Sec-Fetch-Dest": "document" },
451
+ }), () => {
452
+ throw new Error("Downstream error");
453
+ })).rejects.toThrow();
454
+ });
@@ -1,5 +1,5 @@
1
1
  export const ENV_VARIABLES = {
2
2
  PARJS_APP_ID: "library.inlang.paraglideJs",
3
3
  PARJS_POSTHOG_TOKEN: "phc_m5yJZCxjOGxF8CJvP5sQ3H0d76xpnLrsmiZHduT4jDz",
4
- PARJS_PACKAGE_VERSION: "2.0.1",
4
+ PARJS_PACKAGE_VERSION: "2.0.3",
5
5
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@inlang/paraglide-js",
3
3
  "type": "module",
4
- "version": "2.0.1",
4
+ "version": "2.0.3",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public",
@@ -20,6 +20,7 @@
20
20
  "./dist",
21
21
  "./bin"
22
22
  ],
23
+ "types": "./dist/index.d.ts",
23
24
  "exports": {
24
25
  ".": "./dist/index.js",
25
26
  "./urlpattern-polyfill": "./dist/urlpattern-polyfill/index.js"
@@ -49,7 +50,7 @@
49
50
  "typescript": "^5.7.3",
50
51
  "typescript-eslint": "^8.20.0",
51
52
  "vitest": "2.1.8",
52
- "@inlang/paraglide-js": "2.0.1",
53
+ "@inlang/paraglide-js": "2.0.3",
53
54
  "@opral/tsconfig": "1.1.0",
54
55
  "@inlang/plugin-message-format": "4.0.0"
55
56
  },