@inlang/paraglide-js 2.5.0 → 2.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/dist/cli/commands/compile/command.d.ts.map +1 -1
  2. package/dist/cli/commands/compile/command.js +3 -0
  3. package/dist/compiler/compile-bundle.js +1 -1
  4. package/dist/compiler/compile-bundle.test.js +27 -25
  5. package/dist/compiler/compile-local-variable.js +1 -1
  6. package/dist/compiler/compile-local-variable.test.js +2 -2
  7. package/dist/compiler/compile-message.js +7 -7
  8. package/dist/compiler/compile-message.test.js +147 -1
  9. package/dist/compiler/compile-pattern.d.ts +1 -1
  10. package/dist/compiler/compile-pattern.js +2 -2
  11. package/dist/compiler/compile-pattern.test.js +1 -1
  12. package/dist/compiler/compile-project.d.ts.map +1 -1
  13. package/dist/compiler/compile-project.js +5 -0
  14. package/dist/compiler/compile-project.test.js +12 -0
  15. package/dist/compiler/compiler-options.d.ts +24 -0
  16. package/dist/compiler/compiler-options.d.ts.map +1 -1
  17. package/dist/compiler/compiler-options.js +1 -0
  18. package/dist/compiler/emit-ts-declarations.d.ts +12 -0
  19. package/dist/compiler/emit-ts-declarations.d.ts.map +1 -0
  20. package/dist/compiler/emit-ts-declarations.js +100 -0
  21. package/dist/compiler/index.d.ts +1 -1
  22. package/dist/compiler/index.d.ts.map +1 -1
  23. package/dist/compiler/jsdoc-types.js +1 -1
  24. package/dist/compiler/jsdoc-types.test.js +9 -0
  25. package/dist/compiler/output-structure/locale-modules.d.ts.map +1 -1
  26. package/dist/compiler/output-structure/locale-modules.js +8 -1
  27. package/dist/compiler/output-structure/message-modules.d.ts.map +1 -1
  28. package/dist/compiler/output-structure/message-modules.js +10 -7
  29. package/dist/compiler/runtime/create-runtime.d.ts.map +1 -1
  30. package/dist/compiler/runtime/create-runtime.js +42 -0
  31. package/dist/compiler/types.d.ts +18 -2
  32. package/dist/compiler/types.d.ts.map +1 -1
  33. package/dist/services/env-variables/index.js +1 -1
  34. package/package.json +3 -3
@@ -1 +1 @@
1
- {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/compile/command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,eAAO,MAAM,cAAc,SAmDzB,CAAC"}
1
+ {"version":3,"file":"command.d.ts","sourceRoot":"","sources":["../../../../src/cli/commands/compile/command.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAUpC,eAAO,MAAM,cAAc,SA4DzB,CAAC"}
@@ -16,6 +16,7 @@ export const compileCommand = new Command()
16
16
  "Read more on https://inlang.com/m/gerre34r/library-inlang-paraglideJs/strategy",
17
17
  ].join("\n"))
18
18
  .requiredOption("--silent", "Only log errors to the console", false)
19
+ .option("--emit-ts-declarations", "Emit .d.ts files for the generated output (requires the typescript package)", defaultCompilerOptions.emitTsDeclarations)
19
20
  .action(async (options) => {
20
21
  const logger = new Logger({ silent: options.silent, prefix: true });
21
22
  const path = resolve(process.cwd(), options.project);
@@ -25,6 +26,8 @@ export const compileCommand = new Command()
25
26
  project: path,
26
27
  outdir: options.outdir,
27
28
  strategy: options.strategy ?? defaultCompilerOptions.strategy,
29
+ emitTsDeclarations: options.emitTsDeclarations ??
30
+ defaultCompilerOptions.emitTsDeclarations,
28
31
  });
29
32
  }
30
33
  catch (e) {
@@ -49,7 +49,7 @@ ${isSafeBundleId ? "export " : ""}const ${safeBundleId} = (inputs${hasInputs ? "
49
49
  trackMessageCall("${safeBundleId}", locale)
50
50
  ${args.availableLocales
51
51
  .map((locale, index) => `${index > 0 ? " " : ""}${!isFullyTranslated || index < args.availableLocales.length - 1 ? `if (locale === "${locale}") ` : ""}return ${args.messageReferenceExpression(locale, args.bundle.id)}(inputs)`)
52
- .join("\n")}${!isFullyTranslated ? `\n return "${args.bundle.id}"` : ""}
52
+ .join("\n")}${!isFullyTranslated ? `\n return /** @type {LocalizedString} */ ("${args.bundle.id}")` : ""}
53
53
  };`;
54
54
  if (isSafeBundleId === false) {
55
55
  code += `\nexport { ${safeBundleId} as "${escapeForDoubleQuoteString(args.bundle.id)}" }`;
@@ -49,7 +49,7 @@ test("compiles to jsdoc", async () => {
49
49
  *
50
50
  * @param {{ age: NonNullable<unknown> }} inputs
51
51
  * @param {{ locale?: "en" | "en-US" }} options
52
- * @returns {string}
52
+ * @returns {LocalizedString}
53
53
  */
54
54
  /* @__NO_SIDE_EFFECTS__ */
55
55
  export const blue_moon_bottle = (inputs, options = {}) => {
@@ -100,29 +100,31 @@ test("compiles to jsdoc with missing translation", async () => {
100
100
  locales: ["en", "en-US", "fr"],
101
101
  },
102
102
  });
103
- expect(result.bundle.code).toMatchInlineSnapshot(`"/**
104
- * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
105
- *
106
- * - Changing this function will be over-written by the next build.
107
- *
108
- * - If you want to change the translations, you can either edit the source files e.g. \`en.json\`, or
109
- * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
110
- *
111
- * @param {{ age: NonNullable<unknown> }} inputs
112
- * @param {{ locale?: "en" | "en-US" }} options
113
- * @returns {string}
114
- */
115
- /* @__NO_SIDE_EFFECTS__ */
116
- export const blue_moon_bottle = (inputs, options = {}) => {
117
- if (experimentalMiddlewareLocaleSplitting && isServer === false) {
118
- return /** @type {any} */ (globalThis).__paraglide_ssr.blue_moon_bottle(inputs)
119
- }
120
- const locale = options.locale ?? getLocale()
121
- trackMessageCall("blue_moon_bottle", locale)
122
- if (locale === "en") return en.blue_moon_bottle(inputs)
123
- if (locale === "en-US") return en_us2.blue_moon_bottle(inputs)
124
- return "blue_moon_bottle"
125
- };"`);
103
+ expect(result.bundle.code).toMatchInlineSnapshot(`
104
+ "/**
105
+ * This function has been compiled by [Paraglide JS](https://inlang.com/m/gerre34r).
106
+ *
107
+ * - Changing this function will be over-written by the next build.
108
+ *
109
+ * - If you want to change the translations, you can either edit the source files e.g. \`en.json\`, or
110
+ * use another inlang app like [Fink](https://inlang.com/m/tdozzpar) or the [VSCode extension Sherlock](https://inlang.com/m/r7kp499g).
111
+ *
112
+ * @param {{ age: NonNullable<unknown> }} inputs
113
+ * @param {{ locale?: "en" | "en-US" }} options
114
+ * @returns {LocalizedString}
115
+ */
116
+ /* @__NO_SIDE_EFFECTS__ */
117
+ export const blue_moon_bottle = (inputs, options = {}) => {
118
+ if (experimentalMiddlewareLocaleSplitting && isServer === false) {
119
+ return /** @type {any} */ (globalThis).__paraglide_ssr.blue_moon_bottle(inputs)
120
+ }
121
+ const locale = options.locale ?? getLocale()
122
+ trackMessageCall("blue_moon_bottle", locale)
123
+ if (locale === "en") return en.blue_moon_bottle(inputs)
124
+ if (locale === "en-US") return en_us2.blue_moon_bottle(inputs)
125
+ return /** @type {LocalizedString} */ ("blue_moon_bottle")
126
+ };"
127
+ `);
126
128
  });
127
129
  // https://github.com/opral/inlang-paraglide-js/issues/285
128
130
  test("compiles bundles with arbitrary module identifiers", async () => {
@@ -208,5 +210,5 @@ test("handles message pattern with duplicate variable references", async () => {
208
210
  // Check that the pattern is compiled correctly
209
211
  const enMessage = result.messages?.en;
210
212
  expect(enMessage).toBeDefined();
211
- expect(enMessage?.code).toContain("Last ${i.days} days, showing ${i.days} items");
213
+ expect(enMessage?.code).toContain("Last ${i?.days} days, showing ${i?.days} items");
212
214
  });
@@ -33,6 +33,6 @@ function compileLiteralOrVarRef(value) {
33
33
  case "literal":
34
34
  return `"${value.value}"`;
35
35
  case "variable-reference":
36
- return `i.${value.name}`;
36
+ return `i?.${value.name}`;
37
37
  }
38
38
  }
@@ -23,7 +23,7 @@ test("compiles a variable reference local variable", () => {
23
23
  },
24
24
  },
25
25
  });
26
- expect(code).toEqual("const myVar = i.name;");
26
+ expect(code).toEqual("const myVar = i?.name;");
27
27
  });
28
28
  test("compiles a local variable with an annotation and empty options", () => {
29
29
  const code = compileLocalVariable({
@@ -67,5 +67,5 @@ test("compiles a local variable with an annotation and options", () => {
67
67
  },
68
68
  },
69
69
  });
70
- expect(code).toEqual('const myVar = registry.myFunction("en", "Hello", { option1: "value1", option2: i.varRef });');
70
+ expect(code).toEqual('const myVar = registry.myFunction("en", "Hello", { option1: "value1", option2: i?.varRef });');
71
71
  });
@@ -33,8 +33,8 @@ function compileMessageWithOneVariant(declarations, message, variants) {
33
33
  compiledLocalVariables.push(compileLocalVariable({ declaration, locale: message.locale }));
34
34
  }
35
35
  }
36
- const code = `/** @type {(inputs: ${inputsType(inputs)}) => string} */ (${hasInputs ? "i" : ""}) => {
37
- ${compiledLocalVariables.join("\n\t")}return ${compiledPattern.code}
36
+ const code = `/** @type {(inputs: ${inputsType(inputs)}) => LocalizedString} */ (${hasInputs ? "i" : ""}) => {
37
+ ${compiledLocalVariables.join("\n\t")}return /** @type {LocalizedString} */ (${compiledPattern.code})
38
38
  };`;
39
39
  return { code, node: message };
40
40
  }
@@ -54,7 +54,7 @@ function compileMessageWithMultipleVariants(declarations, message, variants) {
54
54
  });
55
55
  const isCatchAll = variant.matches.every((match) => match.type === "catchall-match");
56
56
  if (isCatchAll) {
57
- compiledVariants.push(`return ${compiledPattern.code}`);
57
+ compiledVariants.push(`return /** @type {LocalizedString} */ (${compiledPattern.code})`);
58
58
  hasCatchAll = true;
59
59
  }
60
60
  const conditions = [];
@@ -65,7 +65,7 @@ function compileMessageWithMultipleVariants(declarations, message, variants) {
65
65
  }
66
66
  const variableType = declarations.find((decl) => decl.name === match.key)?.type;
67
67
  if (variableType === "input-variable") {
68
- conditions.push(`i.${match.key} == ${doubleQuote(match.value)}`);
68
+ conditions.push(`i?.${match.key} == ${doubleQuote(match.value)}`);
69
69
  }
70
70
  else if (variableType === "local-variable") {
71
71
  conditions.push(`${match.key} == ${doubleQuote(match.value)}`);
@@ -73,7 +73,7 @@ function compileMessageWithMultipleVariants(declarations, message, variants) {
73
73
  }
74
74
  if (conditions.length === 0)
75
75
  continue;
76
- compiledVariants.push(`if (${conditions.join(" && ")}) return ${compiledPattern.code};`);
76
+ compiledVariants.push(`if (${conditions.join(" && ")}) return /** @type {LocalizedString} */ (${compiledPattern.code});`);
77
77
  }
78
78
  const compiledLocalVariables = [];
79
79
  for (const declaration of declarations) {
@@ -81,9 +81,9 @@ function compileMessageWithMultipleVariants(declarations, message, variants) {
81
81
  compiledLocalVariables.push(compileLocalVariable({ declaration, locale: message.locale }));
82
82
  }
83
83
  }
84
- const code = `/** @type {(inputs: ${inputsType(inputs)}) => string} */ (${hasInputs ? "i" : ""}) => {${compiledLocalVariables.join("\n\t")}
84
+ const code = `/** @type {(inputs: ${inputsType(inputs)}) => LocalizedString} */ (${hasInputs ? "i" : ""}) => {${compiledLocalVariables.join("\n\t")}
85
85
  ${compiledVariants.join("\n\t")}
86
- ${hasCatchAll ? "" : `return "${message.bundleId}";`}
86
+ ${hasCatchAll ? "" : `return /** @type {LocalizedString} */ ("${message.bundleId}");`}
87
87
  };`;
88
88
  return { code, node: message };
89
89
  }
@@ -142,7 +142,7 @@ test("only emits input arguments when inputs exist", async () => {
142
142
  },
143
143
  ];
144
144
  const compiled = compileMessage(declarations, message, variants);
145
- expect(compiled.code).toBe("/** @type {(inputs: {}) => string} */ () => {\n\treturn `Hello`\n};");
145
+ expect(compiled.code).toBe("/** @type {(inputs: {}) => LocalizedString} */ () => {\n\treturn /** @type {LocalizedString} */ (`Hello`)\n};");
146
146
  });
147
147
  // https://github.com/opral/inlang-paraglide-js/issues/379
148
148
  test("compiles messages that use plural()", async () => {
@@ -198,6 +198,90 @@ test("compiles messages that use plural()", async () => {
198
198
  // INTL.plural will match "other" for undefined
199
199
  expect(plural_test({ count: undefined })).toBe("There are many cats.");
200
200
  });
201
+ test("compiles messages that use plural() with ordinal type", async () => {
202
+ const declarations = [
203
+ { type: "input-variable", name: "count" },
204
+ {
205
+ type: "local-variable",
206
+ name: "countOrdinal",
207
+ value: {
208
+ arg: { type: "variable-reference", name: "count" },
209
+ annotation: {
210
+ type: "function-reference",
211
+ name: "plural",
212
+ options: [
213
+ { name: "type", value: { type: "literal", value: "ordinal" } },
214
+ ],
215
+ },
216
+ type: "expression",
217
+ },
218
+ },
219
+ ];
220
+ const message = {
221
+ locale: "en",
222
+ bundleId: "ordinal_test",
223
+ id: "message_id",
224
+ selectors: [{ type: "variable-reference", name: "countOrdinal" }],
225
+ };
226
+ const variants = [
227
+ {
228
+ id: "1",
229
+ messageId: "message_id",
230
+ matches: [{ type: "literal-match", value: "one", key: "countOrdinal" }],
231
+ pattern: [
232
+ {
233
+ type: "expression",
234
+ arg: { type: "variable-reference", name: "count" },
235
+ },
236
+ { type: "text", value: "st place" },
237
+ ],
238
+ },
239
+ {
240
+ id: "2",
241
+ messageId: "message_id",
242
+ matches: [{ type: "literal-match", value: "two", key: "countOrdinal" }],
243
+ pattern: [
244
+ {
245
+ type: "expression",
246
+ arg: { type: "variable-reference", name: "count" },
247
+ },
248
+ { type: "text", value: "nd place" },
249
+ ],
250
+ },
251
+ {
252
+ id: "3",
253
+ messageId: "message_id",
254
+ matches: [{ type: "literal-match", value: "few", key: "countOrdinal" }],
255
+ pattern: [
256
+ {
257
+ type: "expression",
258
+ arg: { type: "variable-reference", name: "count" },
259
+ },
260
+ { type: "text", value: "rd place" },
261
+ ],
262
+ },
263
+ {
264
+ id: "4",
265
+ messageId: "message_id",
266
+ matches: [{ type: "literal-match", value: "other", key: "countOrdinal" }],
267
+ pattern: [
268
+ {
269
+ type: "expression",
270
+ arg: { type: "variable-reference", name: "count" },
271
+ },
272
+ { type: "text", value: "th place" },
273
+ ],
274
+ },
275
+ ];
276
+ const compiled = compileMessage(declarations, message, variants);
277
+ const { ordinal_test } = await import("data:text/javascript;base64," +
278
+ btoa(createRegistry()) +
279
+ btoa("export const ordinal_test = " + compiled.code.replace("registry.", "")));
280
+ expect(ordinal_test({ count: 1 })).toBe("1st place");
281
+ expect(ordinal_test({ count: 2 })).toBe("2nd place");
282
+ expect(ordinal_test({ count: 3 })).toBe("3rd place");
283
+ expect(ordinal_test({ count: 4 })).toBe("4th place");
284
+ });
201
285
  test("compiles messages that use datetime()", async () => {
202
286
  const createMessage = async (locale) => {
203
287
  const declarations = [
@@ -306,3 +390,65 @@ test("compiles messages that use datetime a function with options", async () =>
306
390
  expect(enMessage({ date: "2022-03-31" })).toMatch(/Today is March \d{1,2}\./);
307
391
  expect(deMessage({ date: "2022-03-31" })).toMatch(/Today is \d{1,2}\. März\./);
308
392
  });
393
+ test("does not throw when input is omitted for a single-variant message", async () => {
394
+ const declarations = [
395
+ { type: "input-variable", name: "name" },
396
+ ];
397
+ const message = {
398
+ locale: "en",
399
+ bundleId: "greeting",
400
+ id: "greeting",
401
+ selectors: [{ type: "variable-reference", name: "name" }],
402
+ };
403
+ const variants = [
404
+ {
405
+ id: "1",
406
+ messageId: "greeting",
407
+ matches: [{ type: "catchall-match", key: "name" }],
408
+ pattern: [
409
+ { type: "text", value: "Hello " },
410
+ {
411
+ type: "expression",
412
+ arg: { type: "variable-reference", name: "name" },
413
+ },
414
+ { type: "text", value: "!" },
415
+ ],
416
+ },
417
+ ];
418
+ const compiled = compileMessage(declarations, message, variants);
419
+ const { greeting } = await import("data:text/javascript;base64," +
420
+ btoa("export const greeting = " + compiled.code));
421
+ expect(() => greeting()).not.toThrow();
422
+ expect(greeting()).toBe("Hello undefined!");
423
+ });
424
+ test("does not throw when input is omitted for multi-variant message", async () => {
425
+ const declarations = [
426
+ { type: "input-variable", name: "status" },
427
+ ];
428
+ const message = {
429
+ locale: "en",
430
+ bundleId: "status_message",
431
+ id: "status_message",
432
+ selectors: [{ type: "variable-reference", name: "status" }],
433
+ };
434
+ const variants = [
435
+ {
436
+ id: "1",
437
+ messageId: "status_message",
438
+ matches: [{ type: "literal-match", key: "status", value: "ready" }],
439
+ pattern: [{ type: "text", value: "Ready to go" }],
440
+ },
441
+ {
442
+ id: "2",
443
+ messageId: "status_message",
444
+ matches: [{ type: "catchall-match", key: "status" }],
445
+ pattern: [{ type: "text", value: "Unknown status" }],
446
+ },
447
+ ];
448
+ const compiled = compileMessage(declarations, message, variants);
449
+ const { status_message } = await import("data:text/javascript;base64," +
450
+ btoa("export const status_message = " + compiled.code));
451
+ expect(status_message({ status: "ready" })).toBe("Ready to go");
452
+ expect(() => status_message()).not.toThrow();
453
+ expect(status_message()).toBe("Unknown status");
454
+ });
@@ -11,7 +11,7 @@ import type { Compiled } from "./types.js";
11
11
  *
12
12
  * const { code } = compilePattern({ pattern, declarations: [{ type: "input-variable", name: "age" }] });
13
13
  *
14
- * // code will be: `Your age is ${i.age}`
14
+ * // code will be: `Your age is ${i?.age}`
15
15
  */
16
16
  export declare const compilePattern: (args: {
17
17
  pattern: Pattern;
@@ -10,7 +10,7 @@ import { escapeForTemplateLiteral } from "../services/codegen/escape.js";
10
10
  *
11
11
  * const { code } = compilePattern({ pattern, declarations: [{ type: "input-variable", name: "age" }] });
12
12
  *
13
- * // code will be: `Your age is ${i.age}`
13
+ * // code will be: `Your age is ${i?.age}`
14
14
  */
15
15
  export const compilePattern = (args) => {
16
16
  let result = "";
@@ -22,7 +22,7 @@ export const compilePattern = (args) => {
22
22
  if (part.arg.type === "variable-reference") {
23
23
  const declaration = args.declarations.find((decl) => decl.name === part.arg.name);
24
24
  if (declaration?.type === "input-variable") {
25
- result += `\${i.${part.arg.name}}`;
25
+ result += `\${i?.${part.arg.name}}`;
26
26
  }
27
27
  else if (declaration?.type === "local-variable") {
28
28
  result += `\${${part.arg.name}}`;
@@ -26,7 +26,7 @@ test("should compile a pattern with multiple VariableReference's", () => {
26
26
  { type: "input-variable", name: "count" },
27
27
  ],
28
28
  });
29
- expect(code).toBe("`Hello ${i.name}! You have ${i.count} messages.`");
29
+ expect(code).toBe("`Hello ${i?.name}! You have ${i?.count} messages.`");
30
30
  });
31
31
  test("should escape backticks", () => {
32
32
  const pattern = [{ type: "text", value: "`Hello world`" }];
@@ -1 +1 @@
1
- {"version":3,"file":"compile-project.d.ts","sourceRoot":"","sources":["../../src/compiler/compile-project.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAIrE,OAAO,EAEN,KAAK,eAAe,EACpB,MAAM,uBAAuB,CAAC;AAU/B;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM;IAC1C,OAAO,EAAE,aAAa,CAAC;IACvB,eAAe,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAC;CACrE,KAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAuEjC,CAAC;AAEF,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAC9C,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,GACpB,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAY1B"}
1
+ {"version":3,"file":"compile-project.d.ts","sourceRoot":"","sources":["../../src/compiler/compile-project.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAIrE,OAAO,EAEN,KAAK,eAAe,EACpB,MAAM,uBAAuB,CAAC;AAW/B;;;;;;;;;GASG;AACH,eAAO,MAAM,cAAc,GAAU,MAAM;IAC1C,OAAO,EAAE,aAAa,CAAC;IACvB,eAAe,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,IAAI,GAAG,SAAS,GAAG,QAAQ,CAAC,CAAC;CACrE,KAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4EjC,CAAC;AAEF,wBAAgB,cAAc,CAAC,CAAC,SAAS,MAAM,EAC9C,OAAO,EAAE,CAAC,EAAE,EACZ,UAAU,EAAE,OAAO,CAAC,CAAC,CAAC,GACpB,MAAM,CAAC,CAAC,EAAE,CAAC,GAAG,SAAS,CAAC,CAY1B"}
@@ -7,6 +7,7 @@ import { defaultCompilerOptions, } from "./compiler-options.js";
7
7
  import { createRuntimeFile } from "./runtime/create-runtime.js";
8
8
  import { createServerFile } from "./server/create-server-file.js";
9
9
  import { createRegistry } from "./registry.js";
10
+ import { emitTsDeclarations } from "./emit-ts-declarations.js";
10
11
  const outputStructures = {
11
12
  "locale-modules": localeModules,
12
13
  "message-modules": messageModules,
@@ -73,6 +74,10 @@ export const compileProject = async (args) => {
73
74
  }
74
75
  }
75
76
  }
77
+ if (optionsWithDefaults.emitTsDeclarations) {
78
+ const declarations = await emitTsDeclarations(output);
79
+ Object.assign(output, declarations);
80
+ }
76
81
  return output;
77
82
  };
78
83
  export function getFallbackMap(locales, baseLocale) {
@@ -61,6 +61,18 @@ test("emitPrettierIgnore", async () => {
61
61
  expect(_true).toHaveProperty(".prettierignore");
62
62
  expect(_false).not.toHaveProperty(".prettierignore");
63
63
  });
64
+ test("emitTsDeclarations generates declaration files", async () => {
65
+ const output = await compileProject({
66
+ project,
67
+ compilerOptions: {
68
+ emitTsDeclarations: true,
69
+ outputStructure: "locale-modules",
70
+ },
71
+ });
72
+ expect(output).toHaveProperty("messages/_index.d.ts");
73
+ expect(output).toHaveProperty("messages.d.ts");
74
+ expect(output["messages/_index.d.ts"]).toContain("sad_penguin_bundle");
75
+ });
64
76
  test("handles message bundles with a : in the id", async () => {
65
77
  const project = await loadProjectInMemory({
66
78
  blob: await newProject({
@@ -4,6 +4,7 @@ export declare const defaultCompilerOptions: {
4
4
  readonly emitGitIgnore: true;
5
5
  readonly includeEslintDisableComment: true;
6
6
  readonly emitPrettierIgnore: true;
7
+ readonly emitTsDeclarations: false;
7
8
  readonly cleanOutdir: true;
8
9
  readonly disableAsyncLocalStorage: false;
9
10
  readonly experimentalMiddlewareLocaleSplitting: false;
@@ -164,6 +165,29 @@ export type CompilerOptions = {
164
165
  * @default true
165
166
  */
166
167
  emitPrettierIgnore?: boolean;
168
+ /**
169
+ * Emit `.d.ts` files for the generated output using the TypeScript compiler.
170
+ *
171
+ * Useful when `allowJs: true` cannot be set in your `tsconfig.json`
172
+ * (e.g., due to project constraints or conflicting compiler options).
173
+ *
174
+ * Requires `typescript` to be resolvable in your toolchain.
175
+ *
176
+ * **Note:** Enabling this option reduces compiler speed because TypeScript
177
+ * needs to generate declaration files for all output modules.
178
+ *
179
+ * @example
180
+ * ```ts
181
+ * await compile({
182
+ * project: "./project.inlang",
183
+ * outdir: "./src/paraglide",
184
+ * emitTsDeclarations: true,
185
+ * });
186
+ * ```
187
+ *
188
+ * @default false
189
+ */
190
+ emitTsDeclarations?: boolean;
167
191
  /**
168
192
  * https://inlang.com/m/gerre34r/library-inlang-paraglideJs/strategy#url
169
193
  */
@@ -1 +1 @@
1
- {"version":3,"file":"compiler-options.d.ts","sourceRoot":"","sources":["../../src/compiler/compiler-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;CAcU,CAAC;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC7B;;;;;;;;;;OAUG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;OAUG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/B;;;;;;;;;;;;;;OAcG;IACH,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;OAkBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IACrC;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;;;;;OASG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiDG;IACH,eAAe,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACvD;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;OAIG;IACH,EAAE,CAAC,EAAE,GAAG,CAAC;CACT,CAAC"}
1
+ {"version":3,"file":"compiler-options.d.ts","sourceRoot":"","sources":["../../src/compiler/compiler-options.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AAEjD,eAAO,MAAM,sBAAsB;;;;;;;;;;;;;;;CAeU,CAAC;AAE9C,MAAM,MAAM,eAAe,GAAG;IAC7B;;;;;;;;;;OAUG;IACH,OAAO,EAAE,MAAM,CAAC;IAChB;;;;;;;;;;OAUG;IACH,MAAM,EAAE,MAAM,CAAC;IACf;;;;;;;;;;;;;;;;OAgBG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC,UAAU,CAAC,CAAC;IAC/B;;;;;;;;;;;;;;OAcG;IACH,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD;;;;OAIG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB;;;;;;;;;;;OAWG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;OAkBG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC;;;;;;;;;;;;OAYG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;OAEG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC,aAAa,CAAC,CAAC;IACrC;;;;OAIG;IACH,2BAA2B,CAAC,EAAE,OAAO,CAAC;IACtC;;;;;;;;;OASG;IACH,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC;;;;;;;;;;;;OAYG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OAiDG;IACH,eAAe,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,CAAC;IACvD;;;;OAIG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IAEtB;;;;OAIG;IACH,EAAE,CAAC,EAAE,GAAG,CAAC;CACT,CAAC"}
@@ -3,6 +3,7 @@ export const defaultCompilerOptions = {
3
3
  emitGitIgnore: true,
4
4
  includeEslintDisableComment: true,
5
5
  emitPrettierIgnore: true,
6
+ emitTsDeclarations: false,
6
7
  cleanOutdir: true,
7
8
  disableAsyncLocalStorage: false,
8
9
  experimentalMiddlewareLocaleSplitting: false,
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Generates `.d.ts` files for the compiled Paraglide output using the TypeScript compiler.
3
+ *
4
+ * @param output - The generated compiler output keyed by relative file path.
5
+ * @returns The generated declaration files keyed by relative path.
6
+ *
7
+ * @example
8
+ * const declarations = await emitTsDeclarations(output);
9
+ * // Merge them into the compiler output before writing to disk
10
+ */
11
+ export declare function emitTsDeclarations(output: Record<string, string>): Promise<Record<string, string>>;
12
+ //# sourceMappingURL=emit-ts-declarations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"emit-ts-declarations.d.ts","sourceRoot":"","sources":["../../src/compiler/emit-ts-declarations.ts"],"names":[],"mappings":"AAIA;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAC5B,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CA8HjC"}
@@ -0,0 +1,100 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Generates `.d.ts` files for the compiled Paraglide output using the TypeScript compiler.
4
+ *
5
+ * @param output - The generated compiler output keyed by relative file path.
6
+ * @returns The generated declaration files keyed by relative path.
7
+ *
8
+ * @example
9
+ * const declarations = await emitTsDeclarations(output);
10
+ * // Merge them into the compiler output before writing to disk
11
+ */
12
+ export async function emitTsDeclarations(output) {
13
+ const ts = await import("typescript");
14
+ const jsEntries = Object.entries(output).filter(([fileName]) => fileName.endsWith(".js"));
15
+ if (jsEntries.length === 0) {
16
+ return {};
17
+ }
18
+ const virtualRoot = path.join(process.cwd(), "__paraglide_virtual_output");
19
+ const normalizeFileName = (fileName) => path.normalize(path.isAbsolute(fileName) ? fileName : path.join(virtualRoot, fileName));
20
+ const files = new Map(jsEntries.map(([fileName, content]) => [
21
+ normalizeFileName(fileName),
22
+ content,
23
+ ]));
24
+ const virtualDirectories = new Set(Array.from(files.keys()).flatMap((filePath) => {
25
+ const directories = [];
26
+ let current = path.dirname(filePath);
27
+ while (current.startsWith(virtualRoot) && current !== virtualRoot) {
28
+ directories.push(current);
29
+ const parent = path.dirname(current);
30
+ if (parent === current)
31
+ break;
32
+ current = parent;
33
+ }
34
+ return directories;
35
+ }));
36
+ // Ensure the virtual root itself is treated as existing
37
+ virtualDirectories.add(virtualRoot);
38
+ const compilerOptions = {
39
+ allowJs: true,
40
+ checkJs: true,
41
+ declaration: true,
42
+ emitDeclarationOnly: true,
43
+ isolatedDeclarations: true,
44
+ esModuleInterop: true,
45
+ lib: ["ESNext", "DOM"],
46
+ module: ts.ModuleKind.ESNext,
47
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
48
+ noEmitOnError: false,
49
+ outDir: virtualRoot,
50
+ rootDir: virtualRoot,
51
+ skipLibCheck: true,
52
+ target: ts.ScriptTarget.ESNext,
53
+ };
54
+ const defaultHost = ts.createCompilerHost(compilerOptions, true);
55
+ const declarations = {};
56
+ const host = {
57
+ ...defaultHost,
58
+ fileExists: (fileName) => {
59
+ const normalized = normalizeFileName(fileName);
60
+ return files.has(normalized) || defaultHost.fileExists(fileName);
61
+ },
62
+ directoryExists: (directoryName) => {
63
+ const normalized = normalizeFileName(directoryName);
64
+ return (virtualDirectories.has(normalized) ||
65
+ defaultHost.directoryExists?.(directoryName) === true);
66
+ },
67
+ getDirectories: (directoryName) => {
68
+ const normalized = normalizeFileName(directoryName);
69
+ const children = Array.from(virtualDirectories).filter((dir) => path.dirname(dir) === normalized);
70
+ return [
71
+ ...(defaultHost.getDirectories?.(directoryName) ?? []),
72
+ ...children.map((dir) => path.basename(dir)),
73
+ ];
74
+ },
75
+ readFile: (fileName) => {
76
+ const normalized = normalizeFileName(fileName);
77
+ return files.get(normalized) ?? defaultHost.readFile(fileName);
78
+ },
79
+ getSourceFile: (fileName, languageVersion, onError, shouldCreateNewFile) => {
80
+ const normalized = normalizeFileName(fileName);
81
+ const sourceText = files.get(normalized);
82
+ if (sourceText !== undefined) {
83
+ return ts.createSourceFile(fileName, sourceText, languageVersion, true);
84
+ }
85
+ return defaultHost.getSourceFile(fileName, languageVersion, onError, shouldCreateNewFile);
86
+ },
87
+ writeFile: (fileName, text) => {
88
+ const relativePath = path
89
+ .relative(virtualRoot, fileName)
90
+ .split(path.sep)
91
+ .join(path.posix.sep);
92
+ if (!relativePath.startsWith("..")) {
93
+ declarations[relativePath] = text;
94
+ }
95
+ },
96
+ };
97
+ const program = ts.createProgram(Array.from(files.keys()), compilerOptions, host);
98
+ program.emit(undefined, undefined, undefined, true);
99
+ return declarations;
100
+ }
@@ -1,5 +1,5 @@
1
1
  export { defaultCompilerOptions, type CompilerOptions, } from "./compiler-options.js";
2
- export type { MessageBundleFunction, MessageFunction } from "./types.js";
2
+ export type { LocalizedString, MessageBundleFunction, MessageFunction, } from "./types.js";
3
3
  export type { Runtime } from "./runtime/type.js";
4
4
  export type { ServerRuntime } from "./server/type.js";
5
5
  export { compile } from "./compile.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/compiler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,sBAAsB,EACtB,KAAK,eAAe,GACpB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EAAE,qBAAqB,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AACzE,YAAY,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/compiler/index.ts"],"names":[],"mappings":"AAAA,OAAO,EACN,sBAAsB,EACtB,KAAK,eAAe,GACpB,MAAM,uBAAuB,CAAC;AAC/B,YAAY,EACX,eAAe,EACf,qBAAqB,EACrB,eAAe,GACf,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,OAAO,EAAE,MAAM,mBAAmB,CAAC;AACjD,YAAY,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AACtD,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACvC,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC;AACpD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,2CAA2C,CAAC;AACxE,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC"}
@@ -3,7 +3,7 @@ export function jsDocBundleFunctionTypes(args) {
3
3
  return `
4
4
  * @param {${inputsType(args.inputs)}} inputs
5
5
  * @param {{ locale?: ${localesUnion} }} options
6
- * @returns {string}`;
6
+ * @returns {LocalizedString}`;
7
7
  }
8
8
  /**
9
9
  * Returns the types for the input variables.
@@ -24,3 +24,12 @@ test("jsDocBundleFunctionTypes correctly handles messages with duplicate inputs"
24
24
  // It should not contain duplicated parameters
25
25
  expect(result).not.toContain("@param {{ days: NonNullable<unknown>, days: NonNullable<unknown> }} inputs");
26
26
  });
27
+ test("jsDocBundleFunctionTypes returns LocalizedString type", () => {
28
+ const inputs = [];
29
+ const locales = ["en", "de"];
30
+ const result = jsDocBundleFunctionTypes({ inputs, locales });
31
+ // The JSDoc should specify LocalizedString as the return type
32
+ expect(result).toContain("@returns {LocalizedString}");
33
+ // It should not return plain string
34
+ expect(result).not.toContain("@returns {string}");
35
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"locale-modules.d.ts","sourceRoot":"","sources":["../../../src/compiler/output-structure/locale-modules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAIvE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,UAE1E;AAED,wBAAgB,cAAc,CAC7B,eAAe,EAAE,0BAA0B,EAAE,EAC7C,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,YAAY,CAAC,EACzD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAoDxB"}
1
+ {"version":3,"file":"locale-modules.d.ts","sourceRoot":"","sources":["../../../src/compiler/output-structure/locale-modules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAIvE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,UAE1E;AAED,wBAAgB,cAAc,CAC7B,eAAe,EAAE,0BAA0B,EAAE,EAC7C,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,YAAY,CAAC,EACzD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA4DxB"}
@@ -6,6 +6,7 @@ export function messageReferenceExpression(locale, bundleId) {
6
6
  export function generateOutput(compiledBundles, settings, fallbackMap) {
7
7
  const indexFile = [
8
8
  `import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from "../runtime.js"`,
9
+ `/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */`,
9
10
  settings.locales
10
11
  .map((locale) => `import * as ${toSafeModuleId(locale)} from "./${locale}.js"`)
11
12
  .join("\n"),
@@ -31,7 +32,7 @@ export function generateOutput(compiledBundles, settings, fallbackMap) {
31
32
  }
32
33
  else {
33
34
  // no fallback exists, render the bundleId
34
- file += `\n/** @type {(inputs: ${inputsType(inputs)}) => string} */\nexport const ${bundleModuleId} = () => '${bundleId}'`;
35
+ file += `\n/** @type {(inputs: ${inputsType(inputs)}) => LocalizedString} */\nexport const ${bundleModuleId} = () => /** @type {LocalizedString} */ ('${bundleId}')`;
35
36
  }
36
37
  continue;
37
38
  }
@@ -41,6 +42,12 @@ export function generateOutput(compiledBundles, settings, fallbackMap) {
41
42
  if (file.includes("registry.")) {
42
43
  file = `import * as registry from "../registry.js"\n` + file;
43
44
  }
45
+ // add LocalizedString typedef reference if used
46
+ if (file.includes("LocalizedString")) {
47
+ file =
48
+ `/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */\n` +
49
+ file;
50
+ }
44
51
  output[filename] = file;
45
52
  }
46
53
  return output;
@@ -1 +1 @@
1
- {"version":3,"file":"message-modules.d.ts","sourceRoot":"","sources":["../../../src/compiler/output-structure/message-modules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAKvE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,UAE1E;AAED,wBAAgB,cAAc,CAC7B,eAAe,EAAE,0BAA0B,EAAE,EAC7C,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,YAAY,CAAC,EACzD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CA+GxB"}
1
+ {"version":3,"file":"message-modules.d.ts","sourceRoot":"","sources":["../../../src/compiler/output-structure/message-modules.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AACnD,OAAO,KAAK,EAAE,0BAA0B,EAAE,MAAM,sBAAsB,CAAC;AAKvE,wBAAgB,0BAA0B,CAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,UAE1E;AAED,wBAAgB,cAAc,CAC7B,eAAe,EAAE,0BAA0B,EAAE,EAC7C,QAAQ,EAAE,IAAI,CAAC,eAAe,EAAE,SAAS,GAAG,YAAY,CAAC,EACzD,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC,GAC7C,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAkHxB"}
@@ -56,10 +56,10 @@ export function generateOutput(compiledBundles, settings, fallbackMap) {
56
56
  }
57
57
  if (fallbackLocale) {
58
58
  const safeFallbackLocale = toSafeModuleId(fallbackLocale);
59
- messages.push(`/** @type {(inputs: ${inputsType(inputs)}) => string} */\nconst ${safeLocale}_${safeModuleId} = ${safeFallbackLocale}_${safeModuleId};`);
59
+ messages.push(`/** @type {(inputs: ${inputsType(inputs)}) => LocalizedString} */\nconst ${safeLocale}_${safeModuleId} = ${safeFallbackLocale}_${safeModuleId};`);
60
60
  }
61
61
  else {
62
- messages.push(`/** @type {(inputs: ${inputsType(inputs)}) => string} */\nconst ${safeLocale}_${safeModuleId} = () => '${escapeForSingleQuoteString(bundleId)}'`);
62
+ messages.push(`/** @type {(inputs: ${inputsType(inputs)}) => LocalizedString} */\nconst ${safeLocale}_${safeModuleId} = () => /** @type {LocalizedString} */ ('${escapeForSingleQuoteString(bundleId)}')`);
63
63
  }
64
64
  emittingFallbacks.delete(locale);
65
65
  emittedFallbacks.add(locale);
@@ -68,9 +68,10 @@ export function generateOutput(compiledBundles, settings, fallbackMap) {
68
68
  emitFallback(locale);
69
69
  }
70
70
  output[filename] = messages.join("\n\n") + "\n\n" + output[filename];
71
- // add the imports
71
+ // add the imports and type reference (LocalizedString is defined in runtime.js)
72
72
  output[filename] =
73
- `import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';\n\n` +
73
+ `import { getLocale, trackMessageCall, experimentalMiddlewareLocaleSplitting, isServer } from '../runtime.js';\n` +
74
+ `/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */\n\n` +
74
75
  output[filename];
75
76
  // Add the registry import to the message file
76
77
  // if registry is used
@@ -80,8 +81,10 @@ export function generateOutput(compiledBundles, settings, fallbackMap) {
80
81
  }
81
82
  }
82
83
  // all messages index file
83
- output["messages/_index.js"] = Array.from(moduleFilenames)
84
- .map((filename) => `export * from './${filename}'`)
85
- .join("\n");
84
+ output["messages/_index.js"] =
85
+ `/** @typedef {import('../runtime.js').LocalizedString} LocalizedString */\n` +
86
+ Array.from(moduleFilenames)
87
+ .map((filename) => `export * from './${filename}'`)
88
+ .join("\n");
86
89
  return output;
87
90
  }
@@ -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,CA4IT"}
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,CAsLT"}
@@ -95,6 +95,48 @@ ${injectCode("./strategy.js")}
95
95
  * @typedef {(typeof locales)[number]} Locale
96
96
  */
97
97
 
98
+ /**
99
+ * A branded type representing a localized string.
100
+ *
101
+ * Message functions return this type instead of \`string\`, enabling TypeScript
102
+ * to distinguish translated strings from regular strings at compile time.
103
+ * This allows you to enforce that only properly localized content is used
104
+ * in your UI components.
105
+ *
106
+ * Since \`LocalizedString\` is a branded subtype of \`string\`, it remains fully
107
+ * backward compatible—you can pass it anywhere a \`string\` is expected.
108
+ *
109
+ * @example
110
+ * // Enforce localized strings in your components
111
+ * function PageTitle(props: { title: LocalizedString }) {
112
+ * return <h1>{props.title}</h1>
113
+ * }
114
+ *
115
+ * // ✅ Correct: using a message function
116
+ * <PageTitle title={m.welcome_title()} />
117
+ *
118
+ * // ❌ Type error: raw strings are not LocalizedString
119
+ * <PageTitle title="Welcome" />
120
+ *
121
+ * @example
122
+ * // LocalizedString is assignable to string (backward compatible)
123
+ * const localized: LocalizedString = m.greeting()
124
+ * const str: string = localized // ✅ works fine
125
+ *
126
+ * // But string is not assignable to LocalizedString
127
+ * const raw: LocalizedString = "Hello" // ❌ Type error
128
+ *
129
+ * @example
130
+ * // Catches accidental string concatenation
131
+ * function showMessage(msg: LocalizedString) { ... }
132
+ *
133
+ * showMessage(m.hello()) // ✅
134
+ * showMessage("Hello " + userName) // ❌ Type error
135
+ * showMessage(m.hello_user({ name: userName })) // ✅ use params instead
136
+ *
137
+ * @typedef {string & { readonly __brand: 'LocalizedString' }} LocalizedString
138
+ */
139
+
98
140
  `;
99
141
  return code;
100
142
  }
@@ -4,13 +4,29 @@ export type Compiled<Node> = {
4
4
  /** The code generated to implement the AST node */
5
5
  code: string;
6
6
  };
7
+ /**
8
+ * A branded type representing a localized string.
9
+ * Provides compile-time safety to distinguish translated from untranslated strings.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * import { m } from './paraglide/messages.js'
14
+ * import type { LocalizedString } from '@inlang/paraglide-js'
15
+ *
16
+ * const greeting: LocalizedString = m.hello() // ✓ Type-safe
17
+ * const raw: LocalizedString = "Hello" // ✗ Type error
18
+ * ```
19
+ */
20
+ export type LocalizedString = string & {
21
+ readonly __brand: "LocalizedString";
22
+ };
7
23
  /**
8
24
  * A message function is a message for a specific locale.
9
25
  *
10
26
  * @example
11
27
  * m.hello({ name: 'world' })
12
28
  */
13
- export type MessageFunction = (inputs?: Record<string, never>) => string;
29
+ export type MessageFunction = (inputs?: Record<string, never>) => LocalizedString;
14
30
  /**
15
31
  * A message bundle function that selects the message to be returned.
16
32
  *
@@ -22,5 +38,5 @@ export type MessageFunction = (inputs?: Record<string, never>) => string;
22
38
  */
23
39
  export type MessageBundleFunction<T extends string> = (params: Record<string, never>, options: {
24
40
  locale: T;
25
- }) => string;
41
+ }) => LocalizedString;
26
42
  //# sourceMappingURL=types.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/compiler/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,CAAC,IAAI,IAAI;IAC5B,4BAA4B;IAC5B,IAAI,EAAE,IAAI,CAAC;IACX,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,CAAC,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,MAAM,CAAC;AAEzE;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,IAAI,CACrD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAC7B,OAAO,EAAE;IAAE,MAAM,EAAE,CAAC,CAAA;CAAE,KAClB,MAAM,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/compiler/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,QAAQ,CAAC,IAAI,IAAI;IAC5B,4BAA4B;IAC5B,IAAI,EAAE,IAAI,CAAC;IACX,mDAAmD;IACnD,IAAI,EAAE,MAAM,CAAC;CACb,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG;IAAE,QAAQ,CAAC,OAAO,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAE/E;;;;;GAKG;AACH,MAAM,MAAM,eAAe,GAAG,CAC7B,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KAC1B,eAAe,CAAC;AAErB;;;;;;;;GAQG;AACH,MAAM,MAAM,qBAAqB,CAAC,CAAC,SAAS,MAAM,IAAI,CACrD,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,EAC7B,OAAO,EAAE;IAAE,MAAM,EAAE,CAAC,CAAA;CAAE,KAClB,eAAe,CAAC"}
@@ -1,5 +1,5 @@
1
1
  export const ENV_VARIABLES = {
2
2
  PARJS_APP_ID: "library.inlang.paraglideJs",
3
3
  PARJS_POSTHOG_TOKEN: undefined,
4
- PARJS_PACKAGE_VERSION: "2.5.0",
4
+ PARJS_PACKAGE_VERSION: "2.7.0",
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.5.0",
4
+ "version": "2.7.0",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public",
@@ -31,8 +31,8 @@
31
31
  "json5": "2.2.3",
32
32
  "unplugin": "^2.1.2",
33
33
  "urlpattern-polyfill": "^10.0.0",
34
- "@inlang/recommend-sherlock": "0.2.1",
35
- "@inlang/sdk": "2.4.9"
34
+ "@inlang/sdk": "2.4.9",
35
+ "@inlang/recommend-sherlock": "0.2.1"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@rollup/plugin-virtual": "3.0.2",