intor 2.3.9 → 2.3.10

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 (103) 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/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  5. package/dist/core/src/core/messages/merge-messages.js +33 -0
  6. package/dist/core/src/core/utils/deep-merge.js +35 -19
  7. package/dist/core/src/core/utils/resolve-loader-options.js +9 -0
  8. package/dist/core/src/server/helpers/local-messages-from-url.js +1 -0
  9. package/dist/core/src/server/messages/load-local-messages/load-local-messages.js +2 -2
  10. package/dist/core/src/server/messages/load-messages.js +7 -5
  11. package/dist/core/src/server/translator/create-translator.js +5 -5
  12. package/dist/express/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  13. package/dist/express/src/core/messages/merge-messages.js +33 -0
  14. package/dist/express/src/core/utils/deep-merge.js +35 -19
  15. package/dist/express/src/server/messages/load-local-messages/load-local-messages.js +2 -2
  16. package/dist/express/src/server/messages/load-messages.js +6 -4
  17. package/dist/express/src/server/translator/create-translator.js +5 -5
  18. package/dist/next/src/adapters/next/navigation/link.js +9 -10
  19. package/dist/next/src/adapters/next/navigation/redirect.js +7 -11
  20. package/dist/next/src/adapters/next/navigation/use-router.js +18 -14
  21. package/dist/next/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  22. package/dist/next/src/core/messages/merge-messages.js +33 -0
  23. package/dist/next/src/core/utils/deep-merge.js +35 -19
  24. package/dist/next/src/core/utils/resolve-loader-options.js +9 -0
  25. package/dist/next/src/policies/shoud-full-reload.js +14 -0
  26. package/dist/next/src/routing/navigation/decide-strategy.js +17 -0
  27. package/dist/next/src/routing/navigation/derive-target.js +55 -0
  28. package/dist/next/src/routing/navigation/resolve-navigation.js +26 -0
  29. package/dist/next/src/routing/navigation/utils/derive-host-destination.js +13 -0
  30. package/dist/next/src/routing/navigation/utils/derive-query-destination.js +11 -0
  31. package/dist/next/src/server/messages/load-local-messages/load-local-messages.js +2 -2
  32. package/dist/next/src/server/messages/load-messages.js +7 -5
  33. package/dist/next/src/server/translator/create-translator.js +5 -5
  34. package/dist/react/export/react/index.js +2 -1
  35. package/dist/react/src/client/react/helpers/{use-load-messages.js → use-runtime-state.js} +11 -10
  36. package/dist/react/src/client/react/navigation/use-execute-navigation.js +29 -0
  37. package/dist/react/src/client/react/navigation/use-resolve-navigation.js +4 -4
  38. package/dist/react/src/client/shared/messages/create-refetch-messages.js +6 -6
  39. package/dist/react/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  40. package/dist/react/src/core/messages/merge-messages.js +33 -0
  41. package/dist/react/src/core/utils/deep-merge.js +35 -19
  42. package/dist/react/src/routing/navigation/derive-target.js +5 -8
  43. package/dist/react/src/routing/navigation/resolve-navigation.js +3 -2
  44. package/dist/svelte/export/svelte/index.js +1 -1
  45. package/dist/svelte/src/client/shared/messages/create-refetch-messages.js +6 -6
  46. package/dist/svelte/src/client/svelte/helpers/{create-messages.js → create-runtime-state.js} +9 -8
  47. package/dist/svelte/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  48. package/dist/svelte/src/core/messages/merge-messages.js +33 -0
  49. package/dist/svelte/src/core/utils/deep-merge.js +35 -19
  50. package/dist/types/export/index.d.ts +1 -1
  51. package/dist/types/export/react/index.d.ts +1 -1
  52. package/dist/types/export/svelte/index.d.ts +1 -1
  53. package/dist/types/export/vue/index.d.ts +1 -1
  54. package/dist/types/src/client/react/helpers/index.d.ts +1 -1
  55. package/dist/types/src/client/react/helpers/use-runtime-state.d.ts +10 -0
  56. package/dist/types/src/client/react/index.d.ts +2 -2
  57. package/dist/types/src/client/react/navigation/index.d.ts +1 -0
  58. package/dist/types/src/client/react/navigation/use-execute-navigation.d.ts +5 -0
  59. package/dist/types/src/client/react/navigation/use-resolve-navigation.d.ts +4 -6
  60. package/dist/types/src/client/react/provider/types.d.ts +6 -3
  61. package/dist/types/src/client/shared/types/index.d.ts +1 -1
  62. package/dist/types/src/client/shared/types/runtime-state.d.ts +13 -0
  63. package/dist/types/src/client/svelte/helpers/create-runtime-state.d.ts +11 -0
  64. package/dist/types/src/client/svelte/helpers/index.d.ts +1 -1
  65. package/dist/types/src/client/svelte/index.d.ts +1 -1
  66. package/dist/types/src/client/svelte/runtime/types.d.ts +6 -3
  67. package/dist/types/src/client/vue/helpers/index.d.ts +1 -1
  68. package/dist/types/src/client/vue/helpers/use-runtime-state.d.ts +10 -0
  69. package/dist/types/src/client/vue/index.d.ts +1 -1
  70. package/dist/types/src/client/vue/provider/resolver/resolve-runtime.d.ts +10 -0
  71. package/dist/types/src/client/vue/provider/types.d.ts +8 -3
  72. package/dist/types/src/core/index.d.ts +3 -3
  73. package/dist/types/src/core/messages/index.d.ts +1 -0
  74. package/dist/types/src/core/messages/load-remote-messages/load-remote-messages.d.ts +5 -5
  75. package/dist/types/src/core/messages/load-remote-messages/types.d.ts +1 -0
  76. package/dist/types/src/core/messages/merge-messages.d.ts +19 -0
  77. package/dist/types/src/core/types/bootstrap.d.ts +13 -0
  78. package/dist/types/src/core/types/index.d.ts +1 -0
  79. package/dist/types/src/core/utils/deep-merge.d.ts +20 -6
  80. package/dist/types/src/core/utils/index.d.ts +1 -2
  81. package/dist/types/src/routing/navigation/derive-target.d.ts +1 -1
  82. package/dist/types/src/routing/navigation/resolve-navigation.d.ts +4 -1
  83. package/dist/types/src/server/intor/index.d.ts +1 -1
  84. package/dist/types/src/server/intor/intor.d.ts +2 -2
  85. package/dist/types/src/server/intor/types.d.ts +3 -5
  86. package/dist/types/src/server/messages/load-local-messages/load-local-messages.d.ts +1 -1
  87. package/dist/types/src/server/messages/load-local-messages/types.d.ts +1 -0
  88. package/dist/vue/export/vue/index.js +1 -1
  89. package/dist/vue/src/client/shared/messages/create-refetch-messages.js +6 -6
  90. package/dist/vue/src/client/vue/helpers/{use-load-messages.js → use-runtime-state.js} +12 -8
  91. package/dist/vue/src/client/vue/provider/intor-provider.js +8 -6
  92. package/dist/vue/src/client/vue/provider/resolver/resolve-runtime.js +32 -0
  93. package/dist/vue/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
  94. package/dist/vue/src/core/messages/merge-messages.js +33 -0
  95. package/dist/vue/src/core/utils/deep-merge.js +35 -19
  96. package/package.json +2 -2
  97. package/dist/types/src/client/react/helpers/use-load-messages.d.ts +0 -11
  98. package/dist/types/src/client/shared/types/bootstrap.d.ts +0 -16
  99. package/dist/types/src/client/svelte/helpers/create-messages.d.ts +0 -12
  100. package/dist/types/src/client/vue/helpers/use-load-messages.d.ts +0 -12
  101. /package/dist/next/src/{core → routing/navigation}/utils/is-external-destination.js +0 -0
  102. /package/dist/react/src/{core → routing/navigation}/utils/is-external-destination.js +0 -0
  103. /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';
@@ -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
  };
@@ -37,6 +37,7 @@ async function loadMessagesFromUrl(url, options) {
37
37
  const fallbackLocales = parseMultiValueParam(url.searchParams.getAll("fallbackLocales"));
38
38
  // Load local messages
39
39
  return loadLocalMessages({
40
+ id: options?.id || "default",
40
41
  rootDir,
41
42
  locale,
42
43
  namespaces,
@@ -18,7 +18,7 @@ import { readLocaleMessages } from './read-locale-messages/read-locale-messages.
18
18
  *
19
19
  * File traversal, parsing, and validation are delegated to lower-level utilities.
20
20
  */
21
- const loadLocalMessages = async ({ locale, fallbackLocales, namespaces, rootDir = "messages", concurrency = 10, readOptions, pool = getGlobalMessagesPool(), cacheOptions, allowCacheWrite = false, loggerOptions, }) => {
21
+ const loadLocalMessages = async ({ id, locale, fallbackLocales, namespaces, rootDir = "messages", concurrency = 10, readOptions, pool = getGlobalMessagesPool(), cacheOptions, allowCacheWrite = false, loggerOptions, }) => {
22
22
  const baseLogger = getLogger(loggerOptions);
23
23
  const logger = baseLogger.child({ scope: "load-local-messages" });
24
24
  const start = performance.now();
@@ -30,7 +30,7 @@ const loadLocalMessages = async ({ locale, fallbackLocales, namespaces, rootDir
30
30
  // Cache key resolution
31
31
  // ---------------------------------------------------------------------------
32
32
  const cacheKey = normalizeCacheKey([
33
- loggerOptions.id,
33
+ id,
34
34
  "loaderType:local",
35
35
  rootDir,
36
36
  locale,
@@ -24,19 +24,19 @@ const loadMessages = async ({ config, locale, readOptions, allowCacheWrite = fal
24
24
  // ---------------------------------------------------------------------------
25
25
  // Resolve loader configuration
26
26
  // ---------------------------------------------------------------------------
27
- const loader = resolveLoaderOptions(config);
27
+ const loader = resolveLoaderOptions(config, "server");
28
28
  if (!loader) {
29
29
  logger.warn("No loader options have been configured in the current config.");
30
30
  return;
31
31
  }
32
- const { type, namespaces, rootDir } = loader;
32
+ const { type, namespaces } = loader;
33
33
  const fallbackLocales = config.fallbackLocales[locale] || [];
34
34
  logger.info(`Loading messages for locale "${locale}".`);
35
35
  logger.trace("Starting to load messages with runtime context.", {
36
36
  loaderType: type,
37
37
  locale,
38
38
  fallbackLocales,
39
- namespaces: namespaces && namespaces.length > 0 ? [...namespaces] : "[ALL]",
39
+ namespaces: namespaces && namespaces.length > 0 ? [...namespaces] : ["*"],
40
40
  cache: config.cache,
41
41
  });
42
42
  // ---------------------------------------------------------------------------
@@ -45,10 +45,11 @@ const loadMessages = async ({ config, locale, readOptions, allowCacheWrite = fal
45
45
  let loadedMessages;
46
46
  if (type === "local") {
47
47
  loadedMessages = await loadLocalMessages({
48
+ id: config.id,
48
49
  locale,
49
50
  fallbackLocales,
50
51
  namespaces,
51
- rootDir,
52
+ rootDir: loader.rootDir,
52
53
  concurrency: loader.concurrency,
53
54
  readOptions,
54
55
  cacheOptions: config.cache,
@@ -58,10 +59,11 @@ const loadMessages = async ({ config, locale, readOptions, allowCacheWrite = fal
58
59
  }
59
60
  else if (type === "remote") {
60
61
  loadedMessages = await loadRemoteMessages({
62
+ id: config.id,
61
63
  locale,
62
64
  fallbackLocales,
63
65
  namespaces,
64
- rootDir,
66
+ rootDir: loader.rootDir,
65
67
  url: loader.url,
66
68
  headers: loader.headers,
67
69
  allowCacheWrite,
@@ -1,8 +1,5 @@
1
1
  import { Translator } from 'intor-translator';
2
- import '../../core/error/intor-error.js';
3
- import { deepMerge } from '../../core/utils/deep-merge.js';
4
- import 'logry';
5
- import 'keyv';
2
+ import { mergeMessages } from '../../core/messages/merge-messages.js';
6
3
 
7
4
  /**
8
5
  * Create a server-side translator snapshot.
@@ -17,7 +14,10 @@ import 'keyv';
17
14
  function createTranslator(params) {
18
15
  const { config, locale, messages, preKey, handlers, plugins } = params;
19
16
  // Merge static config messages with runtime-loaded messages
20
- const finalMessages = deepMerge(config.messages, messages);
17
+ const finalMessages = mergeMessages(config.messages, messages, {
18
+ config,
19
+ locale,
20
+ });
21
21
  const translator = new Translator({
22
22
  locale,
23
23
  messages: finalMessages,
@@ -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,7 +18,7 @@ import { readLocaleMessages } from './read-locale-messages/read-locale-messages.
18
18
  *
19
19
  * File traversal, parsing, and validation are delegated to lower-level utilities.
20
20
  */
21
- const loadLocalMessages = async ({ locale, fallbackLocales, namespaces, rootDir = "messages", concurrency = 10, readOptions, pool = getGlobalMessagesPool(), cacheOptions, allowCacheWrite = false, loggerOptions, }) => {
21
+ const loadLocalMessages = async ({ id, locale, fallbackLocales, namespaces, rootDir = "messages", concurrency = 10, readOptions, pool = getGlobalMessagesPool(), cacheOptions, allowCacheWrite = false, loggerOptions, }) => {
22
22
  const baseLogger = getLogger(loggerOptions);
23
23
  const logger = baseLogger.child({ scope: "load-local-messages" });
24
24
  const start = performance.now();
@@ -30,7 +30,7 @@ const loadLocalMessages = async ({ locale, fallbackLocales, namespaces, rootDir
30
30
  // Cache key resolution
31
31
  // ---------------------------------------------------------------------------
32
32
  const cacheKey = normalizeCacheKey([
33
- loggerOptions.id,
33
+ id,
34
34
  "loaderType:local",
35
35
  rootDir,
36
36
  locale,
@@ -29,14 +29,14 @@ const loadMessages = async ({ config, locale, readOptions, allowCacheWrite = fal
29
29
  logger.warn("No loader options have been configured in the current config.");
30
30
  return;
31
31
  }
32
- const { type, namespaces, rootDir } = loader;
32
+ const { type, namespaces } = loader;
33
33
  const fallbackLocales = config.fallbackLocales[locale] || [];
34
34
  logger.info(`Loading messages for locale "${locale}".`);
35
35
  logger.trace("Starting to load messages with runtime context.", {
36
36
  loaderType: type,
37
37
  locale,
38
38
  fallbackLocales,
39
- namespaces: namespaces && namespaces.length > 0 ? [...namespaces] : "[ALL]",
39
+ namespaces: namespaces && namespaces.length > 0 ? [...namespaces] : ["*"],
40
40
  cache: config.cache,
41
41
  });
42
42
  // ---------------------------------------------------------------------------
@@ -45,10 +45,11 @@ const loadMessages = async ({ config, locale, readOptions, allowCacheWrite = fal
45
45
  let loadedMessages;
46
46
  if (type === "local") {
47
47
  loadedMessages = await loadLocalMessages({
48
+ id: config.id,
48
49
  locale,
49
50
  fallbackLocales,
50
51
  namespaces,
51
- rootDir,
52
+ rootDir: loader.rootDir,
52
53
  concurrency: loader.concurrency,
53
54
  readOptions,
54
55
  cacheOptions: config.cache,
@@ -58,10 +59,11 @@ const loadMessages = async ({ config, locale, readOptions, allowCacheWrite = fal
58
59
  }
59
60
  else if (type === "remote") {
60
61
  loadedMessages = await loadRemoteMessages({
62
+ id: config.id,
61
63
  locale,
62
64
  fallbackLocales,
63
65
  namespaces,
64
- rootDir,
66
+ rootDir: loader.rootDir,
65
67
  url: loader.url,
66
68
  headers: loader.headers,
67
69
  allowCacheWrite,
@@ -1,8 +1,5 @@
1
1
  import { Translator } from 'intor-translator';
2
- import '../../core/error/intor-error.js';
3
- import { deepMerge } from '../../core/utils/deep-merge.js';
4
- import 'logry';
5
- import 'keyv';
2
+ import { mergeMessages } from '../../core/messages/merge-messages.js';
6
3
 
7
4
  /**
8
5
  * Create a server-side translator snapshot.
@@ -17,7 +14,10 @@ import 'keyv';
17
14
  function createTranslator(params) {
18
15
  const { config, locale, messages, preKey, handlers, plugins } = params;
19
16
  // Merge static config messages with runtime-loaded messages
20
- const finalMessages = deepMerge(config.messages, messages);
17
+ const finalMessages = mergeMessages(config.messages, messages, {
18
+ config,
19
+ locale,
20
+ });
21
21
  const translator = new Translator({
22
22
  locale,
23
23
  messages: finalMessages,
@@ -3,7 +3,7 @@ import { jsx } from 'react/jsx-runtime';
3
3
  import { formatUrl } from 'next/dist/shared/lib/router/utils/format-url';
4
4
  import NextLink from 'next/link';
5
5
  import 'react';
6
- import { useResolveNavigation } from 'intor/react';
6
+ import { useResolveNavigation, useExecuteNavigation } from 'intor/react';
7
7
  import { usePathname } from './use-pathname.js';
8
8
 
9
9
  /**
@@ -16,26 +16,25 @@ import { usePathname } from './use-pathname.js';
16
16
  */
17
17
  const Link = ({ href, locale, children, onClick, ...props }) => {
18
18
  const { pathname } = usePathname();
19
- const { resolveNavigation } = useResolveNavigation();
19
+ const resolveNavigation = useResolveNavigation();
20
+ const executeNavigation = useExecuteNavigation();
20
21
  // Normalize href into a string destination
21
22
  const rawDestination = typeof href === "string" ? href : href ? formatUrl(href) : undefined;
22
23
  // Resolve navigation result for this link
23
- const { kind, destination } = resolveNavigation(pathname, {
24
+ const navigationResult = resolveNavigation(pathname, {
24
25
  destination: rawDestination,
25
26
  locale,
26
27
  });
28
+ // --------------------------------------------------
29
+ // Execute navigation on user interaction
30
+ // --------------------------------------------------
27
31
  const handleClick = (e) => {
28
32
  onClick?.(e);
29
33
  if (e.defaultPrevented)
30
34
  return;
31
- // Decide how this navigation should be executed
32
- if (kind === "reload") {
33
- e.preventDefault(); // prevent client-side navigation
34
- globalThis.location.href = destination;
35
- return;
36
- }
35
+ executeNavigation(navigationResult, e);
37
36
  };
38
- return (jsx(NextLink, { href: destination, onClick: handleClick, ...props, children: children }));
37
+ return (jsx(NextLink, { href: navigationResult.destination, onClick: handleClick, ...props, children: children }));
39
38
  };
40
39
 
41
40
  export { Link };