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.
- package/README.md +0 -6
- package/dist/core/export/index.js +2 -0
- package/dist/core/export/server/index.js +0 -2
- package/dist/core/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
- package/dist/core/src/core/messages/merge-messages.js +33 -0
- package/dist/core/src/core/utils/deep-merge.js +35 -19
- package/dist/core/src/core/utils/resolve-loader-options.js +9 -0
- package/dist/core/src/server/helpers/local-messages-from-url.js +1 -0
- package/dist/core/src/server/messages/load-local-messages/load-local-messages.js +2 -2
- package/dist/core/src/server/messages/load-messages.js +7 -5
- package/dist/core/src/server/translator/create-translator.js +5 -5
- package/dist/express/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
- package/dist/express/src/core/messages/merge-messages.js +33 -0
- package/dist/express/src/core/utils/deep-merge.js +35 -19
- package/dist/express/src/server/messages/load-local-messages/load-local-messages.js +2 -2
- package/dist/express/src/server/messages/load-messages.js +6 -4
- package/dist/express/src/server/translator/create-translator.js +5 -5
- package/dist/next/src/adapters/next/navigation/link.js +9 -10
- package/dist/next/src/adapters/next/navigation/redirect.js +7 -11
- package/dist/next/src/adapters/next/navigation/use-router.js +18 -14
- package/dist/next/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
- package/dist/next/src/core/messages/merge-messages.js +33 -0
- package/dist/next/src/core/utils/deep-merge.js +35 -19
- package/dist/next/src/core/utils/resolve-loader-options.js +9 -0
- package/dist/next/src/policies/shoud-full-reload.js +14 -0
- package/dist/next/src/routing/navigation/decide-strategy.js +17 -0
- package/dist/next/src/routing/navigation/derive-target.js +55 -0
- package/dist/next/src/routing/navigation/resolve-navigation.js +26 -0
- package/dist/next/src/routing/navigation/utils/derive-host-destination.js +13 -0
- package/dist/next/src/routing/navigation/utils/derive-query-destination.js +11 -0
- package/dist/next/src/server/messages/load-local-messages/load-local-messages.js +2 -2
- package/dist/next/src/server/messages/load-messages.js +7 -5
- package/dist/next/src/server/translator/create-translator.js +5 -5
- package/dist/react/export/react/index.js +2 -1
- package/dist/react/src/client/react/helpers/{use-load-messages.js → use-runtime-state.js} +11 -10
- package/dist/react/src/client/react/navigation/use-execute-navigation.js +29 -0
- package/dist/react/src/client/react/navigation/use-resolve-navigation.js +4 -4
- package/dist/react/src/client/shared/messages/create-refetch-messages.js +6 -6
- package/dist/react/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
- package/dist/react/src/core/messages/merge-messages.js +33 -0
- package/dist/react/src/core/utils/deep-merge.js +35 -19
- package/dist/react/src/routing/navigation/derive-target.js +5 -8
- package/dist/react/src/routing/navigation/resolve-navigation.js +3 -2
- package/dist/svelte/export/svelte/index.js +1 -1
- package/dist/svelte/src/client/shared/messages/create-refetch-messages.js +6 -6
- package/dist/svelte/src/client/svelte/helpers/{create-messages.js → create-runtime-state.js} +9 -8
- package/dist/svelte/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
- package/dist/svelte/src/core/messages/merge-messages.js +33 -0
- package/dist/svelte/src/core/utils/deep-merge.js +35 -19
- package/dist/types/export/index.d.ts +1 -1
- package/dist/types/export/react/index.d.ts +1 -1
- package/dist/types/export/svelte/index.d.ts +1 -1
- package/dist/types/export/vue/index.d.ts +1 -1
- package/dist/types/src/client/react/helpers/index.d.ts +1 -1
- package/dist/types/src/client/react/helpers/use-runtime-state.d.ts +10 -0
- package/dist/types/src/client/react/index.d.ts +2 -2
- package/dist/types/src/client/react/navigation/index.d.ts +1 -0
- package/dist/types/src/client/react/navigation/use-execute-navigation.d.ts +5 -0
- package/dist/types/src/client/react/navigation/use-resolve-navigation.d.ts +4 -6
- package/dist/types/src/client/react/provider/types.d.ts +6 -3
- package/dist/types/src/client/shared/types/index.d.ts +1 -1
- package/dist/types/src/client/shared/types/runtime-state.d.ts +13 -0
- package/dist/types/src/client/svelte/helpers/create-runtime-state.d.ts +11 -0
- package/dist/types/src/client/svelte/helpers/index.d.ts +1 -1
- package/dist/types/src/client/svelte/index.d.ts +1 -1
- package/dist/types/src/client/svelte/runtime/types.d.ts +6 -3
- package/dist/types/src/client/vue/helpers/index.d.ts +1 -1
- package/dist/types/src/client/vue/helpers/use-runtime-state.d.ts +10 -0
- package/dist/types/src/client/vue/index.d.ts +1 -1
- package/dist/types/src/client/vue/provider/resolver/resolve-runtime.d.ts +10 -0
- package/dist/types/src/client/vue/provider/types.d.ts +8 -3
- package/dist/types/src/core/index.d.ts +3 -3
- package/dist/types/src/core/messages/index.d.ts +1 -0
- package/dist/types/src/core/messages/load-remote-messages/load-remote-messages.d.ts +5 -5
- package/dist/types/src/core/messages/load-remote-messages/types.d.ts +1 -0
- package/dist/types/src/core/messages/merge-messages.d.ts +19 -0
- package/dist/types/src/core/types/bootstrap.d.ts +13 -0
- package/dist/types/src/core/types/index.d.ts +1 -0
- package/dist/types/src/core/utils/deep-merge.d.ts +20 -6
- package/dist/types/src/core/utils/index.d.ts +1 -2
- package/dist/types/src/routing/navigation/derive-target.d.ts +1 -1
- package/dist/types/src/routing/navigation/resolve-navigation.d.ts +4 -1
- package/dist/types/src/server/intor/index.d.ts +1 -1
- package/dist/types/src/server/intor/intor.d.ts +2 -2
- package/dist/types/src/server/intor/types.d.ts +3 -5
- package/dist/types/src/server/messages/load-local-messages/load-local-messages.d.ts +1 -1
- package/dist/types/src/server/messages/load-local-messages/types.d.ts +1 -0
- package/dist/vue/export/vue/index.js +1 -1
- package/dist/vue/src/client/shared/messages/create-refetch-messages.js +6 -6
- package/dist/vue/src/client/vue/helpers/{use-load-messages.js → use-runtime-state.js} +12 -8
- package/dist/vue/src/client/vue/provider/intor-provider.js +8 -6
- package/dist/vue/src/client/vue/provider/resolver/resolve-runtime.js +32 -0
- package/dist/vue/src/core/messages/load-remote-messages/load-remote-messages.js +24 -14
- package/dist/vue/src/core/messages/merge-messages.js +33 -0
- package/dist/vue/src/core/utils/deep-merge.js +35 -19
- package/package.json +2 -2
- package/dist/types/src/client/react/helpers/use-load-messages.d.ts +0 -11
- package/dist/types/src/client/shared/types/bootstrap.d.ts +0 -16
- package/dist/types/src/client/svelte/helpers/create-messages.d.ts +0 -12
- package/dist/types/src/client/vue/helpers/use-load-messages.d.ts +0 -12
- /package/dist/next/src/{core → routing/navigation}/utils/is-external-destination.js +0 -0
- /package/dist/react/src/{core → routing/navigation}/utils/is-external-destination.js +0 -0
- /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)
|
|
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
|
|
9
|
-
* It
|
|
8
|
+
* This function serves as the orchestration layer for remote message loading.
|
|
9
|
+
* It coordinates:
|
|
10
10
|
*
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
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
|
-
//
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Cache key resolution
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
28
30
|
const cacheKey = normalizeCacheKey([
|
|
29
|
-
|
|
31
|
+
id,
|
|
30
32
|
"loaderType:remote",
|
|
31
33
|
rootDir,
|
|
32
34
|
locale,
|
|
33
|
-
(fallbackLocales
|
|
34
|
-
(namespaces
|
|
35
|
+
(fallbackLocales || []).toSorted().join(","),
|
|
36
|
+
(namespaces || []).toSorted().join(","),
|
|
35
37
|
]);
|
|
36
|
-
//
|
|
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
|
-
//
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
4
|
+
* - Nested plain objects → merged recursively
|
|
5
|
+
* - Arrays / primitives → `b` overwrites `a`
|
|
6
6
|
*
|
|
7
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
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] : "
|
|
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/
|
|
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 =
|
|
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
|
|
9
|
-
* It
|
|
8
|
+
* This function serves as the orchestration layer for remote message loading.
|
|
9
|
+
* It coordinates:
|
|
10
10
|
*
|
|
11
|
-
* -
|
|
12
|
-
* -
|
|
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
|
-
//
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Cache key resolution
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
28
30
|
const cacheKey = normalizeCacheKey([
|
|
29
|
-
|
|
31
|
+
id,
|
|
30
32
|
"loaderType:remote",
|
|
31
33
|
rootDir,
|
|
32
34
|
locale,
|
|
33
|
-
(fallbackLocales
|
|
34
|
-
(namespaces
|
|
35
|
+
(fallbackLocales || []).toSorted().join(","),
|
|
36
|
+
(namespaces || []).toSorted().join(","),
|
|
35
37
|
]);
|
|
36
|
-
//
|
|
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
|
-
//
|
|
91
|
-
|
|
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
|
-
|
|
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
|
-
* -
|
|
4
|
+
* - Nested plain objects → merged recursively
|
|
5
|
+
* - Arrays / primitives → `b` overwrites `a`
|
|
6
6
|
*
|
|
7
|
-
*
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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
|
|
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] : "
|
|
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/
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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 };
|