intor 2.3.9 → 2.3.11

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 (174) hide show
  1. package/README.md +0 -6
  2. package/dist/core/export/index.js +2 -0
  3. package/dist/core/export/server/index.js +0 -2
  4. package/dist/core/src/config/constants/routing.js +11 -15
  5. package/dist/core/src/config/define-intor-config.js +16 -16
  6. package/dist/core/src/config/resolvers/resolve-fallback-locales.js +45 -35
  7. package/dist/core/src/config/resolvers/resolve-routing-options.js +56 -36
  8. package/dist/core/src/config/validators/validate-default-locale.js +5 -19
  9. package/dist/core/src/config/validators/validate-id.js +21 -0
  10. package/dist/core/src/config/validators/validate-supported-locales.js +6 -16
  11. package/dist/core/src/core/error/intor-error.js +1 -1
  12. package/dist/core/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  13. package/dist/core/src/core/messages/merge-messages.js +33 -0
  14. package/dist/core/src/core/utils/deep-merge.js +35 -19
  15. package/dist/core/src/core/utils/resolve-loader-options.js +9 -0
  16. package/dist/core/src/routing/pathname/locale-prefix-pathname.js +11 -11
  17. package/dist/core/src/server/helpers/local-messages-from-url.js +1 -1
  18. package/dist/core/src/server/messages/load-local-messages/load-local-messages.js +14 -13
  19. package/dist/core/src/server/messages/load-local-messages/read-locale-messages/read-locale-messages.js +1 -1
  20. package/dist/core/src/server/messages/load-messages.js +7 -6
  21. package/dist/core/src/server/translator/create-translator.js +5 -5
  22. package/dist/express/src/adapters/express/middleware/create-intor.js +8 -4
  23. package/dist/express/src/core/constants/headers.js +1 -2
  24. package/dist/express/src/core/error/intor-error.js +1 -1
  25. package/dist/express/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  26. package/dist/express/src/core/messages/merge-messages.js +33 -0
  27. package/dist/express/src/core/utils/deep-merge.js +35 -19
  28. package/dist/express/src/routing/inbound/resolve-inbound.js +9 -8
  29. package/dist/express/src/routing/inbound/resolve-locale/resolve-locale.js +5 -12
  30. package/dist/express/src/routing/inbound/resolve-pathname/resolve-pathname.js +9 -19
  31. package/dist/express/src/routing/inbound/resolve-pathname/strategies/all.js +18 -13
  32. package/dist/express/src/routing/inbound/resolve-pathname/strategies/except-default.js +20 -24
  33. package/dist/express/src/routing/inbound/resolve-pathname/strategies/none.js +1 -7
  34. package/dist/express/src/routing/locale/get-locale-from-query.js +2 -2
  35. package/dist/express/src/routing/pathname/locale-prefix-pathname.js +11 -11
  36. package/dist/express/src/server/messages/load-local-messages/load-local-messages.js +14 -13
  37. package/dist/express/src/server/messages/load-local-messages/read-locale-messages/read-locale-messages.js +1 -1
  38. package/dist/express/src/server/messages/load-messages.js +6 -5
  39. package/dist/express/src/server/translator/create-translator.js +5 -5
  40. package/dist/next/src/adapters/next/navigation/link.js +9 -10
  41. package/dist/next/src/adapters/next/navigation/redirect.js +7 -11
  42. package/dist/next/src/adapters/next/navigation/use-router.js +18 -14
  43. package/dist/next/src/adapters/next/proxy/intor-proxy.js +17 -6
  44. package/dist/next/src/core/constants/headers.js +1 -0
  45. package/dist/next/src/core/error/intor-error.js +1 -1
  46. package/dist/next/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  47. package/dist/next/src/core/messages/merge-messages.js +33 -0
  48. package/dist/next/src/core/utils/deep-merge.js +35 -19
  49. package/dist/next/src/core/utils/resolve-loader-options.js +9 -0
  50. package/dist/next/src/policies/shoud-full-reload.js +14 -0
  51. package/dist/next/src/routing/inbound/resolve-inbound.js +10 -7
  52. package/dist/next/src/routing/inbound/resolve-locale/resolve-locale.js +5 -12
  53. package/dist/next/src/routing/inbound/resolve-pathname/resolve-pathname.js +9 -19
  54. package/dist/next/src/routing/inbound/resolve-pathname/strategies/all.js +24 -13
  55. package/dist/next/src/routing/inbound/resolve-pathname/strategies/except-default.js +25 -23
  56. package/dist/next/src/routing/inbound/resolve-pathname/strategies/none.js +1 -7
  57. package/dist/next/src/routing/locale/get-locale-from-query.js +2 -2
  58. package/dist/next/src/routing/navigation/decide-strategy.js +17 -0
  59. package/dist/next/src/routing/navigation/derive-target.js +58 -0
  60. package/dist/next/src/routing/navigation/resolve-navigation.js +26 -0
  61. package/dist/next/src/routing/navigation/utils/derive-host-destination.js +13 -0
  62. package/dist/next/src/routing/navigation/utils/derive-query-destination.js +11 -0
  63. package/dist/next/src/routing/pathname/locale-prefix-pathname.js +11 -11
  64. package/dist/next/src/server/messages/load-local-messages/load-local-messages.js +14 -13
  65. package/dist/next/src/server/messages/load-local-messages/read-locale-messages/read-locale-messages.js +1 -1
  66. package/dist/next/src/server/messages/load-messages.js +7 -6
  67. package/dist/next/src/server/translator/create-translator.js +5 -5
  68. package/dist/react/export/react/index.js +2 -1
  69. package/dist/react/src/client/react/helpers/{use-load-messages.js → use-runtime-state.js} +11 -10
  70. package/dist/react/src/client/react/navigation/use-execute-navigation.js +41 -0
  71. package/dist/react/src/client/react/navigation/use-resolve-navigation.js +4 -4
  72. package/dist/react/src/client/react/provider/effects/use-locale-effects.js +5 -2
  73. package/dist/react/src/client/shared/messages/create-refetch-messages.js +6 -6
  74. package/dist/react/src/core/error/intor-error.js +1 -1
  75. package/dist/react/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  76. package/dist/react/src/core/messages/merge-messages.js +33 -0
  77. package/dist/react/src/core/utils/deep-merge.js +35 -19
  78. package/dist/react/src/policies/shoud-full-reload.js +1 -1
  79. package/dist/react/src/policies/should-sync-locale.js +8 -0
  80. package/dist/react/src/routing/navigation/derive-target.js +15 -15
  81. package/dist/react/src/routing/navigation/resolve-navigation.js +3 -2
  82. package/dist/react/src/routing/navigation/utils/derive-host-destination.js +3 -3
  83. package/dist/react/src/routing/navigation/utils/derive-query-destination.js +2 -2
  84. package/dist/react/src/routing/pathname/locale-prefix-pathname.js +11 -11
  85. package/dist/svelte/export/svelte/index.js +1 -1
  86. package/dist/svelte/src/client/shared/messages/create-refetch-messages.js +6 -6
  87. package/dist/svelte/src/client/svelte/helpers/{create-messages.js → create-runtime-state.js} +9 -8
  88. package/dist/svelte/src/client/svelte/runtime/effects/locale-effects.js +4 -1
  89. package/dist/svelte/src/core/error/intor-error.js +1 -1
  90. package/dist/svelte/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  91. package/dist/svelte/src/core/messages/merge-messages.js +33 -0
  92. package/dist/svelte/src/core/utils/deep-merge.js +35 -19
  93. package/dist/types/export/index.d.ts +1 -1
  94. package/dist/types/export/react/index.d.ts +1 -1
  95. package/dist/types/export/svelte/index.d.ts +1 -1
  96. package/dist/types/export/vue/index.d.ts +1 -1
  97. package/dist/types/src/adapters/express/middleware/create-intor.d.ts +1 -1
  98. package/dist/types/src/adapters/next/proxy/intor-proxy.d.ts +2 -0
  99. package/dist/types/src/client/react/helpers/index.d.ts +1 -1
  100. package/dist/types/src/client/react/helpers/use-runtime-state.d.ts +10 -0
  101. package/dist/types/src/client/react/index.d.ts +2 -2
  102. package/dist/types/src/client/react/navigation/index.d.ts +1 -0
  103. package/dist/types/src/client/react/navigation/use-execute-navigation.d.ts +5 -0
  104. package/dist/types/src/client/react/navigation/use-resolve-navigation.d.ts +4 -6
  105. package/dist/types/src/client/react/provider/types.d.ts +6 -3
  106. package/dist/types/src/client/shared/types/index.d.ts +1 -1
  107. package/dist/types/src/client/shared/types/runtime-state.d.ts +13 -0
  108. package/dist/types/src/client/svelte/helpers/create-runtime-state.d.ts +11 -0
  109. package/dist/types/src/client/svelte/helpers/index.d.ts +1 -1
  110. package/dist/types/src/client/svelte/index.d.ts +1 -1
  111. package/dist/types/src/client/svelte/runtime/types.d.ts +6 -3
  112. package/dist/types/src/client/vue/helpers/index.d.ts +1 -1
  113. package/dist/types/src/client/vue/helpers/use-runtime-state.d.ts +10 -0
  114. package/dist/types/src/client/vue/index.d.ts +1 -1
  115. package/dist/types/src/client/vue/provider/resolver/resolve-runtime.d.ts +10 -0
  116. package/dist/types/src/client/vue/provider/types.d.ts +8 -3
  117. package/dist/types/src/config/define-intor-config.d.ts +6 -12
  118. package/dist/types/src/config/resolvers/resolve-fallback-locales.d.ts +4 -16
  119. package/dist/types/src/config/resolvers/resolve-routing-options.d.ts +9 -2
  120. package/dist/types/src/config/types/index.d.ts +1 -1
  121. package/dist/types/src/config/types/intor-config.d.ts +16 -3
  122. package/dist/types/src/config/types/routing.d.ts +60 -73
  123. package/dist/types/src/config/types/translator.d.ts +2 -2
  124. package/dist/types/src/config/validators/validate-default-locale.d.ts +3 -9
  125. package/dist/types/src/config/validators/validate-id.d.ts +6 -0
  126. package/dist/types/src/config/validators/validate-supported-locales.d.ts +3 -8
  127. package/dist/types/src/core/constants/headers.d.ts +1 -0
  128. package/dist/types/src/core/error/intor-error.d.ts +1 -1
  129. package/dist/types/src/core/index.d.ts +3 -3
  130. package/dist/types/src/core/messages/index.d.ts +1 -0
  131. package/dist/types/src/core/messages/load-remote-messages/load-remote-messages.d.ts +5 -5
  132. package/dist/types/src/core/messages/load-remote-messages/types.d.ts +1 -0
  133. package/dist/types/src/core/messages/merge-messages.d.ts +19 -0
  134. package/dist/types/src/core/types/bootstrap.d.ts +13 -0
  135. package/dist/types/src/core/types/index.d.ts +2 -1
  136. package/dist/types/src/core/types/routing.d.ts +5 -6
  137. package/dist/types/src/core/types/translator-instance.d.ts +8 -3
  138. package/dist/types/src/core/utils/deep-merge.d.ts +20 -6
  139. package/dist/types/src/core/utils/index.d.ts +1 -2
  140. package/dist/types/src/policies/index.d.ts +2 -0
  141. package/dist/types/src/policies/should-sync-locale.d.ts +4 -0
  142. package/dist/types/src/routing/inbound/resolve-inbound.d.ts +2 -2
  143. package/dist/types/src/routing/inbound/resolve-locale/resolve-locale.d.ts +3 -10
  144. package/dist/types/src/routing/inbound/resolve-pathname/resolve-pathname.d.ts +3 -13
  145. package/dist/types/src/routing/inbound/resolve-pathname/strategies/all.d.ts +1 -1
  146. package/dist/types/src/routing/inbound/resolve-pathname/strategies/except-default.d.ts +1 -1
  147. package/dist/types/src/routing/inbound/resolve-pathname/strategies/none.d.ts +2 -2
  148. package/dist/types/src/routing/inbound/resolve-pathname/types.d.ts +7 -4
  149. package/dist/types/src/routing/navigation/derive-target.d.ts +1 -1
  150. package/dist/types/src/routing/navigation/resolve-navigation.d.ts +4 -1
  151. package/dist/types/src/routing/pathname/locale-prefix-pathname.d.ts +3 -3
  152. package/dist/types/src/server/intor/index.d.ts +1 -1
  153. package/dist/types/src/server/intor/intor.d.ts +2 -2
  154. package/dist/types/src/server/intor/types.d.ts +3 -5
  155. package/dist/types/src/server/messages/load-local-messages/load-local-messages.d.ts +5 -2
  156. package/dist/types/src/server/messages/load-local-messages/types.d.ts +2 -2
  157. package/dist/vue/export/vue/index.js +1 -1
  158. package/dist/vue/src/client/shared/messages/create-refetch-messages.js +6 -6
  159. package/dist/vue/src/client/vue/helpers/{use-load-messages.js → use-runtime-state.js} +12 -8
  160. package/dist/vue/src/client/vue/provider/effects/use-locale-effects.js +4 -1
  161. package/dist/vue/src/client/vue/provider/intor-provider.js +8 -6
  162. package/dist/vue/src/client/vue/provider/resolver/resolve-runtime.js +32 -0
  163. package/dist/vue/src/core/error/intor-error.js +1 -1
  164. package/dist/vue/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  165. package/dist/vue/src/core/messages/merge-messages.js +33 -0
  166. package/dist/vue/src/core/utils/deep-merge.js +35 -19
  167. package/package.json +2 -2
  168. package/dist/types/src/client/react/helpers/use-load-messages.d.ts +0 -11
  169. package/dist/types/src/client/shared/types/bootstrap.d.ts +0 -16
  170. package/dist/types/src/client/svelte/helpers/create-messages.d.ts +0 -12
  171. package/dist/types/src/client/vue/helpers/use-load-messages.d.ts +0 -12
  172. /package/dist/next/src/{core → routing/navigation}/utils/is-external-destination.js +0 -0
  173. /package/dist/react/src/{core → routing/navigation}/utils/is-external-destination.js +0 -0
  174. /package/dist/types/src/{core → routing/navigation}/utils/is-external-destination.d.ts +0 -0
package/README.md CHANGED
@@ -15,9 +15,3 @@ Fast to start, easy to extend, and free from the usual i18n heaviness.
15
15
  [![License](https://img.shields.io/npm/l/intor?style=flat&colorA=000000&colorB=000000)](LICENSE)
16
16
 
17
17
  </div>
18
-
19
- <div align="center">
20
-
21
- #### 🍳 Cooking the Intor v2 docs, crafting them to perfection...
22
-
23
- </div>
@@ -1,10 +1,12 @@
1
1
  export { PREFIX_PLACEHOLDER } from '../src/core/constants/prefix-placeholder.js';
2
2
  export { IntorError, IntorErrorCode } from '../src/core/error/intor-error.js';
3
3
  export { deepMerge } from '../src/core/utils/deep-merge.js';
4
+ export { resolveLoaderOptions } from '../src/core/utils/resolve-loader-options.js';
4
5
  import 'logry';
5
6
  export { clearLoggerPool } from '../src/core/logger/global-logger-pool.js';
6
7
  export { isValidMessages } from '../src/core/messages/utils/is-valid-messages.js';
7
8
  export { clearMessagesPool, setGlobalMessagesPool } from '../src/core/messages/global-messages-pool.js';
9
+ export { mergeMessages } from '../src/core/messages/merge-messages.js';
8
10
  export { defineIntorConfig } from '../src/config/define-intor-config.js';
9
11
  import '../src/config/constants/cookie.js';
10
12
  import '../src/config/constants/cache.js';
@@ -1,8 +1,6 @@
1
1
  export { intor } from '../../src/server/intor/intor.js';
2
2
  export { loadMessages } from '../../src/server/messages/load-messages.js';
3
3
  import 'intor-translator';
4
- import '../../src/core/error/intor-error.js';
5
4
  import 'logry';
6
- import 'keyv';
7
5
  export { getTranslator } from '../../src/server/helpers/get-translator.js';
8
6
  export { loadMessagesFromUrl } from '../../src/server/helpers/local-messages-from-url.js';
@@ -1,22 +1,18 @@
1
1
  // Default routing options
2
2
  const DEFAULT_ROUTING_OPTIONS = {
3
- locale: {
4
- sources: ["path", "query", "cookie", "detected"],
5
- query: { key: "locale" },
6
- },
7
- navigation: {
8
- carrier: "path",
9
- path: { prefix: "none" },
10
- query: { key: "locale" },
11
- host: { map: {} },
3
+ basePath: "/",
4
+ localePrefix: "none",
5
+ inbound: {
6
+ localeSources: ["path", "query", "cookie", "detected"],
7
+ queryKey: "locale",
8
+ firstVisit: { localeSource: "browser", persist: true, redirect: true },
12
9
  },
13
- firstVisit: {
14
- localeSource: "browser",
15
- redirect: true,
16
- persist: true,
10
+ outbound: {
11
+ localeCarrier: "path",
12
+ queryKey: "locale",
13
+ host: { map: {}, default: undefined },
14
+ forceFullReload: false,
17
15
  },
18
- basePath: "/",
19
- forceFullReload: false,
20
16
  };
21
17
 
22
18
  export { DEFAULT_ROUTING_OPTIONS };
@@ -3,34 +3,34 @@ import { resolveCookieOptions } from './resolvers/resolve-cookie-options.js';
3
3
  import { resolveFallbackLocales } from './resolvers/resolve-fallback-locales.js';
4
4
  import { resolveRoutingOptions } from './resolvers/resolve-routing-options.js';
5
5
  import { validateDefaultLocale } from './validators/validate-default-locale.js';
6
+ import { validateId } from './validators/validate-id.js';
6
7
  import { validateSupportedLocales } from './validators/validate-supported-locales.js';
7
8
 
8
9
  /**
9
- * Defines and resolves an Intor configuration.
10
+ * Defines the canonical Intor configuration.
10
11
  *
11
- * This is the canonical entry point for transforming a raw configuration
12
- * into a fully validated and resolved Intor config.
12
+ * Transforms a raw config into a validated and runtime-ready form by:
13
+ * - enforcing required invariants
14
+ * - resolving derived options
15
+ * - applying stable defaults
13
16
  *
14
- * Responsibilities:
15
- * - Validate required invariants
16
- * - Resolve and normalize derived options
17
- * - Apply stable defaults
18
- *
19
- * This function is purely declarative and side-effect free.
20
- * It does not create runtime instances or attach dynamic behavior.
21
- *
22
- * Intended to run once during application initialization.
17
+ * This function is declarative and side-effect free.
23
18
  */
24
19
  const defineIntorConfig = (config) => {
20
+ // -----------------------------------------------------------------------------
25
21
  // Validators
26
- const supportedLocales = validateSupportedLocales(config);
27
- const defaultLocale = validateDefaultLocale(config, supportedLocales);
22
+ // -----------------------------------------------------------------------------
23
+ const id = validateId(config.id ?? "default");
24
+ const supportedLocales = validateSupportedLocales(id, config.supportedLocales);
25
+ const supportedSet = new Set(supportedLocales);
26
+ const defaultLocale = validateDefaultLocale(id, config.defaultLocale, supportedSet);
27
+ // -----------------------------------------------------------------------------
28
28
  // Resolvers
29
- const fallbackLocales = resolveFallbackLocales(config, supportedLocales);
29
+ // -----------------------------------------------------------------------------
30
+ const fallbackLocales = resolveFallbackLocales(config, id, supportedSet);
30
31
  const cookie = resolveCookieOptions(config.cookie);
31
32
  const routing = resolveRoutingOptions(config.routing);
32
33
  const cache = resolveCacheOptions(config.cache);
33
- const id = config.id ?? "default";
34
34
  return {
35
35
  id,
36
36
  messages: config.messages,
@@ -1,50 +1,60 @@
1
+ import '../../core/error/intor-error.js';
2
+ import { getLogger } from '../../core/logger/get-logger.js';
3
+ import 'keyv';
4
+
1
5
  /**
2
- * Resolves and normalizes fallback locale mappings.
3
- *
4
- * This utility validates the user-defined `fallbackLocales` configuration
5
- * against the provided `supportedLocales` list and the configured `defaultLocale`.
6
- *
7
- * Resolution rules:
8
- * - Only locales listed in `supportedLocales` are considered valid fallbacks.
9
- * - The literal value `"default"` is always allowed and represents the configured default locale.
10
- * - Fallback entries matching `defaultLocale` are treated as valid.
11
- * - Invalid fallback locales are ignored and reported via warnings.
6
+ * Resolves fallbackLocales into a runtime-safe mapping.
12
7
  *
13
- * Notes:
14
- * - This function is purely defensive and does not throw on invalid input.
15
- * - Only validated fallback mappings are included in the returned result.
16
- * - Logging is used for diagnostics only and does not affect the output.
8
+ * Invalid entries are ignored and reported via warnings.
17
9
  */
18
- const resolveFallbackLocales = (config, supportedLocales) => {
10
+ const resolveFallbackLocales = (config, id, supportedSet) => {
19
11
  const { defaultLocale, fallbackLocales } = config;
12
+ // Fast exit: no fallback configuration provided
20
13
  if (!fallbackLocales || typeof fallbackLocales !== "object") {
21
14
  return {};
22
15
  }
23
- const supportedSet = new Set(supportedLocales);
24
- const validMap = {}; // Collected valid fallbacks
25
- const invalidFallbackMap = new Map(); // Track invalid ones
16
+ const logger = getLogger({ id }).child({
17
+ scope: "resolve-fallback-locales",
18
+ });
19
+ const resolvedMap = {};
20
+ const invalidMap = new Map();
21
+ // ---------------------------------------------------------------------------
22
+ // Normalize each fallback rule
23
+ // ---------------------------------------------------------------------------
26
24
  for (const [locale, fallbacks] of Object.entries(fallbackLocales)) {
27
- const fallbackArray = Array.isArray(fallbacks) ? fallbacks : [];
28
- // Valid: in supported list, matches default, or literal "default"
29
- const validFallbacks = fallbackArray.filter((fallback) => fallback === "default" ||
30
- supportedSet.has(fallback) ||
31
- fallback === defaultLocale);
32
- // Invalid: not "default", not supported, and not defaultLocale
33
- const invalidFallbacks = fallbackArray.filter((fallback) => fallback !== "default" &&
34
- !supportedSet.has(fallback) &&
35
- fallback !== defaultLocale);
36
- if (invalidFallbacks.length > 0) {
37
- invalidFallbackMap.set(locale, invalidFallbacks);
25
+ // Guard: fallback rules for unreachable locales are ignored.
26
+ if (!supportedSet.has(locale) && locale !== defaultLocale) {
27
+ logger.warn(`Fallback locale "${locale}" is not listed in supportedLocales.`);
28
+ continue;
38
29
  }
39
- if (validFallbacks.length > 0) {
40
- validMap[locale] = validFallbacks;
30
+ const fallbackArray = Array.isArray(fallbacks) ? fallbacks : [];
31
+ const validFallbacks = [];
32
+ const invalidFallbacks = [];
33
+ for (const fallback of fallbackArray) {
34
+ // A fallback target is considered valid if:
35
+ // - It exists in `supportedLocales`
36
+ // - It is the literal value "default"
37
+ if (fallback === "default" || supportedSet.has(fallback)) {
38
+ validFallbacks.push(fallback);
39
+ }
40
+ else {
41
+ invalidFallbacks.push(fallback);
42
+ }
41
43
  }
44
+ if (invalidFallbacks.length > 0)
45
+ invalidMap.set(locale, invalidFallbacks);
46
+ if (validFallbacks.length > 0)
47
+ resolvedMap[locale] = validFallbacks;
42
48
  }
43
- // Log out invalid fallback locales
44
- for (const [locale, invalids] of invalidFallbackMap.entries()) {
45
- console.warn(`Invalid fallback locales for "${locale}"`, { invalids: [...invalids] }, { scope: "resolveFallbackLocales" });
49
+ // ---------------------------------------------------------------------------
50
+ // Diagnostics: report ignored fallback entries
51
+ // ---------------------------------------------------------------------------
52
+ for (const [locale, invalids] of invalidMap.entries()) {
53
+ logger.warn(`Invalid fallback locales for "${locale}".`, {
54
+ invalids: invalids.join(", "),
55
+ });
46
56
  }
47
- return validMap;
57
+ return resolvedMap;
48
58
  };
49
59
 
50
60
  export { resolveFallbackLocales };
@@ -1,45 +1,65 @@
1
1
  import '../../core/error/intor-error.js';
2
- import { normalizePathname } from '../../core/utils/normalizers/normalize-pathname.js';
2
+ import { deepMerge } from '../../core/utils/deep-merge.js';
3
3
  import 'logry';
4
4
  import 'keyv';
5
5
  import { DEFAULT_ROUTING_OPTIONS } from '../constants/routing.js';
6
6
  import '../constants/cookie.js';
7
7
  import '../constants/cache.js';
8
8
 
9
- const resolveRoutingOptions = (routing = {}) => {
10
- return {
11
- ...DEFAULT_ROUTING_OPTIONS,
12
- ...routing,
13
- locale: {
14
- ...DEFAULT_ROUTING_OPTIONS.locale,
15
- ...routing.locale,
16
- query: {
17
- ...DEFAULT_ROUTING_OPTIONS.locale.query,
18
- ...routing.locale?.query,
19
- },
20
- },
21
- navigation: {
22
- ...DEFAULT_ROUTING_OPTIONS.navigation,
23
- ...routing.navigation,
24
- path: {
25
- ...DEFAULT_ROUTING_OPTIONS.navigation.path,
26
- ...routing.navigation?.path,
27
- },
28
- query: {
29
- ...DEFAULT_ROUTING_OPTIONS.navigation.query,
30
- ...routing.navigation?.query,
31
- },
32
- host: {
33
- ...DEFAULT_ROUTING_OPTIONS.navigation.host,
34
- ...routing.navigation?.host,
35
- },
36
- },
37
- firstVisit: {
38
- ...DEFAULT_ROUTING_OPTIONS.firstVisit,
39
- ...routing.firstVisit,
40
- },
41
- basePath: normalizePathname(routing?.basePath || ""),
42
- };
43
- };
9
+ /**
10
+ * Resolves routing configuration into a fully normalized form.
11
+ *
12
+ * - Flat shortcuts are projected into structured options.
13
+ * - Structured options override projected values.
14
+ * - Defaults are applied last.
15
+ */
16
+ function resolveRoutingOptions(raw) {
17
+ if (!raw)
18
+ return DEFAULT_ROUTING_OPTIONS;
19
+ const projectedFromFlat = projectFlatToStructured(raw);
20
+ const structuredOverrides = stripFlatShortcuts(raw);
21
+ const defined = deepMerge(projectedFromFlat, structuredOverrides);
22
+ return deepMerge(DEFAULT_ROUTING_OPTIONS, defined);
23
+ }
24
+ /** Assigns a value to the target only if it is explicitly defined. */
25
+ function assignIfDefined(target, key, value) {
26
+ if (value !== undefined)
27
+ target[key] = value;
28
+ }
29
+ /** Projects flat routing shortcuts into structured options. */
30
+ function projectFlatToStructured(flat) {
31
+ const structured = {};
32
+ assignIfDefined(structured, "basePath", flat.basePath);
33
+ assignIfDefined(structured, "localePrefix", flat.localePrefix);
34
+ // inbound
35
+ const inbound = {};
36
+ assignIfDefined(inbound, "localeSources", flat.localeSources);
37
+ assignIfDefined(inbound, "queryKey", flat.queryKey);
38
+ assignIfDefined(inbound, "firstVisit", flat.firstVisit);
39
+ if (Object.keys(inbound).length > 0)
40
+ structured.inbound = inbound;
41
+ // outbound
42
+ const outbound = {};
43
+ assignIfDefined(outbound, "localeCarrier", flat.localeCarrier);
44
+ assignIfDefined(outbound, "queryKey", flat.queryKey);
45
+ assignIfDefined(outbound, "host", flat.host);
46
+ assignIfDefined(outbound, "forceFullReload", flat.forceFullReload);
47
+ if (Object.keys(outbound).length > 0)
48
+ structured.outbound = outbound;
49
+ return structured;
50
+ }
51
+ /** Removes flat shortcuts, preserving structured overrides only. */
52
+ function stripFlatShortcuts(raw) {
53
+ const structured = {};
54
+ if ("basePath" in raw)
55
+ structured.basePath = raw.basePath;
56
+ if ("localePrefix" in raw)
57
+ structured.localePrefix = raw.localePrefix;
58
+ if ("inbound" in raw)
59
+ structured.inbound = raw.inbound;
60
+ if ("outbound" in raw)
61
+ structured.outbound = raw.outbound;
62
+ return structured;
63
+ }
44
64
 
45
65
  export { resolveRoutingOptions };
@@ -3,30 +3,16 @@ import 'logry';
3
3
  import 'keyv';
4
4
 
5
5
  /**
6
- * Validates the configured default locale.
6
+ * Validates that the configured defaultLocale is supported.
7
7
  *
8
- * - Ensures that `defaultLocale` is explicitly defined.
9
- * - Ensures that `defaultLocale` is included in `supportedLocales`.
10
- *
11
- * This validation is part of the configuration initialization phase
12
- * and is expected to fail fast when misconfigured.
8
+ * Fails fast if `defaultLocale` is not included in `supportedLocales`.
13
9
  */
14
- const validateDefaultLocale = (config, supportedLocales) => {
15
- const { id, defaultLocale } = config;
16
- // Throw error if defaultLocale is undefined
17
- if (!defaultLocale) {
18
- throw new IntorError({
19
- id,
20
- code: IntorErrorCode.MISSING_DEFAULT_LOCALE,
21
- message: `The defaultLocale is undefined`,
22
- });
23
- }
24
- // Throw error if defaultLocale is not listed in supportedLocales
25
- if (!supportedLocales.includes(defaultLocale)) {
10
+ const validateDefaultLocale = (id, defaultLocale, supportedSet) => {
11
+ if (!supportedSet.has(defaultLocale)) {
26
12
  throw new IntorError({
27
13
  id,
28
14
  code: IntorErrorCode.UNSUPPORTED_DEFAULT_LOCALE,
29
- message: `The defaultLocale "${defaultLocale}" is not included in the supportedLocales.`,
15
+ message: `"defaultLocale" must be included in "supportedLocales".`,
30
16
  });
31
17
  }
32
18
  return defaultLocale;
@@ -0,0 +1,21 @@
1
+ import { IntorError, IntorErrorCode } from '../../core/error/intor-error.js';
2
+ import 'logry';
3
+ import 'keyv';
4
+
5
+ /**
6
+ * Validates that the given id is a non-empty string.
7
+ *
8
+ * Fails fast if the id is empty or whitespace-only.
9
+ */
10
+ const validateId = (id) => {
11
+ if (id.trim() === "") {
12
+ throw new IntorError({
13
+ id,
14
+ code: IntorErrorCode.INVALID_CONFIG_ID,
15
+ message: `"id" must be a non-empty string.`,
16
+ });
17
+ }
18
+ return id;
19
+ };
20
+
21
+ export { validateId };
@@ -3,29 +3,19 @@ import 'logry';
3
3
  import 'keyv';
4
4
 
5
5
  /**
6
- * Validates and resolves the list of supported locales.
6
+ * Validates that supportedLocales is provided and non-empty.
7
7
  *
8
- * - Ensures `supportedLocales` is explicitly provided when a loader is used.
9
- * - Falls back to inferring locales from static message keys when no loader
10
- * is configured.
11
- *
12
- * This validation runs during configuration initialization and is expected
13
- * to fail fast when required inputs are missing.
8
+ * Fails fast when missing.
14
9
  */
15
- const validateSupportedLocales = (config) => {
16
- const { id, supportedLocales } = config;
17
- const hasAnyLoader = !!config.loader || !!config.server?.loader || !!config.client?.loader;
18
- // Ensure supportedLocales is set when using loader
19
- if (hasAnyLoader && !supportedLocales) {
10
+ const validateSupportedLocales = (id, supportedLocales) => {
11
+ if (!supportedLocales || supportedLocales.length === 0) {
20
12
  throw new IntorError({
21
13
  id,
22
14
  code: IntorErrorCode.MISSING_SUPPORTED_LOCALES,
23
- message: `"supportedLocales" is required when using message loaders.
24
- Please specify all supported locales explicitly.`,
15
+ message: `"supportedLocales" must be specified.`,
25
16
  });
26
17
  }
27
- // Return supportedLocales or infer from message keys
28
- return supportedLocales || Object.keys(config.messages || {});
18
+ return supportedLocales;
29
19
  };
30
20
 
31
21
  export { validateSupportedLocales };
@@ -13,8 +13,8 @@ class IntorError extends Error {
13
13
  var IntorErrorCode;
14
14
  (function (IntorErrorCode) {
15
15
  // config
16
+ IntorErrorCode["INVALID_CONFIG_ID"] = "INTOR_INVALID_CONFIG_ID";
16
17
  IntorErrorCode["MISSING_SUPPORTED_LOCALES"] = "INTOR_MISSING_SUPPORTED_LOCALES";
17
- IntorErrorCode["MISSING_DEFAULT_LOCALE"] = "INTOR_MISSING_DEFAULT_LOCALE";
18
18
  IntorErrorCode["UNSUPPORTED_DEFAULT_LOCALE"] = "INTOR_UNSUPPORTED_DEFAULT_LOCALE";
19
19
  // runtime
20
20
  IntorErrorCode["RUNTIME_NOT_INITIALIZED"] = "INTOR_RUNTIME_NOT_INITIALIZED";
@@ -5,16 +5,16 @@ import { fetchLocaleMessages } from './fetch-locale-messages/fetch-locale-messag
5
5
  /**
6
6
  * Load locale messages from a remote API.
7
7
  *
8
- * This function acts as the orchestration layer for remote message loading.
9
- * It is responsible for:
8
+ * This function serves as the orchestration layer for remote message loading.
9
+ * It coordinates:
10
10
  *
11
- * - Resolving fallback locales in order
12
- * - Coordinating cache read / write behavior
11
+ * - Locale resolution with fallbacks
12
+ * - Cache read / write behavior
13
13
  * - Respecting abort signals across the entire async flow
14
14
  *
15
15
  * Network fetching and data validation are delegated to lower-level utilities.
16
16
  */
17
- const loadRemoteMessages = async ({ locale, fallbackLocales, namespaces, rootDir, url, headers, signal, pool, cacheOptions, allowCacheWrite = false, loggerOptions, }) => {
17
+ const loadRemoteMessages = async ({ id, locale, fallbackLocales, namespaces, rootDir, url, headers, signal, pool, cacheOptions, allowCacheWrite = false, loggerOptions, }) => {
18
18
  const baseLogger = getLogger(loggerOptions);
19
19
  const logger = baseLogger.child({ scope: "load-remote-messages" });
20
20
  // Abort early if the request has already been cancelled
@@ -24,16 +24,20 @@ const loadRemoteMessages = async ({ locale, fallbackLocales, namespaces, rootDir
24
24
  }
25
25
  const start = performance.now();
26
26
  logger.debug("Loading remote messages.", { url });
27
- // --- Cache key ---
27
+ // ---------------------------------------------------------------------------
28
+ // Cache key resolution
29
+ // ---------------------------------------------------------------------------
28
30
  const cacheKey = normalizeCacheKey([
29
- loggerOptions.id,
31
+ id,
30
32
  "loaderType:remote",
31
33
  rootDir,
32
34
  locale,
33
- (fallbackLocales ?? []).toSorted().join(","),
34
- (namespaces ?? []).toSorted().join(","),
35
+ (fallbackLocales || []).toSorted().join(","),
36
+ (namespaces || []).toSorted().join(","),
35
37
  ]);
36
- // --- Cache read ---
38
+ // ---------------------------------------------------------------------------
39
+ // Cache read
40
+ // ---------------------------------------------------------------------------
37
41
  if (cacheOptions.enabled && cacheKey) {
38
42
  const cached = await pool?.get(cacheKey);
39
43
  if (signal?.aborted) {
@@ -45,9 +49,11 @@ const loadRemoteMessages = async ({ locale, fallbackLocales, namespaces, rootDir
45
49
  return cached;
46
50
  }
47
51
  }
52
+ // ---------------------------------------------------------------------------
53
+ // Resolve locale messages with ordered fallback strategy
54
+ // ---------------------------------------------------------------------------
48
55
  const candidateLocales = [locale, ...(fallbackLocales || [])];
49
56
  let messages;
50
- // Try each candidate locale in order and stop at the first successful result
51
57
  for (let i = 0; i < candidateLocales.length; i++) {
52
58
  const candidateLocale = candidateLocales[i];
53
59
  const isLast = i === candidateLocales.length - 1;
@@ -87,13 +93,17 @@ const loadRemoteMessages = async ({ locale, fallbackLocales, namespaces, rootDir
87
93
  });
88
94
  }
89
95
  }
90
- // --- Cache write ---
91
- if (cacheOptions.enabled && allowCacheWrite && cacheKey && messages) {
96
+ // ---------------------------------------------------------------------------
97
+ // Cache write (explicitly permitted)
98
+ // ---------------------------------------------------------------------------
99
+ if (cacheOptions.enabled && allowCacheWrite) {
92
100
  if (signal?.aborted) {
93
101
  logger.debug("Remote message loading aborted before cache write.");
94
102
  return;
95
103
  }
96
- await pool?.set(cacheKey, messages, cacheOptions.ttl);
104
+ if (cacheKey && messages) {
105
+ await pool?.set(cacheKey, messages, cacheOptions.ttl);
106
+ }
97
107
  }
98
108
  // Final success log with resolved locale and timing
99
109
  if (messages) {
@@ -0,0 +1,33 @@
1
+ import { getLogger } from '../logger/get-logger.js';
2
+ import { deepMerge } from '../utils/deep-merge.js';
3
+
4
+ /**
5
+ * Merge locale-specific messages with runtime overrides.
6
+ *
7
+ * - Only merges messages under the given locale
8
+ * - Emits debug logs for add / override events
9
+ */
10
+ function mergeMessages(a, b, { config, locale, onEvent }) {
11
+ const baseLogger = getLogger({ ...config.logger, id: config.id });
12
+ const logger = baseLogger.child({ scope: "merge-messages" });
13
+ // Merge messages for the active locale only
14
+ const merged = deepMerge(a?.[locale] ?? {}, b?.[locale] ?? {}, {
15
+ onOverride: (event) => {
16
+ if (onEvent) {
17
+ onEvent(event);
18
+ return;
19
+ }
20
+ const { kind, path, next, prev } = event;
21
+ if (kind === "add")
22
+ return;
23
+ logger.debug(`Override | ${locale}: "${path}"`, { prev, next });
24
+ },
25
+ });
26
+ // Preserve other locales, update only the target one
27
+ return {
28
+ ...a,
29
+ [locale]: merged,
30
+ };
31
+ }
32
+
33
+ export { mergeMessages };
@@ -1,28 +1,44 @@
1
1
  /**
2
- * Deeply merges two objects.
2
+ * Deeply merges two plain objects.
3
3
  *
4
- * - Nested objects → merged recursively
5
- * - Array / primitive → b overwrites a
4
+ * - Nested plain objects → merged recursively
5
+ * - Arrays / primitives`b` overwrites `a`
6
6
  *
7
- * This function always returns a plain object.
7
+ * Debug behavior (optional):
8
+ * - Emits override events via `onOverride`
9
+ * - Zero overhead when no options are provided
10
+ *
11
+ * This function always returns a new plain object.
8
12
  */
9
- const deepMerge = (a = {}, b = {}) => {
13
+ const deepMerge = (a = {}, b = {}, options) => {
10
14
  const result = { ...a };
15
+ const basePath = options?._path ?? [];
16
+ // Iterate only over b's own enumerable properties
11
17
  for (const key in b) {
12
- if (Object.prototype.hasOwnProperty.call(b, key)) {
13
- const av = a[key];
14
- const bv = b[key];
15
- if (av &&
16
- bv &&
17
- typeof av === "object" &&
18
- typeof bv === "object" &&
19
- !Array.isArray(av) &&
20
- !Array.isArray(bv)) {
21
- result[key] = deepMerge(av, bv);
22
- }
23
- else {
24
- result[key] = bv;
25
- }
18
+ if (!Object.prototype.hasOwnProperty.call(b, key))
19
+ continue;
20
+ const aValue = a[key];
21
+ const bValue = b[key];
22
+ const nextPath = [...basePath, key];
23
+ // Recursively merge when both sides are plain objects
24
+ if (aValue &&
25
+ bValue &&
26
+ typeof aValue === "object" &&
27
+ typeof bValue === "object" &&
28
+ !Array.isArray(aValue) &&
29
+ !Array.isArray(bValue)) {
30
+ result[key] = deepMerge(aValue, bValue, options ? { ...options, _path: nextPath } : undefined);
31
+ }
32
+ else {
33
+ // Emit override event only when debugging is enabled
34
+ const isAdd = aValue === undefined;
35
+ options?.onOverride?.({
36
+ path: nextPath.join("."),
37
+ prev: aValue,
38
+ next: bValue,
39
+ kind: isAdd ? "add" : "override",
40
+ });
41
+ result[key] = bValue;
26
42
  }
27
43
  }
28
44
  return result;
@@ -18,6 +18,15 @@
18
18
  * for the given runtime.
19
19
  */
20
20
  const resolveLoaderOptions = (config, runtime) => {
21
+ // --- runtime: client ---
22
+ if (runtime === "client") {
23
+ const client = config.client?.loader;
24
+ if (client) {
25
+ // Client loader is always remote by design
26
+ return { type: "remote", ...client };
27
+ }
28
+ return config.server?.loader ?? config.loader;
29
+ }
21
30
  // --- runtime: server ---
22
31
  return config.server?.loader ?? config.loader;
23
32
  };