@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
@@ -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,CA6HjC"}
@@ -0,0 +1,99 @@
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
+ esModuleInterop: true,
44
+ lib: ["ESNext", "DOM"],
45
+ module: ts.ModuleKind.ESNext,
46
+ moduleResolution: ts.ModuleResolutionKind.Bundler,
47
+ noEmitOnError: false,
48
+ outDir: virtualRoot,
49
+ rootDir: virtualRoot,
50
+ skipLibCheck: true,
51
+ target: ts.ScriptTarget.ESNext,
52
+ };
53
+ const defaultHost = ts.createCompilerHost(compilerOptions, true);
54
+ const declarations = {};
55
+ const host = {
56
+ ...defaultHost,
57
+ fileExists: (fileName) => {
58
+ const normalized = normalizeFileName(fileName);
59
+ return files.has(normalized) || defaultHost.fileExists(fileName);
60
+ },
61
+ directoryExists: (directoryName) => {
62
+ const normalized = normalizeFileName(directoryName);
63
+ return (virtualDirectories.has(normalized) ||
64
+ defaultHost.directoryExists?.(directoryName) === true);
65
+ },
66
+ getDirectories: (directoryName) => {
67
+ const normalized = normalizeFileName(directoryName);
68
+ const children = Array.from(virtualDirectories).filter((dir) => path.dirname(dir) === normalized);
69
+ return [
70
+ ...(defaultHost.getDirectories?.(directoryName) ?? []),
71
+ ...children.map((dir) => path.basename(dir)),
72
+ ];
73
+ },
74
+ readFile: (fileName) => {
75
+ const normalized = normalizeFileName(fileName);
76
+ return files.get(normalized) ?? defaultHost.readFile(fileName);
77
+ },
78
+ getSourceFile: (fileName, languageVersion, onError, shouldCreateNewFile) => {
79
+ const normalized = normalizeFileName(fileName);
80
+ const sourceText = files.get(normalized);
81
+ if (sourceText !== undefined) {
82
+ return ts.createSourceFile(fileName, sourceText, languageVersion, true);
83
+ }
84
+ return defaultHost.getSourceFile(fileName, languageVersion, onError, shouldCreateNewFile);
85
+ },
86
+ writeFile: (fileName, text) => {
87
+ const relativePath = path
88
+ .relative(virtualRoot, fileName)
89
+ .split(path.sep)
90
+ .join(path.posix.sep);
91
+ if (!relativePath.startsWith("..")) {
92
+ declarations[relativePath] = text;
93
+ }
94
+ },
95
+ };
96
+ const program = ts.createProgram(Array.from(files.keys()), compilerOptions, host);
97
+ program.emit(undefined, undefined, undefined, true);
98
+ return declarations;
99
+ }
@@ -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,CAkFxB"}
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"}
@@ -31,24 +31,47 @@ export function generateOutput(compiledBundles, settings, fallbackMap) {
31
31
  }
32
32
  // add the fallbacks (needs to be done after the messages to avoid referencing
33
33
  // the message before they are defined)
34
- for (const locale of needsFallback) {
35
- // add fallback
34
+ const needsFallbackSet = new Set(needsFallback);
35
+ const emittedFallbacks = new Set();
36
+ const emittingFallbacks = new Set();
37
+ /**
38
+ * Emits the fallback definition for a locale ensuring that dependent fallbacks
39
+ * are declared beforehand.
40
+ *
41
+ * @example
42
+ * emitFallback("fr-ca");
43
+ */
44
+ const emitFallback = (locale) => {
45
+ if (emittedFallbacks.has(locale))
46
+ return;
47
+ if (emittingFallbacks.has(locale))
48
+ return;
49
+ emittingFallbacks.add(locale);
36
50
  const safeLocale = toSafeModuleId(locale);
37
51
  const fallbackLocale = fallbackMap[locale];
52
+ if (fallbackLocale &&
53
+ needsFallbackSet.has(fallbackLocale) &&
54
+ !compiledBundle.messages[fallbackLocale]) {
55
+ emitFallback(fallbackLocale);
56
+ }
38
57
  if (fallbackLocale) {
39
58
  const safeFallbackLocale = toSafeModuleId(fallbackLocale);
40
- // take the fallback locale
41
- 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};`);
42
60
  }
43
61
  else {
44
- // fallback to just the bundle id
45
- 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)}')`);
46
63
  }
64
+ emittingFallbacks.delete(locale);
65
+ emittedFallbacks.add(locale);
66
+ };
67
+ for (const locale of needsFallback) {
68
+ emitFallback(locale);
47
69
  }
48
70
  output[filename] = messages.join("\n\n") + "\n\n" + output[filename];
49
- // add the imports
71
+ // add the imports and type reference (LocalizedString is defined in runtime.js)
50
72
  output[filename] =
51
- `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` +
52
75
  output[filename];
53
76
  // Add the registry import to the message file
54
77
  // if registry is used
@@ -58,8 +81,10 @@ export function generateOutput(compiledBundles, settings, fallbackMap) {
58
81
  }
59
82
  }
60
83
  // all messages index file
61
- output["messages/_index.js"] = Array.from(moduleFilenames)
62
- .map((filename) => `export * from './${filename}'`)
63
- .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");
64
89
  return output;
65
90
  }
@@ -75,3 +75,45 @@ test("handles case senstivity by creating directories and files only in lowercas
75
75
  expect(output).toHaveProperty("messages/happyelephant2.js");
76
76
  expect(output).not.toHaveProperty("messages/HappyElephant.js");
77
77
  });
78
+ // Regression test for https://github.com/opral/inlang-paraglide-js/issues/507
79
+ test("emits fallback definitions after their dependencies", () => {
80
+ const resources = [
81
+ {
82
+ bundle: {
83
+ code: "export const admin_tasks = (inputs) => inputs;",
84
+ node: {
85
+ id: "admin_tasks",
86
+ declarations: [],
87
+ },
88
+ },
89
+ messages: {
90
+ en: {
91
+ code: '/** @type {(inputs: {}) => string} */ () => "admin"',
92
+ node: {},
93
+ },
94
+ },
95
+ },
96
+ ];
97
+ const settings = {
98
+ locales: ["fr-ca", "fr", "en"],
99
+ baseLocale: "en",
100
+ };
101
+ const fallbackMap = {
102
+ "fr-ca": "fr",
103
+ fr: "en",
104
+ en: undefined,
105
+ };
106
+ const output = generateOutput(resources, settings, fallbackMap);
107
+ const file = output["messages/admin_tasks.js"];
108
+ expect(file).toBeDefined();
109
+ if (!file) {
110
+ throw new Error("messages/admin_tasks.js should have been generated");
111
+ }
112
+ expect(file).toContain("const fr_admin_tasks = en_admin_tasks;");
113
+ expect(file).toContain("const fr_ca_admin_tasks = fr_admin_tasks;");
114
+ const frIndex = file.indexOf("const fr_admin_tasks");
115
+ const frCaIndex = file.indexOf("const fr_ca_admin_tasks");
116
+ expect(frIndex).toBeGreaterThan(-1);
117
+ expect(frCaIndex).toBeGreaterThan(-1);
118
+ expect(frIndex).toBeLessThan(frCaIndex);
119
+ });
@@ -1 +1 @@
1
- {"version":3,"file":"assert-is-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/assert-is-locale.js"],"names":[],"mappings":"AAGA;;;;;;GAMG;AACH,sCAJW,GAAG,GACD,MAAM,CAUlB"}
1
+ {"version":3,"file":"assert-is-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/assert-is-locale.js"],"names":[],"mappings":"AAEA;;;;;;GAMG;AACH,sCAJW,GAAG,GACD,MAAM,CAiBlB"}
@@ -1,4 +1,3 @@
1
- import { isLocale } from "./is-locale.js";
2
1
  import { locales } from "./variables.js";
3
2
  /**
4
3
  * Asserts that the input is a locale.
@@ -8,8 +7,13 @@ import { locales } from "./variables.js";
8
7
  * @throws {Error} If the input is not a locale.
9
8
  */
10
9
  export function assertIsLocale(input) {
11
- if (isLocale(input) === false) {
10
+ if (typeof input !== "string") {
11
+ throw new Error(`Invalid locale: ${input}. Expected a string.`);
12
+ }
13
+ const lowerInput = input.toLowerCase();
14
+ const matchedLocale = locales.find((item) => item.toLowerCase() === lowerInput);
15
+ if (!matchedLocale) {
12
16
  throw new Error(`Invalid locale: ${input}. Expected one of: ${locales.join(", ")}`);
13
17
  }
14
- return input;
18
+ return matchedLocale;
15
19
  }
@@ -1,6 +1,6 @@
1
- import { test, expect } from "vitest";
2
- import { createParaglide } from "../create-paraglide.js";
3
1
  import { newProject } from "@inlang/sdk";
2
+ import { expect, test } from "vitest";
3
+ import { createParaglide } from "../create-paraglide.js";
4
4
  test("throws if the locale is not available", async () => {
5
5
  const runtime = await createParaglide({
6
6
  blob: await newProject({
@@ -12,6 +12,21 @@ test("throws if the locale is not available", async () => {
12
12
  });
13
13
  expect(() => runtime.assertIsLocale("es")).toThrow();
14
14
  });
15
+ test("throws for non-string inputs", async () => {
16
+ const runtime = await createParaglide({
17
+ blob: await newProject({
18
+ settings: {
19
+ baseLocale: "en",
20
+ locales: ["en", "de"],
21
+ },
22
+ }),
23
+ });
24
+ expect(() => runtime.assertIsLocale(null)).toThrow();
25
+ expect(() => runtime.assertIsLocale(undefined)).toThrow();
26
+ expect(() => runtime.assertIsLocale(123)).toThrow();
27
+ expect(() => runtime.assertIsLocale({})).toThrow();
28
+ expect(() => runtime.assertIsLocale([])).toThrow();
29
+ });
15
30
  test("passes if the locale is available", async () => {
16
31
  const runtime = await createParaglide({
17
32
  blob: await newProject({
@@ -37,3 +52,19 @@ test("the return value is a Locale", async () => {
37
52
  // in the ambient type definition
38
53
  locale;
39
54
  });
55
+ test("is case-insensitive", async () => {
56
+ const runtime = await createParaglide({
57
+ blob: await newProject({
58
+ settings: {
59
+ baseLocale: "en",
60
+ locales: ["en", "pt-BR", "de-ch"],
61
+ },
62
+ }),
63
+ });
64
+ expect(() => runtime.assertIsLocale("EN")).not.toThrow();
65
+ expect(() => runtime.assertIsLocale("pt-br")).not.toThrow();
66
+ expect(() => runtime.assertIsLocale("de-CH")).not.toThrow();
67
+ expect(runtime.assertIsLocale("EN")).toBe("en");
68
+ expect(runtime.assertIsLocale("pT-bR")).toBe("pt-BR");
69
+ expect(runtime.assertIsLocale("de-CH")).toBe("de-ch");
70
+ });
@@ -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
  }
@@ -1 +1 @@
1
- {"version":3,"file":"is-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/is-locale.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,iCAHW,GAAG,GACD,MAAM,IAAI,MAAM,CAI5B"}
1
+ {"version":3,"file":"is-locale.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/is-locale.js"],"names":[],"mappings":"AAEA;;;;;;;;;;;;GAYG;AACH,iCAHW,GAAG,GACD,MAAM,IAAI,MAAM,CAO5B"}
@@ -13,5 +13,9 @@ import { locales } from "./variables.js";
13
13
  * @returns {locale is Locale}
14
14
  */
15
15
  export function isLocale(locale) {
16
- return !locale ? false : locales.includes(locale);
16
+ if (typeof locale !== "string")
17
+ return false;
18
+ return !locale
19
+ ? false
20
+ : locales.some((item) => item.toLowerCase() === locale.toLowerCase());
17
21
  }
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=is-locale.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"is-locale.test.d.ts","sourceRoot":"","sources":["../../../src/compiler/runtime/is-locale.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,31 @@
1
+ import { newProject } from "@inlang/sdk";
2
+ import { expect, test } from "vitest";
3
+ import { createParaglide } from "../create-paraglide.js";
4
+ const runtime = await createParaglide({
5
+ blob: await newProject({
6
+ settings: {
7
+ baseLocale: "en",
8
+ locales: ["en", "pt-BR", "de-ch"],
9
+ },
10
+ }),
11
+ });
12
+ test("returns true for exact matches", () => {
13
+ expect(runtime.isLocale("pt-BR")).toBe(true);
14
+ });
15
+ test("is case-insensitive", () => {
16
+ expect(runtime.isLocale("EN")).toBe(true);
17
+ expect(runtime.isLocale("pt-br")).toBe(true);
18
+ expect(runtime.isLocale("de-CH")).toBe(true);
19
+ });
20
+ test("returns false for non-existent locales", () => {
21
+ expect(runtime.isLocale("es")).toBe(false);
22
+ expect(runtime.isLocale("xx")).toBe(false);
23
+ expect(runtime.isLocale("")).toBe(false);
24
+ });
25
+ test("returns false for non-string inputs", () => {
26
+ expect(runtime.isLocale(null)).toBe(false);
27
+ expect(runtime.isLocale(undefined)).toBe(false);
28
+ expect(runtime.isLocale(123)).toBe(false);
29
+ expect(runtime.isLocale({})).toBe(false);
30
+ expect(runtime.isLocale([])).toBe(false);
31
+ });
@@ -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.4.0",
4
+ PARJS_PACKAGE_VERSION: "2.6.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.4.0",
4
+ "version": "2.6.0",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
7
7
  "access": "public",
@@ -26,13 +26,13 @@
26
26
  "./urlpattern-polyfill": "./dist/urlpattern-polyfill/index.js"
27
27
  },
28
28
  "dependencies": {
29
- "@inlang/sdk": "2.4.9",
30
29
  "commander": "11.1.0",
31
30
  "consola": "3.4.0",
32
31
  "json5": "2.2.3",
33
32
  "unplugin": "^2.1.2",
34
33
  "urlpattern-polyfill": "^10.0.0",
35
- "@inlang/recommend-sherlock": "0.2.1"
34
+ "@inlang/recommend-sherlock": "0.2.1",
35
+ "@inlang/sdk": "2.4.9"
36
36
  },
37
37
  "devDependencies": {
38
38
  "@rollup/plugin-virtual": "3.0.2",