@inlang/paraglide-js 2.4.0 → 2.6.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 (45) hide show
  1. package/dist/bundler-plugins/vite.d.ts +1 -1
  2. package/dist/bundler-plugins/vite.d.ts.map +1 -1
  3. package/dist/bundler-plugins/webpack.d.ts +1 -1
  4. package/dist/bundler-plugins/webpack.d.ts.map +1 -1
  5. package/dist/cli/steps/update-ts-config.d.ts +13 -0
  6. package/dist/cli/steps/update-ts-config.d.ts.map +1 -1
  7. package/dist/cli/steps/update-ts-config.js +131 -16
  8. package/dist/cli/steps/update-ts-config.test.d.ts +2 -0
  9. package/dist/cli/steps/update-ts-config.test.d.ts.map +1 -0
  10. package/dist/cli/steps/update-ts-config.test.js +59 -0
  11. package/dist/compiler/compile-bundle.js +1 -1
  12. package/dist/compiler/compile-bundle.test.js +27 -25
  13. package/dist/compiler/compile-local-variable.js +1 -1
  14. package/dist/compiler/compile-local-variable.test.js +2 -2
  15. package/dist/compiler/compile-message.js +7 -7
  16. package/dist/compiler/compile-message.test.js +147 -1
  17. package/dist/compiler/compile-pattern.d.ts +1 -1
  18. package/dist/compiler/compile-pattern.js +2 -2
  19. package/dist/compiler/compile-pattern.test.js +1 -1
  20. package/dist/compiler/emit-ts-declarations.d.ts +12 -0
  21. package/dist/compiler/emit-ts-declarations.d.ts.map +1 -0
  22. package/dist/compiler/emit-ts-declarations.js +99 -0
  23. package/dist/compiler/index.d.ts +1 -1
  24. package/dist/compiler/index.d.ts.map +1 -1
  25. package/dist/compiler/jsdoc-types.js +1 -1
  26. package/dist/compiler/jsdoc-types.test.js +9 -0
  27. package/dist/compiler/output-structure/locale-modules.d.ts.map +1 -1
  28. package/dist/compiler/output-structure/locale-modules.js +8 -1
  29. package/dist/compiler/output-structure/message-modules.d.ts.map +1 -1
  30. package/dist/compiler/output-structure/message-modules.js +36 -11
  31. package/dist/compiler/output-structure/message-modules.test.js +42 -0
  32. package/dist/compiler/runtime/assert-is-locale.d.ts.map +1 -1
  33. package/dist/compiler/runtime/assert-is-locale.js +7 -3
  34. package/dist/compiler/runtime/assert-is-locale.test.js +33 -2
  35. package/dist/compiler/runtime/create-runtime.d.ts.map +1 -1
  36. package/dist/compiler/runtime/create-runtime.js +42 -0
  37. package/dist/compiler/runtime/is-locale.d.ts.map +1 -1
  38. package/dist/compiler/runtime/is-locale.js +5 -1
  39. package/dist/compiler/runtime/is-locale.test.d.ts +2 -0
  40. package/dist/compiler/runtime/is-locale.test.d.ts.map +1 -0
  41. package/dist/compiler/runtime/is-locale.test.js +31 -0
  42. package/dist/compiler/types.d.ts +18 -2
  43. package/dist/compiler/types.d.ts.map +1 -1
  44. package/dist/services/env-variables/index.js +1 -1
  45. package/package.json +3 -3
@@ -1,2 +1,2 @@
1
- export declare const paraglideVitePlugin: (options: import("../index.js").CompilerOptions) => import("vite").Plugin<any> | import("vite").Plugin<any>[];
1
+ export declare const paraglideVitePlugin: (options: import("../index.js").CompilerOptions) => import("unplugin").VitePlugin<any> | import("unplugin").VitePlugin<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,+GAAoC,CAAC"}
1
+ {"version":3,"file":"vite.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/vite.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,mBAAmB,+HAAoC,CAAC"}
@@ -1,2 +1,2 @@
1
- export declare const paraglideWebpackPlugin: (options: import("../index.js").CompilerOptions) => import("unplugin").WebpackPluginInstance;
1
+ export declare const paraglideWebpackPlugin: (options: import("../index.js").CompilerOptions) => WebpackPluginInstance;
2
2
  //# sourceMappingURL=webpack.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"webpack.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/webpack.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,sBAAsB,8FAAuC,CAAC"}
1
+ {"version":3,"file":"webpack.d.ts","sourceRoot":"","sources":["../../src/bundler-plugins/webpack.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,sBAAsB,2EAAuC,CAAC"}
@@ -11,4 +11,17 @@ export declare const maybeUpdateTsConfigAllowJs: CliStep<{
11
11
  fs: typeof import("node:fs/promises");
12
12
  logger: Logger;
13
13
  }, unknown>;
14
+ /**
15
+ * Recursively checks whether allowJs is enabled in the provided tsconfig or any
16
+ * referenced configuration files.
17
+ *
18
+ * @param tsconfigPath The path to the tsconfig to inspect.
19
+ * @param fs The file system used to read the configs.
20
+ * @param visited A set of already inspected files to avoid circular lookups.
21
+ * @example
22
+ * ```ts
23
+ * await hasAllowJsEnabled("./tsconfig.json", fs);
24
+ * ```
25
+ */
26
+ export declare const hasAllowJsEnabled: (tsconfigPath: string, fs: typeof import("node:fs/promises"), visited?: Set<string>) => Promise<boolean>;
14
27
  //# sourceMappingURL=update-ts-config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"update-ts-config.d.ts","sourceRoot":"","sources":["../../../src/cli/steps/update-ts-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAC;AAM7D,eAAO,MAAM,mBAAmB,EAAE,OAAO,CACxC;IAAE,EAAE,EAAE,cAAc,kBAAkB,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACzD,OAAO,CAKP,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,0BAA0B,EAAE,OAAO,CAC/C;IAAE,EAAE,EAAE,cAAc,kBAAkB,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACzD,OAAO,CA4DP,CAAC"}
1
+ {"version":3,"file":"update-ts-config.d.ts","sourceRoot":"","sources":["../../../src/cli/steps/update-ts-config.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,gCAAgC,CAAC;AAS7D,eAAO,MAAM,mBAAmB,EAAE,OAAO,CACxC;IAAE,EAAE,EAAE,cAAc,kBAAkB,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACzD,OAAO,CAKP,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,0BAA0B,EAAE,OAAO,CAC/C;IAAE,EAAE,EAAE,cAAc,kBAAkB,CAAC,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,EACzD,OAAO,CA6CP,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,iBAAiB,GAC7B,cAAc,MAAM,EACpB,IAAI,cAAc,kBAAkB,CAAC,EACrC,UAAS,GAAG,CAAC,MAAM,CAAa,KAC9B,OAAO,CAAC,OAAO,CA4CjB,CAAC"}
@@ -1,7 +1,9 @@
1
1
  import { prompt } from "../utils.js";
2
2
  import JSON5 from "json5";
3
3
  import { pathExists } from "../../services/file-handling/exists.js";
4
- // import nodePath from "node:path";
4
+ import nodePath from "node:path";
5
+ import { createRequire } from "node:module";
6
+ const require = createRequire(import.meta.url);
5
7
  export const maybeUpdateTsConfig = async (ctx) => {
6
8
  const ctx1 = await maybeUpdateTsConfigAllowJs(ctx);
7
9
  return ctx1;
@@ -14,10 +16,7 @@ export const maybeUpdateTsConfigAllowJs = async (ctx) => {
14
16
  if ((await pathExists("./tsconfig.json", ctx.fs)) === false) {
15
17
  return ctx;
16
18
  }
17
- const file = await ctx.fs.readFile("./tsconfig.json", { encoding: "utf-8" });
18
- // tsconfig allows comments ... FML
19
- let tsconfig = JSON5.parse(file);
20
- if (tsconfig.compilerOptions?.allowJs === true) {
19
+ if (await hasAllowJsEnabled("./tsconfig.json", ctx.fs)) {
21
20
  // all clear, allowJs is already set to true
22
21
  return ctx;
23
22
  }
@@ -38,17 +37,7 @@ export const maybeUpdateTsConfigAllowJs = async (ctx) => {
38
37
  ctx.logger.warn("Continuing without adjusting the tsconfig.json. This may lead to type errors.");
39
38
  return ctx;
40
39
  }
41
- // don't re-ask the question if there is an `extends` present in the tsconfig
42
- // just trust that it's correct.
43
- if (tsconfig.extends) {
44
- isValid = true;
45
- return ctx;
46
- }
47
- const file = await ctx.fs.readFile("./tsconfig.json", {
48
- encoding: "utf-8",
49
- });
50
- tsconfig = JSON5.parse(file);
51
- if (tsconfig?.compilerOptions?.allowJs === true) {
40
+ if (await hasAllowJsEnabled("./tsconfig.json", ctx.fs)) {
52
41
  isValid = true;
53
42
  return ctx;
54
43
  }
@@ -58,6 +47,132 @@ export const maybeUpdateTsConfigAllowJs = async (ctx) => {
58
47
  }
59
48
  return ctx;
60
49
  };
50
+ /**
51
+ * Recursively checks whether allowJs is enabled in the provided tsconfig or any
52
+ * referenced configuration files.
53
+ *
54
+ * @param tsconfigPath The path to the tsconfig to inspect.
55
+ * @param fs The file system used to read the configs.
56
+ * @param visited A set of already inspected files to avoid circular lookups.
57
+ * @example
58
+ * ```ts
59
+ * await hasAllowJsEnabled("./tsconfig.json", fs);
60
+ * ```
61
+ */
62
+ export const hasAllowJsEnabled = async (tsconfigPath, fs, visited = new Set()) => {
63
+ const normalizedPath = normalizeConfigPath(tsconfigPath);
64
+ if (visited.has(normalizedPath)) {
65
+ return false;
66
+ }
67
+ visited.add(normalizedPath);
68
+ const file = await fs.readFile(normalizedPath, { encoding: "utf-8" });
69
+ const tsconfig = JSON5.parse(file);
70
+ if (tsconfig?.compilerOptions?.allowJs === true) {
71
+ return true;
72
+ }
73
+ const baseDir = nodePath.dirname(normalizedPath);
74
+ const extendCandidates = Array.isArray(tsconfig?.extends)
75
+ ? tsconfig.extends
76
+ : tsconfig?.extends
77
+ ? [tsconfig.extends]
78
+ : [];
79
+ for (const candidate of extendCandidates) {
80
+ if (typeof candidate !== "string")
81
+ continue;
82
+ const resolved = await resolveExtendedConfig(candidate, baseDir, fs);
83
+ if (resolved && (await hasAllowJsEnabled(resolved, fs, visited))) {
84
+ return true;
85
+ }
86
+ }
87
+ if (Array.isArray(tsconfig?.references)) {
88
+ for (const reference of tsconfig.references) {
89
+ const referencePath = reference?.path;
90
+ if (typeof referencePath !== "string")
91
+ continue;
92
+ const resolved = await resolveReferenceConfig(referencePath, baseDir, fs);
93
+ if (resolved && (await hasAllowJsEnabled(resolved, fs, visited))) {
94
+ return true;
95
+ }
96
+ }
97
+ }
98
+ return false;
99
+ };
100
+ /**
101
+ * Normalizes a tsconfig path to an absolute path.
102
+ */
103
+ const normalizeConfigPath = (configPath) => {
104
+ return nodePath.isAbsolute(configPath)
105
+ ? configPath
106
+ : nodePath.resolve(process.cwd(), configPath);
107
+ };
108
+ /**
109
+ * Resolves the extended tsconfig path relative to the base config.
110
+ */
111
+ const resolveExtendedConfig = async (extendsSpecifier, baseDir, fs) => {
112
+ const candidates = new Set();
113
+ const resolvedBase = nodePath.isAbsolute(extendsSpecifier)
114
+ ? extendsSpecifier
115
+ : nodePath.resolve(baseDir, extendsSpecifier);
116
+ candidates.add(resolvedBase);
117
+ if (nodePath.extname(resolvedBase) === "") {
118
+ candidates.add(`${resolvedBase}.json`);
119
+ candidates.add(nodePath.join(resolvedBase, "tsconfig.json"));
120
+ }
121
+ for (const candidate of candidates) {
122
+ if (await pathExists(candidate, fs)) {
123
+ return candidate;
124
+ }
125
+ }
126
+ try {
127
+ return require.resolve(extendsSpecifier, { paths: [baseDir] });
128
+ }
129
+ catch {
130
+ if (extendsSpecifier.endsWith(".json") === false) {
131
+ try {
132
+ return require.resolve(`${extendsSpecifier}.json`, {
133
+ paths: [baseDir],
134
+ });
135
+ }
136
+ catch {
137
+ return undefined;
138
+ }
139
+ }
140
+ }
141
+ return undefined;
142
+ };
143
+ /**
144
+ * Resolves the tsconfig referenced through the `references` property.
145
+ */
146
+ const resolveReferenceConfig = async (referenceSpecifier, baseDir, fs) => {
147
+ const candidates = new Set();
148
+ const resolvedBase = nodePath.isAbsolute(referenceSpecifier)
149
+ ? referenceSpecifier
150
+ : nodePath.resolve(baseDir, referenceSpecifier);
151
+ candidates.add(resolvedBase);
152
+ if (nodePath.extname(resolvedBase) === "") {
153
+ candidates.add(`${resolvedBase}.json`);
154
+ candidates.add(nodePath.join(resolvedBase, "tsconfig.json"));
155
+ }
156
+ for (const candidate of candidates) {
157
+ if (await pathExists(candidate, fs)) {
158
+ try {
159
+ const stats = await fs.stat(candidate);
160
+ if (stats.isDirectory()) {
161
+ const directoryConfig = nodePath.join(candidate, "tsconfig.json");
162
+ if (await pathExists(directoryConfig, fs)) {
163
+ return directoryConfig;
164
+ }
165
+ continue;
166
+ }
167
+ }
168
+ catch {
169
+ // ignore, we'll continue checking other candidates
170
+ }
171
+ return candidate;
172
+ }
173
+ }
174
+ return undefined;
175
+ };
61
176
  // /**
62
177
  // * Ensures that the moduleResolution compiler option is set to "bundler" or similar in the tsconfig.json.
63
178
  // *
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=update-ts-config.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-ts-config.test.d.ts","sourceRoot":"","sources":["../../../src/cli/steps/update-ts-config.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,59 @@
1
+ import { expect, test, vi } from "vitest";
2
+ import { memfs } from "memfs";
3
+ import { maybeUpdateTsConfigAllowJs } from "./update-ts-config.js";
4
+ import { Logger } from "../../services/logger/index.js";
5
+ const setCwd = (cwd) => {
6
+ const original = process.cwd;
7
+ process.cwd = (() => cwd);
8
+ return () => {
9
+ process.cwd = original;
10
+ };
11
+ };
12
+ // Regression coverage for https://github.com/opral/inlang-paraglide-js/issues/560
13
+ test("skips prompting when allowJs is set in a referenced tsconfig", async () => {
14
+ const fs = memfs({
15
+ "/tsconfig.json": JSON.stringify({
16
+ references: [{ path: "./tsconfig.app.json" }],
17
+ }),
18
+ "/tsconfig.app.json": JSON.stringify({
19
+ compilerOptions: { allowJs: true },
20
+ }),
21
+ }).fs;
22
+ const restoreCwd = setCwd("/");
23
+ const logger = new Logger({ silent: true, prefix: false });
24
+ const infoSpy = vi.spyOn(logger, "info");
25
+ try {
26
+ await maybeUpdateTsConfigAllowJs({
27
+ fs: fs.promises,
28
+ logger,
29
+ });
30
+ expect(infoSpy).not.toHaveBeenCalled();
31
+ }
32
+ finally {
33
+ restoreCwd();
34
+ }
35
+ });
36
+ // Regression coverage for https://github.com/opral/inlang-paraglide-js/issues/560
37
+ test("skips prompting when allowJs is provided via extends", async () => {
38
+ const fs = memfs({
39
+ "/tsconfig.json": JSON.stringify({
40
+ extends: "./tsconfig.base.json",
41
+ }),
42
+ "/tsconfig.base.json": JSON.stringify({
43
+ compilerOptions: { allowJs: true },
44
+ }),
45
+ }).fs;
46
+ const restoreCwd = setCwd("/");
47
+ const logger = new Logger({ silent: true, prefix: false });
48
+ const infoSpy = vi.spyOn(logger, "info");
49
+ try {
50
+ await maybeUpdateTsConfigAllowJs({
51
+ fs: fs.promises,
52
+ logger,
53
+ });
54
+ expect(infoSpy).not.toHaveBeenCalled();
55
+ }
56
+ finally {
57
+ restoreCwd();
58
+ }
59
+ });
@@ -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`" }];