wolmar-i18n 0.1.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 (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +158 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +23 -0
  4. package/dist/client/create-client.cjs +87 -0
  5. package/dist/client/create-client.cjs.map +1 -0
  6. package/dist/client/create-client.d.cts +57 -0
  7. package/dist/client/create-client.d.ts +57 -0
  8. package/dist/client/create-client.js +85 -0
  9. package/dist/client/create-client.js.map +1 -0
  10. package/dist/client/index.cjs +4 -0
  11. package/dist/client/index.d.cts +2 -0
  12. package/dist/client/index.d.ts +2 -0
  13. package/dist/client/index.js +3 -0
  14. package/dist/index.cjs +7 -0
  15. package/dist/index.d.cts +5 -0
  16. package/dist/index.d.ts +5 -0
  17. package/dist/index.js +3 -0
  18. package/dist/proxy/create-proxy.cjs +42 -0
  19. package/dist/proxy/create-proxy.cjs.map +1 -0
  20. package/dist/proxy/create-proxy.d.cts +25 -0
  21. package/dist/proxy/create-proxy.d.ts +25 -0
  22. package/dist/proxy/create-proxy.js +41 -0
  23. package/dist/proxy/create-proxy.js.map +1 -0
  24. package/dist/proxy/index.cjs +6 -0
  25. package/dist/proxy/index.d.cts +3 -0
  26. package/dist/proxy/index.d.ts +3 -0
  27. package/dist/proxy/index.js +3 -0
  28. package/dist/proxy/negotiate.cjs +40 -0
  29. package/dist/proxy/negotiate.cjs.map +1 -0
  30. package/dist/proxy/negotiate.d.cts +20 -0
  31. package/dist/proxy/negotiate.d.ts +20 -0
  32. package/dist/proxy/negotiate.js +39 -0
  33. package/dist/proxy/negotiate.js.map +1 -0
  34. package/dist/server/create-server.cjs +35 -0
  35. package/dist/server/create-server.cjs.map +1 -0
  36. package/dist/server/create-server.d.cts +32 -0
  37. package/dist/server/create-server.d.ts +32 -0
  38. package/dist/server/create-server.js +35 -0
  39. package/dist/server/create-server.js.map +1 -0
  40. package/dist/server/index.cjs +3 -0
  41. package/dist/server/index.d.cts +2 -0
  42. package/dist/server/index.d.ts +2 -0
  43. package/dist/server/index.js +2 -0
  44. package/dist/types/config.d.cts +76 -0
  45. package/dist/types/config.d.ts +76 -0
  46. package/dist/types/messages.d.cts +63 -0
  47. package/dist/types/messages.d.ts +63 -0
  48. package/dist/utils/config.cjs +37 -0
  49. package/dist/utils/config.cjs.map +1 -0
  50. package/dist/utils/config.d.cts +24 -0
  51. package/dist/utils/config.d.ts +24 -0
  52. package/dist/utils/config.js +36 -0
  53. package/dist/utils/config.js.map +1 -0
  54. package/dist/utils/dictionary.cjs +20 -0
  55. package/dist/utils/dictionary.cjs.map +1 -0
  56. package/dist/utils/dictionary.js +20 -0
  57. package/dist/utils/dictionary.js.map +1 -0
  58. package/dist/utils/interpolate.cjs +20 -0
  59. package/dist/utils/interpolate.cjs.map +1 -0
  60. package/dist/utils/interpolate.js +20 -0
  61. package/dist/utils/interpolate.js.map +1 -0
  62. package/dist/utils/locale.cjs +14 -0
  63. package/dist/utils/locale.cjs.map +1 -0
  64. package/dist/utils/locale.js +14 -0
  65. package/dist/utils/locale.js.map +1 -0
  66. package/dist/utils/paths.cjs +40 -0
  67. package/dist/utils/paths.cjs.map +1 -0
  68. package/dist/utils/paths.d.cts +19 -0
  69. package/dist/utils/paths.d.ts +19 -0
  70. package/dist/utils/paths.js +38 -0
  71. package/dist/utils/paths.js.map +1 -0
  72. package/dist/utils/translator.cjs +54 -0
  73. package/dist/utils/translator.cjs.map +1 -0
  74. package/dist/utils/translator.js +54 -0
  75. package/dist/utils/translator.js.map +1 -0
  76. package/package.json +95 -0
@@ -0,0 +1,63 @@
1
+ //#region src/types/messages.d.ts
2
+ /**
3
+ * The value types that end a key path — everything else is a nested namespace.
4
+ */
5
+ type Leaf = string | readonly string[];
6
+ /**
7
+ * The dot-separated keys of a dictionary that hold a message, e.g. `"home.title"`,
8
+ * used to type the key `t` takes. A key naming a nested object is excluded.
9
+ */
10
+ type MessagePath<TDictionary> = TDictionary extends object ? { [K in keyof TDictionary & string]: TDictionary[K] extends Leaf ? K : `${K}.${MessagePath<TDictionary[K]>}` }[keyof TDictionary & string] : never;
11
+ /**
12
+ * The dot-separated keys of the nested objects in a dictionary, e.g. `"home"`,
13
+ * used to type the namespace `getTranslations` and `useTranslations` take.
14
+ */
15
+ type NamespacePath<TDictionary> = TDictionary extends object ? { [K in keyof TDictionary & string]: TDictionary[K] extends Leaf ? never : K | `${K}.${NamespacePath<TDictionary[K]>}` }[keyof TDictionary & string] : never;
16
+ /**
17
+ * The value stored at a dot path, used to type what `t` returns and which
18
+ * interpolation params it demands. Resolves a namespace to its sub-dictionary
19
+ * as well as a message key to its string.
20
+ */
21
+ type MessageAt<TDictionary, TPath extends string> = TPath extends `${infer Head}.${infer Rest}` ? Head extends keyof TDictionary ? MessageAt<TDictionary[Head], Rest> : never : TPath extends keyof TDictionary ? TDictionary[TPath] : never;
22
+ /**
23
+ * Collapses an intersection chain into a single object type, so a message's
24
+ * params read as one object.
25
+ */
26
+ type Flatten<T> = { [K in keyof T]: T[K] };
27
+ /**
28
+ * The interpolation params of a message, collected one `{placeholder}` at a time.
29
+ */
30
+ type ExtractArgs<TMessage> = TMessage extends `${string}{${infer Name}}${infer Rest}` ? Record<Name, string | number> & ExtractArgs<Rest> : {};
31
+ /**
32
+ * The interpolation params a message needs, derived from its `{placeholder}`
33
+ * tokens. A message with no placeholders needs none.
34
+ */
35
+ type MessageArgs<TMessage> = Flatten<ExtractArgs<TMessage>>;
36
+ /**
37
+ * The rest params `t` takes for a given message: none when the message has no
38
+ * placeholders, one required params object when it does.
39
+ */
40
+ type TranslatorArgs<TMessage> = [keyof MessageArgs<TMessage>] extends [never] ? [] : [args: MessageArgs<TMessage>];
41
+ /**
42
+ * The value `t` returns for a given message, widened off the literal held in
43
+ * the default locale's dictionary.
44
+ */
45
+ type MessageValue<TMessage> = TMessage extends readonly string[] ? readonly string[] : string;
46
+ /**
47
+ * The `t` function returned by `getTranslations` and `useTranslations`.
48
+ */
49
+ type Translator<TDictionary> = <TPath extends MessagePath<TDictionary>>(key: TPath, ...args: TranslatorArgs<MessageAt<TDictionary, TPath>>) => MessageValue<MessageAt<TDictionary, TPath>>;
50
+ /**
51
+ * The dictionary a `t` function reads from, narrowed to a namespace's
52
+ * sub-dictionary when one is given and the whole dictionary when it is not.
53
+ */
54
+ type Scoped<TDictionary, TNamespace> = TNamespace extends string ? MessageAt<TDictionary, TNamespace> : TDictionary;
55
+ /**
56
+ * The shape every dictionary must match, with each message widened to `string`.
57
+ * Non-default locales pair it with `satisfies` so a missing or stray key is a
58
+ * compile error.
59
+ */
60
+ type MessageSchema<TDictionary> = { [K in keyof TDictionary]: TDictionary[K] extends string ? string : TDictionary[K] extends readonly string[] ? readonly string[] : MessageSchema<TDictionary[K]> };
61
+ //#endregion
62
+ export { MessageArgs, MessageAt, MessagePath, MessageSchema, NamespacePath, Scoped, Translator };
63
+ //# sourceMappingURL=messages.d.ts.map
@@ -0,0 +1,37 @@
1
+ const require_locale = require("./locale.cjs");
2
+ const require_paths = require("./paths.cjs");
3
+ //#region src/utils/config.ts
4
+ /**
5
+ * The default cookie name used to store the user's locale preference.
6
+ * This can be overridden in the i18n config.
7
+ * @default "NEXT_LOCALE"
8
+ */
9
+ const DEFAULT_COOKIE_NAME = "NEXT_LOCALE";
10
+ /**
11
+ * A function to build the shared i18n config.
12
+ * Call this once and export the result; every other factory in the package takes it as its first argument.
13
+ * @param options The supported locales, the default locale, and an optional cookie name, which falls back to `"NEXT_LOCALE"`.
14
+ * @returns A frozen config object whose `hasLocale`, `stripLocale` and `staticParams` are already bound to `locales`, so callers do not pass the list around.
15
+ * @example
16
+ * export const i18n = defineI18nConfig({
17
+ * locales: ["da", "en"],
18
+ * defaultLocale: "da",
19
+ * });
20
+ */
21
+ function defineI18nConfig(options) {
22
+ const { locales, defaultLocale, cookieName = DEFAULT_COOKIE_NAME } = options;
23
+ return Object.freeze({
24
+ locales,
25
+ defaultLocale,
26
+ cookieName,
27
+ hasLocale: (value) => require_locale.hasLocale(value, locales),
28
+ localizePath: require_paths.localizePath,
29
+ stripLocale: (pathname) => require_paths.stripLocale(pathname, locales),
30
+ staticParams: () => locales.map((locale) => ({ locale }))
31
+ });
32
+ }
33
+ //#endregion
34
+ exports.DEFAULT_COOKIE_NAME = DEFAULT_COOKIE_NAME;
35
+ exports.defineI18nConfig = defineI18nConfig;
36
+
37
+ //# sourceMappingURL=config.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.cjs","names":["hasLocale","stripLocale"],"sources":["../../src/utils/config.ts"],"sourcesContent":["import type { I18nConfig, I18nOptions, LocaleList } from \"../types\";\nimport { hasLocale } from \"./locale\";\nimport { localizePath, stripLocale } from \"./paths\";\n\n/**\n * The default cookie name used to store the user's locale preference.\n * This can be overridden in the i18n config.\n * @default \"NEXT_LOCALE\"\n */\nexport const DEFAULT_COOKIE_NAME = \"NEXT_LOCALE\";\n\n/**\n * A function to build the shared i18n config.\n * Call this once and export the result; every other factory in the package takes it as its first argument.\n * @param options The supported locales, the default locale, and an optional cookie name, which falls back to `\"NEXT_LOCALE\"`.\n * @returns A frozen config object whose `hasLocale`, `stripLocale` and `staticParams` are already bound to `locales`, so callers do not pass the list around.\n * @example\n * export const i18n = defineI18nConfig({\n * locales: [\"da\", \"en\"],\n * defaultLocale: \"da\",\n * });\n */\nexport function defineI18nConfig<const TLocales extends LocaleList>(\n options: I18nOptions<TLocales>,\n): I18nConfig<TLocales> {\n const { locales, defaultLocale, cookieName = DEFAULT_COOKIE_NAME } = options;\n\n return Object.freeze({\n locales,\n defaultLocale,\n cookieName,\n\n hasLocale: (value: unknown): value is TLocales[number] =>\n hasLocale(value, locales),\n\n localizePath,\n\n stripLocale: (pathname: string) => stripLocale(pathname, locales),\n\n staticParams: () => locales.map((locale) => ({ locale })),\n });\n}\n"],"mappings":";;;;;;;;AASA,MAAa,sBAAsB;;;;;;;;;;;;AAanC,SAAgB,iBACd,SACsB;CACtB,MAAM,EAAE,SAAS,eAAe,aAAa,wBAAwB;AAErE,QAAO,OAAO,OAAO;EACnB;EACA;EACA;EAEA,YAAY,UACVA,eAAAA,UAAU,OAAO,QAAQ;EAE3B,cAAA,cAAA;EAEA,cAAc,aAAqBC,cAAAA,YAAY,UAAU,QAAQ;EAEjE,oBAAoB,QAAQ,KAAK,YAAY,EAAE,QAAQ,EAAE;EAC1D,CAAC"}
@@ -0,0 +1,24 @@
1
+ import { I18nConfig, I18nOptions, LocaleList } from "../types/config.cjs";
2
+
3
+ //#region src/utils/config.d.ts
4
+ /**
5
+ * The default cookie name used to store the user's locale preference.
6
+ * This can be overridden in the i18n config.
7
+ * @default "NEXT_LOCALE"
8
+ */
9
+ declare const DEFAULT_COOKIE_NAME = "NEXT_LOCALE";
10
+ /**
11
+ * A function to build the shared i18n config.
12
+ * Call this once and export the result; every other factory in the package takes it as its first argument.
13
+ * @param options The supported locales, the default locale, and an optional cookie name, which falls back to `"NEXT_LOCALE"`.
14
+ * @returns A frozen config object whose `hasLocale`, `stripLocale` and `staticParams` are already bound to `locales`, so callers do not pass the list around.
15
+ * @example
16
+ * export const i18n = defineI18nConfig({
17
+ * locales: ["da", "en"],
18
+ * defaultLocale: "da",
19
+ * });
20
+ */
21
+ declare function defineI18nConfig<const TLocales extends LocaleList>(options: I18nOptions<TLocales>): I18nConfig<TLocales>;
22
+ //#endregion
23
+ export { DEFAULT_COOKIE_NAME, defineI18nConfig };
24
+ //# sourceMappingURL=config.d.cts.map
@@ -0,0 +1,24 @@
1
+ import { I18nConfig, I18nOptions, LocaleList } from "../types/config.js";
2
+
3
+ //#region src/utils/config.d.ts
4
+ /**
5
+ * The default cookie name used to store the user's locale preference.
6
+ * This can be overridden in the i18n config.
7
+ * @default "NEXT_LOCALE"
8
+ */
9
+ declare const DEFAULT_COOKIE_NAME = "NEXT_LOCALE";
10
+ /**
11
+ * A function to build the shared i18n config.
12
+ * Call this once and export the result; every other factory in the package takes it as its first argument.
13
+ * @param options The supported locales, the default locale, and an optional cookie name, which falls back to `"NEXT_LOCALE"`.
14
+ * @returns A frozen config object whose `hasLocale`, `stripLocale` and `staticParams` are already bound to `locales`, so callers do not pass the list around.
15
+ * @example
16
+ * export const i18n = defineI18nConfig({
17
+ * locales: ["da", "en"],
18
+ * defaultLocale: "da",
19
+ * });
20
+ */
21
+ declare function defineI18nConfig<const TLocales extends LocaleList>(options: I18nOptions<TLocales>): I18nConfig<TLocales>;
22
+ //#endregion
23
+ export { DEFAULT_COOKIE_NAME, defineI18nConfig };
24
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1,36 @@
1
+ import { hasLocale } from "./locale.js";
2
+ import { localizePath, stripLocale } from "./paths.js";
3
+ //#region src/utils/config.ts
4
+ /**
5
+ * The default cookie name used to store the user's locale preference.
6
+ * This can be overridden in the i18n config.
7
+ * @default "NEXT_LOCALE"
8
+ */
9
+ const DEFAULT_COOKIE_NAME = "NEXT_LOCALE";
10
+ /**
11
+ * A function to build the shared i18n config.
12
+ * Call this once and export the result; every other factory in the package takes it as its first argument.
13
+ * @param options The supported locales, the default locale, and an optional cookie name, which falls back to `"NEXT_LOCALE"`.
14
+ * @returns A frozen config object whose `hasLocale`, `stripLocale` and `staticParams` are already bound to `locales`, so callers do not pass the list around.
15
+ * @example
16
+ * export const i18n = defineI18nConfig({
17
+ * locales: ["da", "en"],
18
+ * defaultLocale: "da",
19
+ * });
20
+ */
21
+ function defineI18nConfig(options) {
22
+ const { locales, defaultLocale, cookieName = DEFAULT_COOKIE_NAME } = options;
23
+ return Object.freeze({
24
+ locales,
25
+ defaultLocale,
26
+ cookieName,
27
+ hasLocale: (value) => hasLocale(value, locales),
28
+ localizePath,
29
+ stripLocale: (pathname) => stripLocale(pathname, locales),
30
+ staticParams: () => locales.map((locale) => ({ locale }))
31
+ });
32
+ }
33
+ //#endregion
34
+ export { DEFAULT_COOKIE_NAME, defineI18nConfig };
35
+
36
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.js","names":[],"sources":["../../src/utils/config.ts"],"sourcesContent":["import type { I18nConfig, I18nOptions, LocaleList } from \"../types\";\nimport { hasLocale } from \"./locale\";\nimport { localizePath, stripLocale } from \"./paths\";\n\n/**\n * The default cookie name used to store the user's locale preference.\n * This can be overridden in the i18n config.\n * @default \"NEXT_LOCALE\"\n */\nexport const DEFAULT_COOKIE_NAME = \"NEXT_LOCALE\";\n\n/**\n * A function to build the shared i18n config.\n * Call this once and export the result; every other factory in the package takes it as its first argument.\n * @param options The supported locales, the default locale, and an optional cookie name, which falls back to `\"NEXT_LOCALE\"`.\n * @returns A frozen config object whose `hasLocale`, `stripLocale` and `staticParams` are already bound to `locales`, so callers do not pass the list around.\n * @example\n * export const i18n = defineI18nConfig({\n * locales: [\"da\", \"en\"],\n * defaultLocale: \"da\",\n * });\n */\nexport function defineI18nConfig<const TLocales extends LocaleList>(\n options: I18nOptions<TLocales>,\n): I18nConfig<TLocales> {\n const { locales, defaultLocale, cookieName = DEFAULT_COOKIE_NAME } = options;\n\n return Object.freeze({\n locales,\n defaultLocale,\n cookieName,\n\n hasLocale: (value: unknown): value is TLocales[number] =>\n hasLocale(value, locales),\n\n localizePath,\n\n stripLocale: (pathname: string) => stripLocale(pathname, locales),\n\n staticParams: () => locales.map((locale) => ({ locale })),\n });\n}\n"],"mappings":";;;;;;;;AASA,MAAa,sBAAsB;;;;;;;;;;;;AAanC,SAAgB,iBACd,SACsB;CACtB,MAAM,EAAE,SAAS,eAAe,aAAa,wBAAwB;AAErE,QAAO,OAAO,OAAO;EACnB;EACA;EACA;EAEA,YAAY,UACV,UAAU,OAAO,QAAQ;EAE3B;EAEA,cAAc,aAAqB,YAAY,UAAU,QAAQ;EAEjE,oBAAoB,QAAQ,KAAK,YAAY,EAAE,QAAQ,EAAE;EAC1D,CAAC"}
@@ -0,0 +1,20 @@
1
+ //#region src/utils/dictionary.ts
2
+ /**
3
+ * A function to read the value at a dot-separated key in a dictionary.
4
+ * @param source The dictionary, or a sub-dictionary, to read from.
5
+ * @param key The dot-separated key to read, e.g. `"home.title"`.
6
+ * @returns The value at the key, or `undefined` if any segment is missing.
7
+ */
8
+ function resolveMessageAt(source, key) {
9
+ let current = source;
10
+ for (const segment of key.split(".")) {
11
+ if (current === null || typeof current !== "object") return void 0;
12
+ if (!Object.hasOwn(current, segment)) return void 0;
13
+ current = current[segment];
14
+ }
15
+ return current;
16
+ }
17
+ //#endregion
18
+ exports.resolveMessageAt = resolveMessageAt;
19
+
20
+ //# sourceMappingURL=dictionary.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dictionary.cjs","names":[],"sources":["../../src/utils/dictionary.ts"],"sourcesContent":["/**\n * A function to read the value at a dot-separated key in a dictionary.\n * @param source The dictionary, or a sub-dictionary, to read from.\n * @param key The dot-separated key to read, e.g. `\"home.title\"`.\n * @returns The value at the key, or `undefined` if any segment is missing.\n */\nexport function resolveMessageAt(source: unknown, key: string): unknown {\n let current: unknown = source;\n\n for (const segment of key.split(\".\")) {\n if (current === null || typeof current !== \"object\") return undefined;\n if (!Object.hasOwn(current, segment)) return undefined;\n\n current = (current as Record<string, unknown>)[segment];\n }\n\n return current;\n}\n"],"mappings":";;;;;;;AAMA,SAAgB,iBAAiB,QAAiB,KAAsB;CACtE,IAAI,UAAmB;AAEvB,MAAK,MAAM,WAAW,IAAI,MAAM,IAAI,EAAE;AACpC,MAAI,YAAY,QAAQ,OAAO,YAAY,SAAU,QAAO,KAAA;AAC5D,MAAI,CAAC,OAAO,OAAO,SAAS,QAAQ,CAAE,QAAO,KAAA;AAE7C,YAAW,QAAoC;;AAGjD,QAAO"}
@@ -0,0 +1,20 @@
1
+ //#region src/utils/dictionary.ts
2
+ /**
3
+ * A function to read the value at a dot-separated key in a dictionary.
4
+ * @param source The dictionary, or a sub-dictionary, to read from.
5
+ * @param key The dot-separated key to read, e.g. `"home.title"`.
6
+ * @returns The value at the key, or `undefined` if any segment is missing.
7
+ */
8
+ function resolveMessageAt(source, key) {
9
+ let current = source;
10
+ for (const segment of key.split(".")) {
11
+ if (current === null || typeof current !== "object") return void 0;
12
+ if (!Object.hasOwn(current, segment)) return void 0;
13
+ current = current[segment];
14
+ }
15
+ return current;
16
+ }
17
+ //#endregion
18
+ export { resolveMessageAt };
19
+
20
+ //# sourceMappingURL=dictionary.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dictionary.js","names":[],"sources":["../../src/utils/dictionary.ts"],"sourcesContent":["/**\n * A function to read the value at a dot-separated key in a dictionary.\n * @param source The dictionary, or a sub-dictionary, to read from.\n * @param key The dot-separated key to read, e.g. `\"home.title\"`.\n * @returns The value at the key, or `undefined` if any segment is missing.\n */\nexport function resolveMessageAt(source: unknown, key: string): unknown {\n let current: unknown = source;\n\n for (const segment of key.split(\".\")) {\n if (current === null || typeof current !== \"object\") return undefined;\n if (!Object.hasOwn(current, segment)) return undefined;\n\n current = (current as Record<string, unknown>)[segment];\n }\n\n return current;\n}\n"],"mappings":";;;;;;;AAMA,SAAgB,iBAAiB,QAAiB,KAAsB;CACtE,IAAI,UAAmB;AAEvB,MAAK,MAAM,WAAW,IAAI,MAAM,IAAI,EAAE;AACpC,MAAI,YAAY,QAAQ,OAAO,YAAY,SAAU,QAAO,KAAA;AAC5D,MAAI,CAAC,OAAO,OAAO,SAAS,QAAQ,CAAE,QAAO,KAAA;AAE7C,YAAW,QAAoC;;AAGjD,QAAO"}
@@ -0,0 +1,20 @@
1
+ //#region src/utils/interpolate.ts
2
+ /**
3
+ * A function to replace the `{placeholder}` tokens in a message.
4
+ * @param message The message to interpolate.
5
+ * @param args The values to replace the tokens with, keyed by placeholder name.
6
+ * @returns The message with every matched token replaced. A token with no
7
+ * matching value is left in place.
8
+ */
9
+ function interpolate(message, args) {
10
+ if (!args) return message;
11
+ return message.replace(/\{([^}]+)\}/g, (token, name) => {
12
+ if (!Object.hasOwn(args, name)) return token;
13
+ const value = args[name];
14
+ return value === void 0 || value === null ? token : String(value);
15
+ });
16
+ }
17
+ //#endregion
18
+ exports.interpolate = interpolate;
19
+
20
+ //# sourceMappingURL=interpolate.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interpolate.cjs","names":[],"sources":["../../src/utils/interpolate.ts"],"sourcesContent":["/**\n * A function to replace the `{placeholder}` tokens in a message.\n * @param message The message to interpolate.\n * @param args The values to replace the tokens with, keyed by placeholder name.\n * @returns The message with every matched token replaced. A token with no\n * matching value is left in place.\n */\nexport function interpolate(\n message: string,\n args?: Record<string, string | number>,\n): string {\n if (!args) return message;\n\n return message.replace(/\\{([^}]+)\\}/g, (token, name: string) => {\n if (!Object.hasOwn(args, name)) return token;\n\n const value = args[name];\n\n return value === undefined || value === null ? token : String(value);\n });\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,YACd,SACA,MACQ;AACR,KAAI,CAAC,KAAM,QAAO;AAElB,QAAO,QAAQ,QAAQ,iBAAiB,OAAO,SAAiB;AAC9D,MAAI,CAAC,OAAO,OAAO,MAAM,KAAK,CAAE,QAAO;EAEvC,MAAM,QAAQ,KAAK;AAEnB,SAAO,UAAU,KAAA,KAAa,UAAU,OAAO,QAAQ,OAAO,MAAM;GACpE"}
@@ -0,0 +1,20 @@
1
+ //#region src/utils/interpolate.ts
2
+ /**
3
+ * A function to replace the `{placeholder}` tokens in a message.
4
+ * @param message The message to interpolate.
5
+ * @param args The values to replace the tokens with, keyed by placeholder name.
6
+ * @returns The message with every matched token replaced. A token with no
7
+ * matching value is left in place.
8
+ */
9
+ function interpolate(message, args) {
10
+ if (!args) return message;
11
+ return message.replace(/\{([^}]+)\}/g, (token, name) => {
12
+ if (!Object.hasOwn(args, name)) return token;
13
+ const value = args[name];
14
+ return value === void 0 || value === null ? token : String(value);
15
+ });
16
+ }
17
+ //#endregion
18
+ export { interpolate };
19
+
20
+ //# sourceMappingURL=interpolate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interpolate.js","names":[],"sources":["../../src/utils/interpolate.ts"],"sourcesContent":["/**\n * A function to replace the `{placeholder}` tokens in a message.\n * @param message The message to interpolate.\n * @param args The values to replace the tokens with, keyed by placeholder name.\n * @returns The message with every matched token replaced. A token with no\n * matching value is left in place.\n */\nexport function interpolate(\n message: string,\n args?: Record<string, string | number>,\n): string {\n if (!args) return message;\n\n return message.replace(/\\{([^}]+)\\}/g, (token, name: string) => {\n if (!Object.hasOwn(args, name)) return token;\n\n const value = args[name];\n\n return value === undefined || value === null ? token : String(value);\n });\n}\n"],"mappings":";;;;;;;;AAOA,SAAgB,YACd,SACA,MACQ;AACR,KAAI,CAAC,KAAM,QAAO;AAElB,QAAO,QAAQ,QAAQ,iBAAiB,OAAO,SAAiB;AAC9D,MAAI,CAAC,OAAO,OAAO,MAAM,KAAK,CAAE,QAAO;EAEvC,MAAM,QAAQ,KAAK;AAEnB,SAAO,UAAU,KAAA,KAAa,UAAU,OAAO,QAAQ,OAAO,MAAM;GACpE"}
@@ -0,0 +1,14 @@
1
+ //#region src/utils/locale.ts
2
+ /**
3
+ * A function to check whether a value is one of the supported locales.
4
+ * @param value The value to check.
5
+ * @param locales A list of supported locales to check against.
6
+ * @returns True if the value is a supported locale.
7
+ */
8
+ function hasLocale(value, locales) {
9
+ return typeof value === "string" && locales.includes(value);
10
+ }
11
+ //#endregion
12
+ exports.hasLocale = hasLocale;
13
+
14
+ //# sourceMappingURL=locale.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale.cjs","names":[],"sources":["../../src/utils/locale.ts"],"sourcesContent":["import type { LocaleList } from \"../types\";\n\n/**\n * A function to check whether a value is one of the supported locales.\n * @param value The value to check.\n * @param locales A list of supported locales to check against.\n * @returns True if the value is a supported locale.\n */\nexport function hasLocale<TLocales extends LocaleList>(\n value: unknown,\n locales: TLocales,\n): value is TLocales[number] {\n return typeof value === \"string\" && locales.includes(value);\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,UACd,OACA,SAC2B;AAC3B,QAAO,OAAO,UAAU,YAAY,QAAQ,SAAS,MAAM"}
@@ -0,0 +1,14 @@
1
+ //#region src/utils/locale.ts
2
+ /**
3
+ * A function to check whether a value is one of the supported locales.
4
+ * @param value The value to check.
5
+ * @param locales A list of supported locales to check against.
6
+ * @returns True if the value is a supported locale.
7
+ */
8
+ function hasLocale(value, locales) {
9
+ return typeof value === "string" && locales.includes(value);
10
+ }
11
+ //#endregion
12
+ export { hasLocale };
13
+
14
+ //# sourceMappingURL=locale.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale.js","names":[],"sources":["../../src/utils/locale.ts"],"sourcesContent":["import type { LocaleList } from \"../types\";\n\n/**\n * A function to check whether a value is one of the supported locales.\n * @param value The value to check.\n * @param locales A list of supported locales to check against.\n * @returns True if the value is a supported locale.\n */\nexport function hasLocale<TLocales extends LocaleList>(\n value: unknown,\n locales: TLocales,\n): value is TLocales[number] {\n return typeof value === \"string\" && locales.includes(value);\n}\n"],"mappings":";;;;;;;AAQA,SAAgB,UACd,OACA,SAC2B;AAC3B,QAAO,OAAO,UAAU,YAAY,QAAQ,SAAS,MAAM"}
@@ -0,0 +1,40 @@
1
+ //#region src/utils/paths.ts
2
+ /**
3
+ * A function to localize a path by prepending the locale to the path.
4
+ * @param barePath The path to localize, which should not already contain a locale segment.
5
+ * A missing leading slash is normalized, so `"projects"` is treated as `"/projects"`.
6
+ * @param locale The locale to prepend to the path.
7
+ * @returns The localized path. A trailing slash is not added, so `"/"` becomes
8
+ * `"/da"` rather than `"/da/"`.
9
+ */
10
+ function localizePath(barePath, locale) {
11
+ if (!barePath || barePath === "/") return `/${locale}`;
12
+ return `/${locale}${barePath.startsWith("/") ? barePath : `/${barePath}`}`;
13
+ }
14
+ /**
15
+ * A function to strip the locale from a pathname.
16
+ * @param pathname The pathname from which to strip the locale.
17
+ * @param locales A list of supported locales to check against.
18
+ * @returns The pathname without the locale segment, or the pathname unchanged
19
+ * when its first segment is not a supported locale. A trailing slash is
20
+ * dropped, so `"/da/"` becomes `"/"`.
21
+ */
22
+ function stripLocale(pathname, locales) {
23
+ const segments = pathname.split("/").filter(Boolean);
24
+ if (!segments.length || !locales.includes(segments[0])) return pathname;
25
+ return `/${segments.slice(1).join("/")}`;
26
+ }
27
+ /**
28
+ * A function to get the first segment of a pathname.
29
+ * @param pathname The pathname to read, e.g. `"/da/projects"`.
30
+ * @returns The first segment, or `undefined` when the pathname has none.
31
+ */
32
+ function firstSegment(pathname) {
33
+ return pathname.split("/").filter(Boolean)[0];
34
+ }
35
+ //#endregion
36
+ exports.firstSegment = firstSegment;
37
+ exports.localizePath = localizePath;
38
+ exports.stripLocale = stripLocale;
39
+
40
+ //# sourceMappingURL=paths.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.cjs","names":[],"sources":["../../src/utils/paths.ts"],"sourcesContent":["import type { LocaleList } from \"../types\";\n\n/**\n * A function to localize a path by prepending the locale to the path.\n * @param barePath The path to localize, which should not already contain a locale segment.\n * A missing leading slash is normalized, so `\"projects\"` is treated as `\"/projects\"`.\n * @param locale The locale to prepend to the path.\n * @returns The localized path. A trailing slash is not added, so `\"/\"` becomes\n * `\"/da\"` rather than `\"/da/\"`.\n */\nexport function localizePath(barePath: string, locale: string): string {\n if (!barePath || barePath === \"/\") return `/${locale}`;\n\n const path = barePath.startsWith(\"/\") ? barePath : `/${barePath}`;\n\n return `/${locale}${path}`;\n}\n\n/**\n * A function to strip the locale from a pathname.\n * @param pathname The pathname from which to strip the locale.\n * @param locales A list of supported locales to check against.\n * @returns The pathname without the locale segment, or the pathname unchanged\n * when its first segment is not a supported locale. A trailing slash is\n * dropped, so `\"/da/\"` becomes `\"/\"`.\n */\nexport function stripLocale(pathname: string, locales: LocaleList): string {\n const segments = pathname.split(\"/\").filter(Boolean);\n if (!segments.length || !locales.includes(segments[0])) return pathname;\n\n return `/${segments.slice(1).join(\"/\")}`;\n}\n\n/**\n * A function to get the first segment of a pathname.\n * @param pathname The pathname to read, e.g. `\"/da/projects\"`.\n * @returns The first segment, or `undefined` when the pathname has none.\n */\nexport function firstSegment(pathname: string): string | undefined {\n return pathname.split(\"/\").filter(Boolean)[0];\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,aAAa,UAAkB,QAAwB;AACrE,KAAI,CAAC,YAAY,aAAa,IAAK,QAAO,IAAI;AAI9C,QAAO,IAAI,SAFE,SAAS,WAAW,IAAI,GAAG,WAAW,IAAI;;;;;;;;;;AAazD,SAAgB,YAAY,UAAkB,SAA6B;CACzE,MAAM,WAAW,SAAS,MAAM,IAAI,CAAC,OAAO,QAAQ;AACpD,KAAI,CAAC,SAAS,UAAU,CAAC,QAAQ,SAAS,SAAS,GAAG,CAAE,QAAO;AAE/D,QAAO,IAAI,SAAS,MAAM,EAAE,CAAC,KAAK,IAAI;;;;;;;AAQxC,SAAgB,aAAa,UAAsC;AACjE,QAAO,SAAS,MAAM,IAAI,CAAC,OAAO,QAAQ,CAAC"}
@@ -0,0 +1,19 @@
1
+ //#region src/utils/paths.d.ts
2
+ /**
3
+ * A function to localize a path by prepending the locale to the path.
4
+ * @param barePath The path to localize, which should not already contain a locale segment.
5
+ * A missing leading slash is normalized, so `"projects"` is treated as `"/projects"`.
6
+ * @param locale The locale to prepend to the path.
7
+ * @returns The localized path. A trailing slash is not added, so `"/"` becomes
8
+ * `"/da"` rather than `"/da/"`.
9
+ */
10
+ declare function localizePath(barePath: string, locale: string): string;
11
+ /**
12
+ * A function to get the first segment of a pathname.
13
+ * @param pathname The pathname to read, e.g. `"/da/projects"`.
14
+ * @returns The first segment, or `undefined` when the pathname has none.
15
+ */
16
+ declare function firstSegment(pathname: string): string | undefined;
17
+ //#endregion
18
+ export { firstSegment, localizePath };
19
+ //# sourceMappingURL=paths.d.cts.map
@@ -0,0 +1,19 @@
1
+ //#region src/utils/paths.d.ts
2
+ /**
3
+ * A function to localize a path by prepending the locale to the path.
4
+ * @param barePath The path to localize, which should not already contain a locale segment.
5
+ * A missing leading slash is normalized, so `"projects"` is treated as `"/projects"`.
6
+ * @param locale The locale to prepend to the path.
7
+ * @returns The localized path. A trailing slash is not added, so `"/"` becomes
8
+ * `"/da"` rather than `"/da/"`.
9
+ */
10
+ declare function localizePath(barePath: string, locale: string): string;
11
+ /**
12
+ * A function to get the first segment of a pathname.
13
+ * @param pathname The pathname to read, e.g. `"/da/projects"`.
14
+ * @returns The first segment, or `undefined` when the pathname has none.
15
+ */
16
+ declare function firstSegment(pathname: string): string | undefined;
17
+ //#endregion
18
+ export { firstSegment, localizePath };
19
+ //# sourceMappingURL=paths.d.ts.map
@@ -0,0 +1,38 @@
1
+ //#region src/utils/paths.ts
2
+ /**
3
+ * A function to localize a path by prepending the locale to the path.
4
+ * @param barePath The path to localize, which should not already contain a locale segment.
5
+ * A missing leading slash is normalized, so `"projects"` is treated as `"/projects"`.
6
+ * @param locale The locale to prepend to the path.
7
+ * @returns The localized path. A trailing slash is not added, so `"/"` becomes
8
+ * `"/da"` rather than `"/da/"`.
9
+ */
10
+ function localizePath(barePath, locale) {
11
+ if (!barePath || barePath === "/") return `/${locale}`;
12
+ return `/${locale}${barePath.startsWith("/") ? barePath : `/${barePath}`}`;
13
+ }
14
+ /**
15
+ * A function to strip the locale from a pathname.
16
+ * @param pathname The pathname from which to strip the locale.
17
+ * @param locales A list of supported locales to check against.
18
+ * @returns The pathname without the locale segment, or the pathname unchanged
19
+ * when its first segment is not a supported locale. A trailing slash is
20
+ * dropped, so `"/da/"` becomes `"/"`.
21
+ */
22
+ function stripLocale(pathname, locales) {
23
+ const segments = pathname.split("/").filter(Boolean);
24
+ if (!segments.length || !locales.includes(segments[0])) return pathname;
25
+ return `/${segments.slice(1).join("/")}`;
26
+ }
27
+ /**
28
+ * A function to get the first segment of a pathname.
29
+ * @param pathname The pathname to read, e.g. `"/da/projects"`.
30
+ * @returns The first segment, or `undefined` when the pathname has none.
31
+ */
32
+ function firstSegment(pathname) {
33
+ return pathname.split("/").filter(Boolean)[0];
34
+ }
35
+ //#endregion
36
+ export { firstSegment, localizePath, stripLocale };
37
+
38
+ //# sourceMappingURL=paths.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"paths.js","names":[],"sources":["../../src/utils/paths.ts"],"sourcesContent":["import type { LocaleList } from \"../types\";\n\n/**\n * A function to localize a path by prepending the locale to the path.\n * @param barePath The path to localize, which should not already contain a locale segment.\n * A missing leading slash is normalized, so `\"projects\"` is treated as `\"/projects\"`.\n * @param locale The locale to prepend to the path.\n * @returns The localized path. A trailing slash is not added, so `\"/\"` becomes\n * `\"/da\"` rather than `\"/da/\"`.\n */\nexport function localizePath(barePath: string, locale: string): string {\n if (!barePath || barePath === \"/\") return `/${locale}`;\n\n const path = barePath.startsWith(\"/\") ? barePath : `/${barePath}`;\n\n return `/${locale}${path}`;\n}\n\n/**\n * A function to strip the locale from a pathname.\n * @param pathname The pathname from which to strip the locale.\n * @param locales A list of supported locales to check against.\n * @returns The pathname without the locale segment, or the pathname unchanged\n * when its first segment is not a supported locale. A trailing slash is\n * dropped, so `\"/da/\"` becomes `\"/\"`.\n */\nexport function stripLocale(pathname: string, locales: LocaleList): string {\n const segments = pathname.split(\"/\").filter(Boolean);\n if (!segments.length || !locales.includes(segments[0])) return pathname;\n\n return `/${segments.slice(1).join(\"/\")}`;\n}\n\n/**\n * A function to get the first segment of a pathname.\n * @param pathname The pathname to read, e.g. `\"/da/projects\"`.\n * @returns The first segment, or `undefined` when the pathname has none.\n */\nexport function firstSegment(pathname: string): string | undefined {\n return pathname.split(\"/\").filter(Boolean)[0];\n}\n"],"mappings":";;;;;;;;;AAUA,SAAgB,aAAa,UAAkB,QAAwB;AACrE,KAAI,CAAC,YAAY,aAAa,IAAK,QAAO,IAAI;AAI9C,QAAO,IAAI,SAFE,SAAS,WAAW,IAAI,GAAG,WAAW,IAAI;;;;;;;;;;AAazD,SAAgB,YAAY,UAAkB,SAA6B;CACzE,MAAM,WAAW,SAAS,MAAM,IAAI,CAAC,OAAO,QAAQ;AACpD,KAAI,CAAC,SAAS,UAAU,CAAC,QAAQ,SAAS,SAAS,GAAG,CAAE,QAAO;AAE/D,QAAO,IAAI,SAAS,MAAM,EAAE,CAAC,KAAK,IAAI;;;;;;;AAQxC,SAAgB,aAAa,UAAsC;AACjE,QAAO,SAAS,MAAM,IAAI,CAAC,OAAO,QAAQ,CAAC"}
@@ -0,0 +1,54 @@
1
+ const require_dictionary = require("./dictionary.cjs");
2
+ const require_interpolate = require("./interpolate.cjs");
3
+ //#region src/utils/translator.ts
4
+ /**
5
+ * The warnings already logged. Module-scoped, so a message is logged once per
6
+ * process rather than once per translator.
7
+ */
8
+ const warned = /* @__PURE__ */ new Set();
9
+ /**
10
+ * A function to log a problem once in development and never in production.
11
+ * @param problem What went wrong, e.g. `"Missing message"`.
12
+ * @param key The key it went wrong for.
13
+ */
14
+ function warnOnce(problem, key) {
15
+ if (process.env.NODE_ENV === "production") return;
16
+ const message = `${problem}: "${key}"`;
17
+ if (warned.has(message)) return;
18
+ warned.add(message);
19
+ console.warn(`[wolmar-i18n] ${message}`);
20
+ }
21
+ /**
22
+ * A function to build the `t` function messages are read through.
23
+ *
24
+ * `TScoped` is the shape the returned `t` reads from — the namespace's
25
+ * sub-dictionary when a namespace is given, the whole dictionary when it is
26
+ * not. Callers pass it explicitly, usually as `Scoped<TDictionary, TNamespace>`.
27
+ *
28
+ * @param dictionary The dictionary to read messages from.
29
+ * @param namespace The dot-separated key to read relative to, e.g. `"home"`.
30
+ * @returns A `t` function whose keys are relative to the namespace.
31
+ *
32
+ * An unresolved key is returned as its own full dotted path rather than
33
+ * throwing, and warns once in development. Note this means a key typed as
34
+ * `readonly string[]` yields a **string** when it is missing, so calling
35
+ * `.map()` on the result would fail — only reachable through untyped data or a
36
+ * cast, since `MessageSchema` makes a missing key a compile error.
37
+ */
38
+ function createTranslator(dictionary, namespace) {
39
+ const scope = namespace ? require_dictionary.resolveMessageAt(dictionary, namespace) : dictionary;
40
+ if (namespace && scope === void 0) warnOnce("Missing namespace", namespace);
41
+ function translate(key, args) {
42
+ const value = require_dictionary.resolveMessageAt(scope, key);
43
+ if (typeof value === "string") return require_interpolate.interpolate(value, args);
44
+ if (Array.isArray(value)) return value;
45
+ const path = namespace ? `${namespace}.${key}` : key;
46
+ warnOnce("Missing message", path);
47
+ return path;
48
+ }
49
+ return translate;
50
+ }
51
+ //#endregion
52
+ exports.createTranslator = createTranslator;
53
+
54
+ //# sourceMappingURL=translator.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translator.cjs","names":["resolveMessageAt","interpolate"],"sources":["../../src/utils/translator.ts"],"sourcesContent":["import type { Translator } from \"../types\";\nimport { resolveMessageAt } from \"./dictionary\";\nimport { interpolate } from \"./interpolate\";\n\n/**\n * The warnings already logged. Module-scoped, so a message is logged once per\n * process rather than once per translator.\n */\nconst warned = new Set<string>();\n\n/**\n * A function to log a problem once in development and never in production.\n * @param problem What went wrong, e.g. `\"Missing message\"`.\n * @param key The key it went wrong for.\n */\nfunction warnOnce(problem: string, key: string): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const message = `${problem}: \"${key}\"`;\n\n if (warned.has(message)) return;\n\n warned.add(message);\n console.warn(`[wolmar-i18n] ${message}`);\n}\n\n/**\n * A function to build the `t` function messages are read through.\n *\n * `TScoped` is the shape the returned `t` reads from — the namespace's\n * sub-dictionary when a namespace is given, the whole dictionary when it is\n * not. Callers pass it explicitly, usually as `Scoped<TDictionary, TNamespace>`.\n *\n * @param dictionary The dictionary to read messages from.\n * @param namespace The dot-separated key to read relative to, e.g. `\"home\"`.\n * @returns A `t` function whose keys are relative to the namespace.\n *\n * An unresolved key is returned as its own full dotted path rather than\n * throwing, and warns once in development. Note this means a key typed as\n * `readonly string[]` yields a **string** when it is missing, so calling\n * `.map()` on the result would fail — only reachable through untyped data or a\n * cast, since `MessageSchema` makes a missing key a compile error.\n */\nexport function createTranslator<TScoped>(\n dictionary: unknown,\n namespace?: string,\n): Translator<TScoped> {\n const scope = namespace\n ? resolveMessageAt(dictionary, namespace)\n : dictionary;\n\n if (namespace && scope === undefined) {\n warnOnce(\"Missing namespace\", namespace);\n }\n\n function translate(\n key: string,\n args?: Record<string, string | number>,\n ): string | readonly string[] {\n const value = resolveMessageAt(scope, key);\n\n if (typeof value === \"string\") return interpolate(value, args);\n if (Array.isArray(value)) return value;\n\n const path = namespace ? `${namespace}.${key}` : key;\n\n warnOnce(\"Missing message\", path);\n\n return path;\n }\n\n return translate as unknown as Translator<TScoped>;\n}\n"],"mappings":";;;;;;;AAQA,MAAM,yBAAS,IAAI,KAAa;;;;;;AAOhC,SAAS,SAAS,SAAiB,KAAmB;AACpD,KAAI,QAAQ,IAAI,aAAa,aAAc;CAE3C,MAAM,UAAU,GAAG,QAAQ,KAAK,IAAI;AAEpC,KAAI,OAAO,IAAI,QAAQ,CAAE;AAEzB,QAAO,IAAI,QAAQ;AACnB,SAAQ,KAAK,iBAAiB,UAAU;;;;;;;;;;;;;;;;;;;AAoB1C,SAAgB,iBACd,YACA,WACqB;CACrB,MAAM,QAAQ,YACVA,mBAAAA,iBAAiB,YAAY,UAAU,GACvC;AAEJ,KAAI,aAAa,UAAU,KAAA,EACzB,UAAS,qBAAqB,UAAU;CAG1C,SAAS,UACP,KACA,MAC4B;EAC5B,MAAM,QAAQA,mBAAAA,iBAAiB,OAAO,IAAI;AAE1C,MAAI,OAAO,UAAU,SAAU,QAAOC,oBAAAA,YAAY,OAAO,KAAK;AAC9D,MAAI,MAAM,QAAQ,MAAM,CAAE,QAAO;EAEjC,MAAM,OAAO,YAAY,GAAG,UAAU,GAAG,QAAQ;AAEjD,WAAS,mBAAmB,KAAK;AAEjC,SAAO;;AAGT,QAAO"}
@@ -0,0 +1,54 @@
1
+ import { resolveMessageAt } from "./dictionary.js";
2
+ import { interpolate } from "./interpolate.js";
3
+ //#region src/utils/translator.ts
4
+ /**
5
+ * The warnings already logged. Module-scoped, so a message is logged once per
6
+ * process rather than once per translator.
7
+ */
8
+ const warned = /* @__PURE__ */ new Set();
9
+ /**
10
+ * A function to log a problem once in development and never in production.
11
+ * @param problem What went wrong, e.g. `"Missing message"`.
12
+ * @param key The key it went wrong for.
13
+ */
14
+ function warnOnce(problem, key) {
15
+ if (process.env.NODE_ENV === "production") return;
16
+ const message = `${problem}: "${key}"`;
17
+ if (warned.has(message)) return;
18
+ warned.add(message);
19
+ console.warn(`[wolmar-i18n] ${message}`);
20
+ }
21
+ /**
22
+ * A function to build the `t` function messages are read through.
23
+ *
24
+ * `TScoped` is the shape the returned `t` reads from — the namespace's
25
+ * sub-dictionary when a namespace is given, the whole dictionary when it is
26
+ * not. Callers pass it explicitly, usually as `Scoped<TDictionary, TNamespace>`.
27
+ *
28
+ * @param dictionary The dictionary to read messages from.
29
+ * @param namespace The dot-separated key to read relative to, e.g. `"home"`.
30
+ * @returns A `t` function whose keys are relative to the namespace.
31
+ *
32
+ * An unresolved key is returned as its own full dotted path rather than
33
+ * throwing, and warns once in development. Note this means a key typed as
34
+ * `readonly string[]` yields a **string** when it is missing, so calling
35
+ * `.map()` on the result would fail — only reachable through untyped data or a
36
+ * cast, since `MessageSchema` makes a missing key a compile error.
37
+ */
38
+ function createTranslator(dictionary, namespace) {
39
+ const scope = namespace ? resolveMessageAt(dictionary, namespace) : dictionary;
40
+ if (namespace && scope === void 0) warnOnce("Missing namespace", namespace);
41
+ function translate(key, args) {
42
+ const value = resolveMessageAt(scope, key);
43
+ if (typeof value === "string") return interpolate(value, args);
44
+ if (Array.isArray(value)) return value;
45
+ const path = namespace ? `${namespace}.${key}` : key;
46
+ warnOnce("Missing message", path);
47
+ return path;
48
+ }
49
+ return translate;
50
+ }
51
+ //#endregion
52
+ export { createTranslator };
53
+
54
+ //# sourceMappingURL=translator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"translator.js","names":[],"sources":["../../src/utils/translator.ts"],"sourcesContent":["import type { Translator } from \"../types\";\nimport { resolveMessageAt } from \"./dictionary\";\nimport { interpolate } from \"./interpolate\";\n\n/**\n * The warnings already logged. Module-scoped, so a message is logged once per\n * process rather than once per translator.\n */\nconst warned = new Set<string>();\n\n/**\n * A function to log a problem once in development and never in production.\n * @param problem What went wrong, e.g. `\"Missing message\"`.\n * @param key The key it went wrong for.\n */\nfunction warnOnce(problem: string, key: string): void {\n if (process.env.NODE_ENV === \"production\") return;\n\n const message = `${problem}: \"${key}\"`;\n\n if (warned.has(message)) return;\n\n warned.add(message);\n console.warn(`[wolmar-i18n] ${message}`);\n}\n\n/**\n * A function to build the `t` function messages are read through.\n *\n * `TScoped` is the shape the returned `t` reads from — the namespace's\n * sub-dictionary when a namespace is given, the whole dictionary when it is\n * not. Callers pass it explicitly, usually as `Scoped<TDictionary, TNamespace>`.\n *\n * @param dictionary The dictionary to read messages from.\n * @param namespace The dot-separated key to read relative to, e.g. `\"home\"`.\n * @returns A `t` function whose keys are relative to the namespace.\n *\n * An unresolved key is returned as its own full dotted path rather than\n * throwing, and warns once in development. Note this means a key typed as\n * `readonly string[]` yields a **string** when it is missing, so calling\n * `.map()` on the result would fail — only reachable through untyped data or a\n * cast, since `MessageSchema` makes a missing key a compile error.\n */\nexport function createTranslator<TScoped>(\n dictionary: unknown,\n namespace?: string,\n): Translator<TScoped> {\n const scope = namespace\n ? resolveMessageAt(dictionary, namespace)\n : dictionary;\n\n if (namespace && scope === undefined) {\n warnOnce(\"Missing namespace\", namespace);\n }\n\n function translate(\n key: string,\n args?: Record<string, string | number>,\n ): string | readonly string[] {\n const value = resolveMessageAt(scope, key);\n\n if (typeof value === \"string\") return interpolate(value, args);\n if (Array.isArray(value)) return value;\n\n const path = namespace ? `${namespace}.${key}` : key;\n\n warnOnce(\"Missing message\", path);\n\n return path;\n }\n\n return translate as unknown as Translator<TScoped>;\n}\n"],"mappings":";;;;;;;AAQA,MAAM,yBAAS,IAAI,KAAa;;;;;;AAOhC,SAAS,SAAS,SAAiB,KAAmB;AACpD,KAAI,QAAQ,IAAI,aAAa,aAAc;CAE3C,MAAM,UAAU,GAAG,QAAQ,KAAK,IAAI;AAEpC,KAAI,OAAO,IAAI,QAAQ,CAAE;AAEzB,QAAO,IAAI,QAAQ;AACnB,SAAQ,KAAK,iBAAiB,UAAU;;;;;;;;;;;;;;;;;;;AAoB1C,SAAgB,iBACd,YACA,WACqB;CACrB,MAAM,QAAQ,YACV,iBAAiB,YAAY,UAAU,GACvC;AAEJ,KAAI,aAAa,UAAU,KAAA,EACzB,UAAS,qBAAqB,UAAU;CAG1C,SAAS,UACP,KACA,MAC4B;EAC5B,MAAM,QAAQ,iBAAiB,OAAO,IAAI;AAE1C,MAAI,OAAO,UAAU,SAAU,QAAO,YAAY,OAAO,KAAK;AAC9D,MAAI,MAAM,QAAQ,MAAM,CAAE,QAAO;EAEjC,MAAM,OAAO,YAAY,GAAG,UAAU,GAAG,QAAQ;AAEjD,WAAS,mBAAmB,KAAK;AAEjC,SAAO;;AAGT,QAAO"}