intor 2.3.35 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -3
- package/dist/core/export/edge/index.js +6 -0
- package/dist/core/src/{server → core}/translator/create-translator.js +1 -1
- package/dist/core/src/core/utils/normalizers/normalize-locale.js +59 -0
- package/dist/core/src/core/utils/normalizers/normalize-query.js +25 -0
- package/dist/core/src/edge/helpers/get-translator.js +29 -0
- package/dist/core/src/edge/translator/init-translator.js +35 -0
- package/dist/core/src/routing/inbound/helpers/resolve-inbound-from-request.js +29 -0
- package/dist/core/src/routing/inbound/resolve-inbound.js +45 -0
- package/dist/core/src/routing/inbound/resolve-locale/resolve-locale.js +35 -0
- package/dist/{next/src/routing/inbound/resolve-pathname/resolve-pathname.js → core/src/routing/inbound/resolve-path/resolve-path.js} +2 -2
- package/dist/core/src/routing/locale/get-locale-from-accept-language.js +38 -0
- package/dist/core/src/routing/locale/get-locale-from-host.js +32 -0
- package/dist/core/src/routing/locale/get-locale-from-pathname.js +47 -0
- package/dist/core/src/routing/locale/get-locale-from-query.js +21 -0
- package/dist/core/src/server/helpers/get-translator.js +4 -5
- package/dist/core/src/server/translator/init-translator.js +1 -1
- package/dist/express/src/adapters/express/create-intor-handler.js +2 -4
- package/dist/express/src/adapters/express/get-translator.js +7 -1
- package/dist/express/src/{server → core}/translator/create-translator.js +1 -1
- package/dist/express/src/core/utils/parse-cookie-header.js +22 -0
- package/dist/express/src/routing/inbound/resolve-inbound.js +2 -2
- package/dist/express/src/routing/inbound/{resolve-pathname/resolve-pathname.js → resolve-path/resolve-path.js} +2 -2
- package/dist/express/src/server/helpers/get-translator.js +4 -5
- package/dist/express/src/server/translator/init-translator.js +1 -1
- package/dist/fastify/export/fastify/index.js +3 -0
- package/dist/fastify/src/adapters/fastify/create-intor-handler.js +54 -0
- package/dist/fastify/src/adapters/fastify/get-translator.js +28 -0
- package/dist/fastify/src/adapters/fastify/intor-fastify-plugin.js +34 -0
- package/dist/fastify/src/core/constants/prefix-placeholder.js +4 -0
- package/dist/fastify/src/core/error/intor-error.js +9 -0
- package/dist/fastify/src/core/logger/get-logger.js +39 -0
- package/dist/fastify/src/core/logger/global-logger-pool.js +8 -0
- package/dist/fastify/src/core/messages/load-remote-messages/collect-remote-resources.js +25 -0
- package/dist/fastify/src/core/messages/load-remote-messages/fetch-remote-resource.js +47 -0
- package/dist/fastify/src/core/messages/load-remote-messages/load-remote-messages.js +93 -0
- package/dist/fastify/src/core/messages/load-remote-messages/resolve-remote-resources.js +24 -0
- package/dist/fastify/src/core/messages/merge-messages.js +33 -0
- package/dist/fastify/src/core/messages/utils/is-valid-messages.js +44 -0
- package/dist/fastify/src/core/messages/utils/nest-object-from-path.js +21 -0
- package/dist/fastify/src/core/render/create-html-renderer.js +44 -0
- package/dist/fastify/src/core/render/utils/escape-html.js +10 -0
- package/dist/fastify/src/core/render/utils/render-attributes.js +17 -0
- package/dist/fastify/src/core/translator/create-t-rich.js +22 -0
- package/dist/{svelte-kit/src/server → fastify/src/core}/translator/create-translator.js +1 -1
- package/dist/fastify/src/core/utils/deep-merge.js +47 -0
- package/dist/fastify/src/core/utils/normalizers/normalize-cache-key.js +45 -0
- package/dist/fastify/src/core/utils/normalizers/normalize-locale.js +59 -0
- package/dist/fastify/src/core/utils/normalizers/normalize-pathname.js +43 -0
- package/dist/fastify/src/core/utils/normalizers/normalize-query.js +25 -0
- package/dist/fastify/src/core/utils/parse-cookie-header.js +22 -0
- package/dist/fastify/src/core/utils/resolve-loader-options.js +27 -0
- package/dist/fastify/src/routing/inbound/resolve-inbound.js +45 -0
- package/dist/fastify/src/routing/inbound/resolve-locale/resolve-locale.js +35 -0
- package/dist/{svelte-kit/src/routing/inbound/resolve-pathname/resolve-pathname.js → fastify/src/routing/inbound/resolve-path/resolve-path.js} +2 -2
- package/dist/fastify/src/routing/inbound/resolve-path/strategies/all.js +28 -0
- package/dist/fastify/src/routing/inbound/resolve-path/strategies/except-default.js +29 -0
- package/dist/fastify/src/routing/locale/get-locale-from-accept-language.js +38 -0
- package/dist/fastify/src/routing/locale/get-locale-from-host.js +32 -0
- package/dist/fastify/src/routing/locale/get-locale-from-pathname.js +47 -0
- package/dist/fastify/src/routing/locale/get-locale-from-query.js +21 -0
- package/dist/fastify/src/routing/pathname/canonicalize-pathname.js +47 -0
- package/dist/fastify/src/routing/pathname/localize-pathname.js +36 -0
- package/dist/fastify/src/routing/pathname/materialize-pathname.js +42 -0
- package/dist/fastify/src/routing/pathname/standardize-pathname.js +34 -0
- package/dist/fastify/src/server/helpers/get-translator.js +31 -0
- package/dist/fastify/src/server/messages/load-local-messages/cache/messages-pool.js +11 -0
- package/dist/fastify/src/server/messages/load-local-messages/load-local-messages.js +108 -0
- package/dist/fastify/src/server/messages/load-local-messages/read-locale-messages/collect-file-entries/collect-file-entries.js +91 -0
- package/dist/fastify/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/parse-file-entries.js +103 -0
- package/dist/fastify/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/utils/json-reader.js +12 -0
- package/dist/fastify/src/server/messages/load-local-messages/read-locale-messages/read-locale-messages.js +42 -0
- package/dist/fastify/src/server/messages/load-messages.js +78 -0
- package/dist/fastify/src/server/translator/init-translator.js +36 -0
- package/dist/hono/export/hono/index.js +3 -0
- package/dist/hono/src/adapters/hono/create-intor-handler.js +40 -0
- package/dist/hono/src/adapters/hono/get-translator.js +20 -0
- package/dist/hono/src/core/constants/prefix-placeholder.js +4 -0
- package/dist/hono/src/core/error/intor-error.js +9 -0
- package/dist/hono/src/core/logger/get-logger.js +39 -0
- package/dist/hono/src/core/logger/global-logger-pool.js +8 -0
- package/dist/hono/src/core/messages/load-remote-messages/collect-remote-resources.js +25 -0
- package/dist/hono/src/core/messages/load-remote-messages/fetch-remote-resource.js +47 -0
- package/dist/hono/src/core/messages/load-remote-messages/load-remote-messages.js +93 -0
- package/dist/hono/src/core/messages/load-remote-messages/resolve-remote-resources.js +24 -0
- package/dist/hono/src/core/messages/merge-messages.js +33 -0
- package/dist/hono/src/core/messages/utils/is-valid-messages.js +44 -0
- package/dist/hono/src/core/messages/utils/nest-object-from-path.js +21 -0
- package/dist/hono/src/core/render/create-html-renderer.js +44 -0
- package/dist/hono/src/core/render/utils/escape-html.js +10 -0
- package/dist/hono/src/core/render/utils/render-attributes.js +17 -0
- package/dist/hono/src/core/translator/create-t-rich.js +22 -0
- package/dist/{next/src/server → hono/src/core}/translator/create-translator.js +1 -1
- package/dist/hono/src/core/utils/deep-merge.js +47 -0
- package/dist/hono/src/core/utils/normalizers/normalize-locale.js +59 -0
- package/dist/hono/src/core/utils/normalizers/normalize-pathname.js +43 -0
- package/dist/hono/src/core/utils/normalizers/normalize-query.js +25 -0
- package/dist/hono/src/core/utils/parse-cookie-header.js +22 -0
- package/dist/hono/src/edge/helpers/get-translator.js +29 -0
- package/dist/hono/src/edge/translator/init-translator.js +35 -0
- package/dist/hono/src/routing/inbound/resolve-inbound.js +45 -0
- package/dist/hono/src/routing/inbound/resolve-locale/resolve-locale.js +35 -0
- package/dist/hono/src/routing/inbound/resolve-path/resolve-path.js +42 -0
- package/dist/hono/src/routing/inbound/resolve-path/strategies/all.js +28 -0
- package/dist/hono/src/routing/inbound/resolve-path/strategies/except-default.js +29 -0
- package/dist/hono/src/routing/inbound/resolve-path/strategies/none.js +8 -0
- package/dist/hono/src/routing/locale/get-locale-from-accept-language.js +38 -0
- package/dist/hono/src/routing/locale/get-locale-from-host.js +32 -0
- package/dist/hono/src/routing/locale/get-locale-from-pathname.js +47 -0
- package/dist/hono/src/routing/locale/get-locale-from-query.js +21 -0
- package/dist/hono/src/routing/pathname/canonicalize-pathname.js +47 -0
- package/dist/hono/src/routing/pathname/localize-pathname.js +36 -0
- package/dist/hono/src/routing/pathname/materialize-pathname.js +42 -0
- package/dist/hono/src/routing/pathname/standardize-pathname.js +34 -0
- package/dist/next/src/adapters/next/server/get-translator.js +7 -1
- package/dist/next/src/core/translator/create-translator.js +30 -0
- package/dist/next/src/routing/inbound/resolve-inbound.js +2 -2
- package/dist/next/src/routing/inbound/resolve-path/resolve-path.js +42 -0
- package/dist/next/src/routing/inbound/resolve-path/strategies/none.js +8 -0
- package/dist/next/src/server/helpers/get-translator.js +4 -5
- package/dist/next/src/server/translator/init-translator.js +1 -1
- package/dist/react/src/client/react/translator/use-translator.js +5 -5
- package/dist/react/src/client/shared/helpers/get-client-locale.js +1 -7
- package/dist/svelte/src/client/shared/helpers/get-client-locale.js +1 -7
- package/dist/svelte/src/client/svelte/translator/use-translator.js +6 -6
- package/dist/svelte-kit/src/adapters/svelte-kit/server/create-intor-handler.js +1 -3
- package/dist/svelte-kit/src/core/translator/create-translator.js +30 -0
- package/dist/svelte-kit/src/routing/inbound/resolve-inbound.js +2 -2
- package/dist/svelte-kit/src/routing/inbound/resolve-path/resolve-path.js +42 -0
- package/dist/svelte-kit/src/routing/inbound/resolve-path/strategies/all.js +28 -0
- package/dist/svelte-kit/src/routing/inbound/resolve-path/strategies/except-default.js +29 -0
- package/dist/svelte-kit/src/routing/inbound/resolve-path/strategies/none.js +8 -0
- package/dist/svelte-kit/src/server/translator/init-translator.js +1 -1
- package/dist/types/export/edge/index.d.ts +2 -0
- package/dist/types/export/fastify/index.d.ts +1 -0
- package/dist/types/export/hono/index.d.ts +1 -0
- package/dist/types/src/adapters/express/create-intor-handler.d.ts +2 -4
- package/dist/types/src/adapters/express/get-translator.d.ts +5 -6
- package/dist/types/src/adapters/express/global.d.ts +4 -4
- package/dist/types/src/adapters/fastify/create-intor-handler.d.ts +12 -0
- package/dist/types/src/adapters/fastify/get-translator.d.ts +17 -0
- package/dist/types/src/adapters/fastify/global.d.ts +17 -0
- package/dist/types/src/adapters/fastify/index.d.ts +3 -0
- package/dist/types/src/adapters/fastify/intor-fastify-plugin.d.ts +21 -0
- package/dist/types/src/adapters/hono/create-intor-handler.d.ts +10 -0
- package/dist/types/src/adapters/hono/get-translator.d.ts +17 -0
- package/dist/types/src/adapters/hono/global.d.ts +17 -0
- package/dist/types/src/adapters/hono/index.d.ts +3 -0
- package/dist/types/src/adapters/next/server/get-translator.d.ts +5 -6
- package/dist/types/src/adapters/svelte-kit/server/create-intor-handler.d.ts +0 -2
- package/dist/types/src/client/react/translator/translator-instance.d.ts +1 -5
- package/dist/types/src/client/react/translator/use-translator.d.ts +4 -3
- package/dist/types/src/client/shared/helpers/get-client-locale.d.ts +3 -8
- package/dist/types/src/client/svelte/translator/translator-instance.d.ts +7 -7
- package/dist/types/src/client/svelte/translator/use-translator.d.ts +5 -4
- package/dist/types/src/client/vue/translator/translator-instance.d.ts +2 -2
- package/dist/types/src/client/vue/translator/use-translator.d.ts +5 -4
- package/dist/types/src/core/index.d.ts +2 -2
- package/dist/types/src/core/translator/index.d.ts +1 -0
- package/dist/types/src/core/types/translator-instance.d.ts +9 -2
- package/dist/types/src/core/utils/index.d.ts +1 -0
- package/dist/types/src/edge/helpers/get-translator.d.ts +15 -0
- package/dist/types/src/edge/helpers/index.d.ts +1 -0
- package/dist/types/src/edge/index.d.ts +1 -0
- package/dist/types/src/edge/translator/index.d.ts +1 -0
- package/dist/types/src/edge/translator/init-translator.d.ts +14 -0
- package/dist/types/src/routing/inbound/helpers/index.d.ts +1 -0
- package/dist/types/src/routing/inbound/helpers/resolve-inbound-from-request.d.ts +3 -0
- package/dist/types/src/routing/inbound/index.d.ts +1 -0
- package/dist/types/src/routing/inbound/resolve-path/index.d.ts +1 -0
- package/dist/types/src/routing/inbound/{resolve-pathname/resolve-pathname.d.ts → resolve-path/resolve-path.d.ts} +2 -2
- package/dist/types/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/all.d.ts +2 -2
- package/dist/types/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/except-default.d.ts +2 -2
- package/dist/types/src/routing/inbound/resolve-path/strategies/none.d.ts +5 -0
- package/dist/types/src/routing/inbound/{resolve-pathname → resolve-path}/types.d.ts +5 -5
- package/dist/types/src/routing/index.d.ts +1 -1
- package/dist/types/src/server/helpers/get-translator.d.ts +6 -8
- package/dist/types/src/server/index.d.ts +0 -2
- package/dist/types/src/server/translator/index.d.ts +1 -2
- package/dist/types/src/server/translator/init-translator.d.ts +3 -2
- package/dist/vue/src/client/shared/helpers/get-client-locale.js +1 -7
- package/dist/vue/src/client/vue/translator/use-translator.js +5 -5
- package/package.json +18 -1
- package/dist/types/src/routing/inbound/resolve-pathname/index.d.ts +0 -1
- package/dist/types/src/routing/inbound/resolve-pathname/strategies/none.d.ts +0 -5
- package/dist/types/src/server/shared/utils/index.d.ts +0 -1
- package/dist/types/src/server/translator/translator-instance.d.ts +0 -10
- /package/dist/{express/src/server/shared → core/src/core}/utils/parse-cookie-header.js +0 -0
- /package/dist/{express/src/routing/inbound/resolve-pathname → core/src/routing/inbound/resolve-path}/strategies/all.js +0 -0
- /package/dist/{express/src/routing/inbound/resolve-pathname → core/src/routing/inbound/resolve-path}/strategies/except-default.js +0 -0
- /package/dist/{express/src/routing/inbound/resolve-pathname → core/src/routing/inbound/resolve-path}/strategies/none.js +0 -0
- /package/dist/{svelte-kit/src/routing/inbound/resolve-pathname → express/src/routing/inbound/resolve-path}/strategies/all.js +0 -0
- /package/dist/{svelte-kit/src/routing/inbound/resolve-pathname → express/src/routing/inbound/resolve-path}/strategies/except-default.js +0 -0
- /package/dist/{next/src/routing/inbound/resolve-pathname → express/src/routing/inbound/resolve-path}/strategies/none.js +0 -0
- /package/dist/{svelte-kit/src/routing/inbound/resolve-pathname → fastify/src/routing/inbound/resolve-path}/strategies/none.js +0 -0
- /package/dist/next/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/all.js +0 -0
- /package/dist/next/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/except-default.js +0 -0
- /package/dist/types/src/{server → core}/translator/create-translator.d.ts +0 -0
- /package/dist/types/src/{server/shared → core}/utils/parse-cookie-header.d.ts +0 -0
- /package/dist/types/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/index.d.ts +0 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { logry } from 'logry';
|
|
2
|
+
import { getGlobalLoggerPool } from './global-logger-pool.js';
|
|
3
|
+
|
|
4
|
+
const DEFAULT_FORMAT_CONFIG = {
|
|
5
|
+
timestamp: { withDate: false },
|
|
6
|
+
};
|
|
7
|
+
const DEFAULT_RENDER_CONFIG = {
|
|
8
|
+
timestamp: {},
|
|
9
|
+
id: { visible: true, prefix: "<", suffix: ">" },
|
|
10
|
+
meta: { lineBreaksAfter: 1 },
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Get a shared logger instance by id.
|
|
14
|
+
* - Safe across hot reloads
|
|
15
|
+
* - Prevents unbounded memory usage via soft LRU
|
|
16
|
+
*/
|
|
17
|
+
function getLogger({ id = "default", formatConfig, renderConfig, preset, ...options }) {
|
|
18
|
+
const pool = getGlobalLoggerPool();
|
|
19
|
+
let logger = pool.get(id);
|
|
20
|
+
if (!logger) {
|
|
21
|
+
logger = logry({
|
|
22
|
+
id,
|
|
23
|
+
formatConfig: !formatConfig && !preset ? DEFAULT_FORMAT_CONFIG : formatConfig,
|
|
24
|
+
renderConfig: !renderConfig && !preset ? DEFAULT_RENDER_CONFIG : renderConfig,
|
|
25
|
+
preset,
|
|
26
|
+
...options,
|
|
27
|
+
});
|
|
28
|
+
pool.set(id, logger);
|
|
29
|
+
// Soft LRU: keep pool size under control
|
|
30
|
+
if (pool.size > 1000) {
|
|
31
|
+
const keys = [...pool.keys()];
|
|
32
|
+
for (const key of keys.slice(0, 200))
|
|
33
|
+
pool.delete(key);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return logger;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export { getLogger };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect remote message resources for a given locale.
|
|
3
|
+
*
|
|
4
|
+
* - Always includes the root `index.json`
|
|
5
|
+
* - Optionally includes namespace-specific resources
|
|
6
|
+
* - Produces semantic paths for later message nesting
|
|
7
|
+
*
|
|
8
|
+
* This function performs no I/O and does not validate resource existence.
|
|
9
|
+
*/
|
|
10
|
+
function collectRemoteResources({ locale, baseUrl, namespaces, }) {
|
|
11
|
+
const basePath = `${baseUrl}/${locale}`;
|
|
12
|
+
// Root translation resource (always loaded)
|
|
13
|
+
const indexResource = { url: `${basePath}/index.json`, path: [] };
|
|
14
|
+
// When no namespaces are provided, the locale domain consists of index only
|
|
15
|
+
if (!namespaces || namespaces.length === 0)
|
|
16
|
+
return [indexResource];
|
|
17
|
+
// Namespace-specific resources are nested under their namespace key
|
|
18
|
+
const nsResources = namespaces.map((ns) => ({
|
|
19
|
+
url: `${basePath}/${ns}.json`,
|
|
20
|
+
path: [ns],
|
|
21
|
+
}));
|
|
22
|
+
return [indexResource, ...nsResources];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { collectRemoteResources };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getLogger } from '../../logger/get-logger.js';
|
|
2
|
+
import { isValidMessages } from '../utils/is-valid-messages.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Fetch a single remote messages resource.
|
|
6
|
+
*
|
|
7
|
+
* This function performs a single HTTP request to retrieve
|
|
8
|
+
* a remote translation messages payload.
|
|
9
|
+
*
|
|
10
|
+
* It is responsible for:
|
|
11
|
+
* - Issuing the network request
|
|
12
|
+
* - Validating the returned message structure
|
|
13
|
+
* - Handling abort and network errors
|
|
14
|
+
*/
|
|
15
|
+
async function fetchRemoteResource({ fetch, url, headers, signal, loggerOptions, }) {
|
|
16
|
+
const baseLogger = getLogger(loggerOptions);
|
|
17
|
+
const logger = baseLogger.child({ scope: "fetch-locale-messages" });
|
|
18
|
+
try {
|
|
19
|
+
// Fetch
|
|
20
|
+
const response = await fetch(url, {
|
|
21
|
+
method: "GET",
|
|
22
|
+
headers: { "Content-Type": "application/json", ...headers },
|
|
23
|
+
cache: "no-store",
|
|
24
|
+
signal,
|
|
25
|
+
});
|
|
26
|
+
if (!response.ok) {
|
|
27
|
+
throw new Error(`HTTP ${response.status} ${response.statusText}`);
|
|
28
|
+
}
|
|
29
|
+
// Parse JSON body
|
|
30
|
+
const data = await response.json();
|
|
31
|
+
// Validate messages structure
|
|
32
|
+
if (!isValidMessages(data)) {
|
|
33
|
+
throw new Error("Invalid messages structure");
|
|
34
|
+
}
|
|
35
|
+
return data;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
39
|
+
logger.debug("Remote fetch aborted.", { url });
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
logger.warn("Failed to fetch remote messages.", { url, error });
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { fetchRemoteResource };
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import pLimit from 'p-limit';
|
|
2
|
+
import { getLogger } from '../../logger/get-logger.js';
|
|
3
|
+
import { collectRemoteResources } from './collect-remote-resources.js';
|
|
4
|
+
import { fetchRemoteResource } from './fetch-remote-resource.js';
|
|
5
|
+
import { resolveRemoteResources } from './resolve-remote-resources.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Load locale messages from a remote source.
|
|
9
|
+
*
|
|
10
|
+
* This function serves as the orchestration layer for remote message loading.
|
|
11
|
+
* It coordinates:
|
|
12
|
+
*
|
|
13
|
+
* - Locale resolution with fallbacks
|
|
14
|
+
* - Concurrency control for network requests
|
|
15
|
+
* - Remote resource fetching and message merging
|
|
16
|
+
*
|
|
17
|
+
* Remote messages are fetched on demand and are not memoized at the process level.
|
|
18
|
+
*
|
|
19
|
+
* Network requests and response validation are delegated to lower-level utilities.
|
|
20
|
+
*/
|
|
21
|
+
const loadRemoteMessages = async ({ locale, fallbackLocales, namespaces, concurrency, fetch, url: baseUrl, headers, signal, loggerOptions, }) => {
|
|
22
|
+
const baseLogger = getLogger(loggerOptions);
|
|
23
|
+
const logger = baseLogger.child({ scope: "load-remote-messages" });
|
|
24
|
+
// Abort early if the request has already been cancelled
|
|
25
|
+
if (signal?.aborted) {
|
|
26
|
+
logger.debug("Remote message loading aborted before fetch.");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const start = performance.now();
|
|
30
|
+
logger.debug("Loading remote messages.", { baseUrl });
|
|
31
|
+
// ----------------------------------------------------------------
|
|
32
|
+
// Resolve locale messages with ordered fallback strategy
|
|
33
|
+
// ----------------------------------------------------------------
|
|
34
|
+
const limit = concurrency ? pLimit(concurrency) : undefined;
|
|
35
|
+
const candidateLocales = [locale, ...(fallbackLocales || [])];
|
|
36
|
+
let messages;
|
|
37
|
+
for (let i = 0; i < candidateLocales.length; i++) {
|
|
38
|
+
const candidateLocale = candidateLocales[i];
|
|
39
|
+
const isLast = i === candidateLocales.length - 1;
|
|
40
|
+
try {
|
|
41
|
+
// -----------------------------------------------------------------
|
|
42
|
+
// Collect remote message resources for the locale
|
|
43
|
+
// -----------------------------------------------------------------
|
|
44
|
+
const resources = collectRemoteResources({
|
|
45
|
+
locale: candidateLocale,
|
|
46
|
+
baseUrl,
|
|
47
|
+
namespaces,
|
|
48
|
+
});
|
|
49
|
+
// -----------------------------------------------------------------
|
|
50
|
+
// Fetch all message chunks in parallel
|
|
51
|
+
// -----------------------------------------------------------------
|
|
52
|
+
const fetchUrl = (url) => fetchRemoteResource({ url, headers, signal, loggerOptions, fetch });
|
|
53
|
+
const results = await Promise.all(resources.map(({ url }) => limit ? limit(() => fetchUrl(url)) : fetchUrl(url)));
|
|
54
|
+
// Guard: no valid remote resources
|
|
55
|
+
if (!results.some(Boolean))
|
|
56
|
+
continue;
|
|
57
|
+
// -----------------------------------------------------------------
|
|
58
|
+
// Resolve and merge remote message resources
|
|
59
|
+
// -----------------------------------------------------------------
|
|
60
|
+
const resolved = resolveRemoteResources(resources.map((res, i) => ({ path: res.path, data: results[i] })));
|
|
61
|
+
// -----------------------------------------------------------------
|
|
62
|
+
// Wrap resolved messages into locale-scoped LocaleMessages
|
|
63
|
+
// -----------------------------------------------------------------
|
|
64
|
+
messages = { [candidateLocale]: resolved };
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
if (signal?.aborted) {
|
|
69
|
+
logger.debug("Remote message loading aborted.");
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (isLast) {
|
|
73
|
+
logger.warn("Failed to load messages for all candidate locales.", {
|
|
74
|
+
locale,
|
|
75
|
+
fallbackLocales,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
logger.warn(`Failed to load locale messages for "${candidateLocale}", trying next fallback.`);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// Final success log with resolved locale and timing
|
|
84
|
+
if (messages) {
|
|
85
|
+
logger.trace("Finished loading remote messages.", {
|
|
86
|
+
loadedLocale: Object.keys(messages)[0],
|
|
87
|
+
duration: `${Math.round(performance.now() - start)} ms`,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return messages;
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
export { loadRemoteMessages };
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { deepMerge } from '../../utils/deep-merge.js';
|
|
2
|
+
import { nestObjectFromPath } from '../utils/nest-object-from-path.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolve remote message resources into a single MessageObject.
|
|
6
|
+
*
|
|
7
|
+
* - Applies semantic nesting based on resource path
|
|
8
|
+
* - Merges all resolved message chunks
|
|
9
|
+
*
|
|
10
|
+
* Always returns a MessageObject.
|
|
11
|
+
* An empty object represents an empty translation domain.
|
|
12
|
+
*/
|
|
13
|
+
function resolveRemoteResources(resources) {
|
|
14
|
+
let result = {};
|
|
15
|
+
for (const { path, data } of resources) {
|
|
16
|
+
if (!data)
|
|
17
|
+
continue;
|
|
18
|
+
const resolved = path.length > 0 ? nestObjectFromPath(path, data) : data;
|
|
19
|
+
result = deepMerge(result, resolved);
|
|
20
|
+
}
|
|
21
|
+
return result;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export { resolveRemoteResources };
|
|
@@ -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 };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Check if a value is a plain object (not null, not array) */
|
|
2
|
+
function isPlainObject(value) {
|
|
3
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Check if a value is a valid MessageObject.
|
|
7
|
+
*
|
|
8
|
+
* - Supports all MessageValue variants (primitive, array, object).
|
|
9
|
+
* - Uses an iterative approach to avoid stack overflow.
|
|
10
|
+
*/
|
|
11
|
+
function isValidMessages(value) {
|
|
12
|
+
if (!isPlainObject(value))
|
|
13
|
+
return false;
|
|
14
|
+
const stack = [value];
|
|
15
|
+
while (stack.length > 0) {
|
|
16
|
+
const current = stack.pop();
|
|
17
|
+
// primitives are always valid
|
|
18
|
+
if (current === null ||
|
|
19
|
+
typeof current === "string" ||
|
|
20
|
+
typeof current === "number" ||
|
|
21
|
+
typeof current === "boolean") {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
// array → validate each item
|
|
25
|
+
if (Array.isArray(current)) {
|
|
26
|
+
for (const item of current) {
|
|
27
|
+
stack.push(item);
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// object → validate each value
|
|
32
|
+
if (isPlainObject(current)) {
|
|
33
|
+
for (const v of Object.values(current)) {
|
|
34
|
+
stack.push(v);
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
// everything else is invalid
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { isPlainObject, isValidMessages };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wraps a value inside nested objects according to a given path.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* ```ts
|
|
6
|
+
* const value = { a: "A" };
|
|
7
|
+
*
|
|
8
|
+
* nestObjectFromPath(["auth", "verify"], value); // → { auth: { verify: { a: "A" } } }
|
|
9
|
+
*
|
|
10
|
+
* nestObjectFromPath([], value); // → { a: "A" }
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
function nestObjectFromPath(path, value) {
|
|
14
|
+
let obj = value;
|
|
15
|
+
for (let i = path.length - 1; i >= 0; i--) {
|
|
16
|
+
obj = { [path[i]]: obj };
|
|
17
|
+
}
|
|
18
|
+
return obj;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export { nestObjectFromPath };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { escapeHtml } from './utils/escape-html.js';
|
|
2
|
+
import { renderAttributes } from './utils/render-attributes.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create an HTML string renderer for semantic rich messages.
|
|
6
|
+
*
|
|
7
|
+
* - Transforms semantic rich AST nodes into escaped HTML strings.
|
|
8
|
+
* - Can be used in any HTML-based environment.
|
|
9
|
+
*
|
|
10
|
+
* This renderer is intentionally minimal and stateless.
|
|
11
|
+
*/
|
|
12
|
+
const createHtmlRenderer = (tagRenderers) => {
|
|
13
|
+
return {
|
|
14
|
+
/** Render plain text nodes */
|
|
15
|
+
text(value) {
|
|
16
|
+
return escapeHtml(value);
|
|
17
|
+
},
|
|
18
|
+
/** Render semantic tag nodes */
|
|
19
|
+
tag(name, attributes, children) {
|
|
20
|
+
const tagRenderer = tagRenderers?.[name];
|
|
21
|
+
if (tagRenderer) {
|
|
22
|
+
return typeof tagRenderer === "function"
|
|
23
|
+
? tagRenderer(children)
|
|
24
|
+
: tagRenderer;
|
|
25
|
+
}
|
|
26
|
+
// Default behavior: render as native HTML tag
|
|
27
|
+
return `<${name}${renderAttributes(attributes)}>${children.join("")}</${name}>`;
|
|
28
|
+
},
|
|
29
|
+
/** Render raw (non-tokenized) message values as escaped HTML strings */
|
|
30
|
+
raw(value) {
|
|
31
|
+
if (value == null)
|
|
32
|
+
return "";
|
|
33
|
+
if (typeof value === "string" || typeof value === "number") {
|
|
34
|
+
return escapeHtml(String(value));
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value)) {
|
|
37
|
+
return value.map((v) => escapeHtml(String(v))).join("");
|
|
38
|
+
}
|
|
39
|
+
throw new Error("[intor] HTML renderer cannot render raw non-primitive values.");
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export { createHtmlRenderer };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { escapeHtml } from './escape-html.js';
|
|
2
|
+
|
|
3
|
+
function renderAttributes(attributes) {
|
|
4
|
+
if (!attributes)
|
|
5
|
+
return "";
|
|
6
|
+
return Object.entries(attributes)
|
|
7
|
+
.map(([key, value]) => {
|
|
8
|
+
if (value === true)
|
|
9
|
+
return ` ${key}`;
|
|
10
|
+
if (value == null)
|
|
11
|
+
return "";
|
|
12
|
+
return ` ${key}="${escapeHtml(String(value))}"`;
|
|
13
|
+
})
|
|
14
|
+
.join("");
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export { renderAttributes };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { renderRichMessage } from 'intor-translator';
|
|
2
|
+
import { createHtmlRenderer } from '../render/create-html-renderer.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Create an HTML-string rich translation function.
|
|
6
|
+
*
|
|
7
|
+
* - Resolves translated messages via `translator.t`
|
|
8
|
+
* - Renders semantic rich tags into escaped HTML strings
|
|
9
|
+
* - Supports optional scoped keys via `preKey`
|
|
10
|
+
*
|
|
11
|
+
* Can be used in any HTML-based environment (Astro, Svelte, SSR, etc.).
|
|
12
|
+
*/
|
|
13
|
+
const createTRich = (t) => {
|
|
14
|
+
return (key, tagRenderers, replacements) => {
|
|
15
|
+
const message = t(key, replacements);
|
|
16
|
+
const renderer = createHtmlRenderer(tagRenderers);
|
|
17
|
+
const nodes = renderRichMessage(message, renderer);
|
|
18
|
+
return nodes.join("");
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
export { createTRich };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deeply merges two plain objects.
|
|
3
|
+
*
|
|
4
|
+
* - Nested plain objects → merged recursively
|
|
5
|
+
* - Arrays / primitives → `b` overwrites `a`
|
|
6
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
const deepMerge = (a = {}, b = {}, options) => {
|
|
14
|
+
const result = { ...a };
|
|
15
|
+
const basePath = options?._path ?? [];
|
|
16
|
+
// Iterate only over b's own enumerable properties
|
|
17
|
+
for (const key in b) {
|
|
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;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
export { deepMerge };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const toCanonical = (input) => {
|
|
2
|
+
try {
|
|
3
|
+
return Intl.getCanonicalLocales(input)[0];
|
|
4
|
+
}
|
|
5
|
+
catch {
|
|
6
|
+
return;
|
|
7
|
+
}
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* Normalizes a locale string and resolves the best match
|
|
11
|
+
* from a list of supported locales.
|
|
12
|
+
*
|
|
13
|
+
* Resolution strategy:
|
|
14
|
+
*
|
|
15
|
+
* 1. Exact canonical match (BCP 47)
|
|
16
|
+
* 2. Base language fallback
|
|
17
|
+
* - Falls back by base language when no exact match is found
|
|
18
|
+
* (e.g. `"en"` → `"en-US"`).
|
|
19
|
+
* - Script and region subtags are ignored during this step
|
|
20
|
+
* (e.g. `"zh-Hans"` → `"zh-Hant-TW"`)
|
|
21
|
+
* - Preference is determined by the order of `supportedLocales`.
|
|
22
|
+
*
|
|
23
|
+
* Returns `undefined` if no suitable match is found.
|
|
24
|
+
*
|
|
25
|
+
* Notes:
|
|
26
|
+
* - Invalid locale inputs are ignored gracefully.
|
|
27
|
+
* - Always returns an original entry from `supportedLocales`.
|
|
28
|
+
* - Requires `Intl` locale support in the runtime.
|
|
29
|
+
*/
|
|
30
|
+
const normalizeLocale = (locale, supportedLocales = []) => {
|
|
31
|
+
if (!locale || supportedLocales.length === 0)
|
|
32
|
+
return;
|
|
33
|
+
const canonicalLocale = toCanonical(locale);
|
|
34
|
+
if (!canonicalLocale)
|
|
35
|
+
return;
|
|
36
|
+
const supportedCanonicalMap = new Map();
|
|
37
|
+
for (const l of supportedLocales) {
|
|
38
|
+
const normalized = toCanonical(l);
|
|
39
|
+
if (normalized) {
|
|
40
|
+
supportedCanonicalMap.set(normalized, l);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
// 1. Exact match
|
|
44
|
+
if (supportedCanonicalMap.has(canonicalLocale)) {
|
|
45
|
+
return supportedCanonicalMap.get(canonicalLocale);
|
|
46
|
+
}
|
|
47
|
+
const baseLang = canonicalLocale.split("-")[0];
|
|
48
|
+
// 2. Match by same base language (e.g., "en" matches "en-US" or "en-GB")
|
|
49
|
+
for (const [key, original] of supportedCanonicalMap) {
|
|
50
|
+
const supportedBase = key.split("-")[0];
|
|
51
|
+
if (supportedBase === baseLang) {
|
|
52
|
+
return original;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
// 3. No match
|
|
56
|
+
return;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export { normalizeLocale };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a raw pathname string to ensure consistent formatting.
|
|
3
|
+
*
|
|
4
|
+
* - Trims leading and trailing whitespace (code points ≤ 32).
|
|
5
|
+
* - Collapses consecutive slashes into a single slash.
|
|
6
|
+
* - Ensures a single leading slash and removes redundant trailing slashes.
|
|
7
|
+
* - Optionally removes the leading slash.
|
|
8
|
+
* - Avoids intermediate array allocations for performance.
|
|
9
|
+
*/
|
|
10
|
+
const normalizePathname = (rawPathname, options = {}) => {
|
|
11
|
+
const length = rawPathname.length;
|
|
12
|
+
let start = 0;
|
|
13
|
+
let end = length - 1;
|
|
14
|
+
// Trim leading whitespace
|
|
15
|
+
while (start <= end && (rawPathname.codePointAt(start) ?? 0) <= 32)
|
|
16
|
+
start++;
|
|
17
|
+
// Trim trailing whitespace
|
|
18
|
+
while (end >= start && (rawPathname.codePointAt(end) ?? 0) <= 32)
|
|
19
|
+
end--;
|
|
20
|
+
if (start > end)
|
|
21
|
+
return "/"; // Only whitespace
|
|
22
|
+
let result = "";
|
|
23
|
+
let hasSlash = false;
|
|
24
|
+
for (let i = start; i <= end; i++) {
|
|
25
|
+
const char = rawPathname[i];
|
|
26
|
+
if (char === "/") {
|
|
27
|
+
if (!hasSlash) {
|
|
28
|
+
hasSlash = true;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
result += hasSlash || result === "" ? "/" + char : char;
|
|
33
|
+
hasSlash = false;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
// If the result has a leading slash and we want to remove it, do so
|
|
37
|
+
if (options.removeLeadingSlash && result.startsWith("/")) {
|
|
38
|
+
result = result.slice(1);
|
|
39
|
+
}
|
|
40
|
+
return result || "/";
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export { normalizePathname };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Normalize a raw query object into a string-only map.
|
|
3
|
+
*
|
|
4
|
+
* This utility is used to sanitize framework-specific query inputs
|
|
5
|
+
* into a stable shape that the routing core
|
|
6
|
+
* can safely consume.
|
|
7
|
+
*
|
|
8
|
+
* Behavior:
|
|
9
|
+
* - Keeps only entries whose values are strings
|
|
10
|
+
* - Ignores arrays, objects, and other non-string values
|
|
11
|
+
* - Does not throw or attempt to coerce values
|
|
12
|
+
*
|
|
13
|
+
* This function is intentionally conservative by design.
|
|
14
|
+
*/
|
|
15
|
+
function normalizeQuery(query) {
|
|
16
|
+
const normalized = {};
|
|
17
|
+
for (const [key, value] of Object.entries(query)) {
|
|
18
|
+
if (typeof value === "string") {
|
|
19
|
+
normalized[key] = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export { normalizeQuery };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse a raw HTTP Cookie header into a key-value record.
|
|
3
|
+
*/
|
|
4
|
+
function parseCookieHeader(cookieHeader) {
|
|
5
|
+
if (!cookieHeader)
|
|
6
|
+
return {};
|
|
7
|
+
const record = {};
|
|
8
|
+
// Split the Cookie header into individual key-value pairs
|
|
9
|
+
const pairs = cookieHeader.split(";");
|
|
10
|
+
for (const pair of pairs) {
|
|
11
|
+
// Locate the first "=" to separate key and value
|
|
12
|
+
const index = pair.indexOf("=");
|
|
13
|
+
if (index === -1)
|
|
14
|
+
continue;
|
|
15
|
+
const key = pair.slice(0, index).trim();
|
|
16
|
+
const value = pair.slice(index + 1).trim();
|
|
17
|
+
record[key] = decodeURIComponent(value);
|
|
18
|
+
}
|
|
19
|
+
return record;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { parseCookieHeader };
|