intor 2.3.14 → 2.3.15
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/dist/core/src/config/resolvers/resolve-fallback-locales.js +1 -1
- package/dist/core/src/core/messages/utils/is-valid-messages.js +23 -15
- package/dist/core/src/server/helpers/get-translator.js +3 -2
- package/dist/core/src/server/helpers/local-messages-from-url.js +1 -1
- package/dist/core/src/server/messages/load-local-messages/load-local-messages.js +2 -2
- package/dist/core/src/server/messages/load-local-messages/read-locale-messages/collect-file-entries/collect-file-entries.js +6 -5
- package/dist/core/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/parse-file-entries.js +14 -5
- package/dist/core/src/server/messages/load-local-messages/read-locale-messages/read-locale-messages.js +3 -3
- package/dist/core/src/server/messages/load-messages.js +4 -4
- package/dist/core/src/server/runtime/create-intor-runtime.js +12 -7
- package/dist/core/src/server/translator/create-translator.js +1 -0
- package/dist/express/src/adapters/express/helpers/get-translator.js +3 -3
- package/dist/express/src/adapters/express/middleware/create-intor.js +5 -4
- package/dist/express/src/core/messages/utils/is-valid-messages.js +23 -15
- package/dist/express/src/routing/inbound/resolve-inbound.js +5 -5
- package/dist/express/src/routing/inbound/resolve-locale/resolve-locale.js +13 -6
- package/dist/express/src/routing/locale/get-locale-from-accept-language.js +7 -16
- package/dist/express/src/routing/locale/get-locale-from-host.js +13 -14
- package/dist/express/src/routing/locale/get-locale-from-pathname.js +4 -13
- package/dist/express/src/routing/locale/get-locale-from-query.js +10 -16
- package/dist/express/src/server/helpers/get-translator.js +3 -2
- package/dist/express/src/server/messages/load-local-messages/load-local-messages.js +2 -2
- package/dist/express/src/server/messages/load-local-messages/read-locale-messages/collect-file-entries/collect-file-entries.js +6 -5
- package/dist/express/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/parse-file-entries.js +14 -5
- package/dist/express/src/server/messages/load-local-messages/read-locale-messages/read-locale-messages.js +3 -3
- package/dist/express/src/server/messages/load-messages.js +4 -4
- package/dist/express/src/server/runtime/create-intor-runtime.js +12 -7
- package/dist/express/src/server/translator/create-translator.js +1 -0
- package/dist/next/src/adapters/next/proxy/intor-proxy.js +1 -1
- package/dist/next/src/adapters/next/server/get-translator.js +3 -3
- package/dist/next/src/adapters/next/server/intor.js +2 -2
- package/dist/next/src/core/messages/utils/is-valid-messages.js +23 -15
- package/dist/next/src/routing/inbound/resolve-inbound.js +5 -5
- package/dist/next/src/routing/inbound/resolve-locale/resolve-locale.js +13 -6
- package/dist/next/src/routing/locale/get-locale-from-accept-language.js +7 -16
- package/dist/next/src/routing/locale/get-locale-from-host.js +13 -14
- package/dist/next/src/routing/locale/get-locale-from-pathname.js +4 -13
- package/dist/next/src/routing/locale/get-locale-from-query.js +10 -16
- package/dist/next/src/server/helpers/get-translator.js +3 -2
- package/dist/next/src/server/messages/load-local-messages/load-local-messages.js +2 -2
- package/dist/next/src/server/messages/load-local-messages/read-locale-messages/collect-file-entries/collect-file-entries.js +6 -5
- package/dist/next/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/parse-file-entries.js +14 -5
- package/dist/next/src/server/messages/load-local-messages/read-locale-messages/read-locale-messages.js +3 -3
- package/dist/next/src/server/messages/load-messages.js +4 -4
- package/dist/next/src/server/runtime/create-intor-runtime.js +12 -7
- package/dist/next/src/server/translator/create-translator.js +1 -0
- package/dist/react/src/client/react/translator/use-translator.js +1 -0
- package/dist/react/src/core/messages/utils/is-valid-messages.js +23 -15
- package/dist/svelte/src/client/svelte/runtime/create-intor-api.js +4 -0
- package/dist/svelte/src/client/svelte/runtime/create-intor.js +2 -1
- package/dist/svelte/src/core/messages/utils/is-valid-messages.js +23 -15
- package/dist/types/export/index.d.ts +2 -2
- package/dist/types/src/adapters/express/global.d.ts +2 -1
- package/dist/types/src/adapters/express/helpers/get-translator.d.ts +1 -1
- package/dist/types/src/adapters/next/server/get-translator.d.ts +1 -1
- package/dist/types/src/adapters/next/server/intor.d.ts +2 -2
- package/dist/types/src/client/svelte/runtime/create-intor-api.d.ts +2 -0
- package/dist/types/src/client/svelte/runtime/types.d.ts +3 -1
- package/dist/types/src/client/vue/provider/resolver/resolve-runtime.d.ts +1 -1
- package/dist/types/src/core/index.d.ts +1 -1
- package/dist/types/src/core/messages/index.d.ts +1 -1
- package/dist/types/src/core/messages/types.d.ts +14 -36
- package/dist/types/src/core/messages/utils/is-valid-messages.d.ts +5 -10
- package/dist/types/src/core/types/translator-instance.d.ts +3 -1
- package/dist/types/src/routing/inbound/resolve-locale/resolve-locale.d.ts +4 -3
- package/dist/types/src/routing/locale/get-locale-from-accept-language.d.ts +5 -7
- package/dist/types/src/routing/locale/get-locale-from-host.d.ts +12 -8
- package/dist/types/src/routing/locale/get-locale-from-pathname.d.ts +4 -13
- package/dist/types/src/routing/locale/get-locale-from-query.d.ts +9 -10
- package/dist/types/src/server/helpers/get-translator.d.ts +2 -3
- 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/read-locale-messages/parse-file-entries/parse-file-entries.d.ts +2 -2
- package/dist/types/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/types.d.ts +4 -3
- package/dist/types/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/utils/json-reader.d.ts +2 -2
- package/dist/types/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/utils/nest-object-from-path.d.ts +2 -2
- package/dist/types/src/server/messages/load-local-messages/read-locale-messages/read-locale-messages.d.ts +1 -1
- package/dist/types/src/server/messages/load-local-messages/read-locale-messages/types.d.ts +2 -2
- package/dist/types/src/server/messages/load-local-messages/types.d.ts +2 -2
- package/dist/types/src/server/messages/load-messages.d.ts +1 -1
- package/dist/types/src/server/messages/types.d.ts +2 -2
- package/dist/types/src/server/runtime/types.d.ts +2 -2
- package/dist/types/src/server/translator/create-translator.d.ts +1 -0
- package/dist/vue/src/client/vue/translator/use-translator.js +3 -1
- package/dist/vue/src/core/messages/utils/is-valid-messages.js +23 -15
- package/package.json +2 -2
|
@@ -12,7 +12,7 @@ const resolveFallbackLocales = (config, id, supportedSet) => {
|
|
|
12
12
|
if (!fallbackLocales || typeof fallbackLocales !== "object") {
|
|
13
13
|
return {};
|
|
14
14
|
}
|
|
15
|
-
const logger = getLogger({ id }).child({
|
|
15
|
+
const logger = getLogger({ ...config.logger, id }).child({
|
|
16
16
|
scope: "resolve-fallback-locales",
|
|
17
17
|
});
|
|
18
18
|
const resolvedMap = {};
|
|
@@ -3,15 +3,10 @@ function isPlainObject(value) {
|
|
|
3
3
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Check if a value is a valid
|
|
6
|
+
* Check if a value is a valid MessageObject.
|
|
7
7
|
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* isValidMessages({ en: { hello: "Hello" } }) // true
|
|
13
|
-
* isValidMessages({ en: { count: 5 } }) // false
|
|
14
|
-
* ```
|
|
8
|
+
* - Supports all MessageValue variants (primitive, array, object).
|
|
9
|
+
* - Uses an iterative approach to avoid stack overflow.
|
|
15
10
|
*/
|
|
16
11
|
function isValidMessages(value) {
|
|
17
12
|
if (!isPlainObject(value))
|
|
@@ -19,16 +14,29 @@ function isValidMessages(value) {
|
|
|
19
14
|
const stack = [value];
|
|
20
15
|
while (stack.length > 0) {
|
|
21
16
|
const current = stack.pop();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// object → validate each value
|
|
32
|
+
if (isPlainObject(current)) {
|
|
33
|
+
for (const v of Object.values(current)) {
|
|
34
|
+
stack.push(v);
|
|
30
35
|
}
|
|
36
|
+
continue;
|
|
31
37
|
}
|
|
38
|
+
// everything else is invalid
|
|
39
|
+
return false;
|
|
32
40
|
}
|
|
33
41
|
return true;
|
|
34
42
|
}
|
|
@@ -2,11 +2,11 @@ import { createIntorRuntime } from '../runtime/create-intor-runtime.js';
|
|
|
2
2
|
|
|
3
3
|
// Implementation
|
|
4
4
|
async function getTranslator(config, params) {
|
|
5
|
-
const {
|
|
5
|
+
const { readers, allowCacheWrite, preKey, handlers, plugins } = params;
|
|
6
6
|
const locale = params.locale;
|
|
7
7
|
// Create runtime (request-scoped, no cache write)
|
|
8
8
|
const runtime = createIntorRuntime(config, {
|
|
9
|
-
|
|
9
|
+
readers,
|
|
10
10
|
allowCacheWrite,
|
|
11
11
|
});
|
|
12
12
|
// Ensure messages & create translator snapshot
|
|
@@ -21,6 +21,7 @@ async function getTranslator(config, params) {
|
|
|
21
21
|
locale,
|
|
22
22
|
hasKey: translator.hasKey,
|
|
23
23
|
t: translator.t,
|
|
24
|
+
tRaw: translator.tRaw,
|
|
24
25
|
};
|
|
25
26
|
}
|
|
26
27
|
|
|
@@ -43,7 +43,7 @@ async function loadMessagesFromUrl(url, options) {
|
|
|
43
43
|
namespaces,
|
|
44
44
|
fallbackLocales,
|
|
45
45
|
concurrency: options?.concurrency,
|
|
46
|
-
|
|
46
|
+
readers: options?.readers,
|
|
47
47
|
pool: options?.pool,
|
|
48
48
|
allowCacheWrite: options?.allowCacheWrite,
|
|
49
49
|
loggerOptions: options?.loggerOptions || { id: "default" },
|
|
@@ -21,7 +21,7 @@ import { readLocaleMessages } from './read-locale-messages/read-locale-messages.
|
|
|
21
21
|
*
|
|
22
22
|
* File traversal, parsing, and validation are delegated to lower-level utilities.
|
|
23
23
|
*/
|
|
24
|
-
const loadLocalMessages = async ({ id, locale, fallbackLocales, namespaces, rootDir = "messages", concurrency = 10,
|
|
24
|
+
const loadLocalMessages = async ({ id, locale, fallbackLocales, namespaces, rootDir = "messages", concurrency = 10, readers, pool = getGlobalMessagesPool(), allowCacheWrite = false, loggerOptions, }) => {
|
|
25
25
|
const baseLogger = getLogger(loggerOptions);
|
|
26
26
|
const logger = baseLogger.child({ scope: "load-local-messages" });
|
|
27
27
|
const start = performance.now();
|
|
@@ -65,7 +65,7 @@ const loadLocalMessages = async ({ id, locale, fallbackLocales, namespaces, root
|
|
|
65
65
|
namespaces,
|
|
66
66
|
rootDir,
|
|
67
67
|
limit,
|
|
68
|
-
|
|
68
|
+
readers,
|
|
69
69
|
loggerOptions,
|
|
70
70
|
});
|
|
71
71
|
// Stop at the first locale that yields non-empty messages
|
|
@@ -20,9 +20,10 @@ import { getLogger } from '../../../../../core/logger/get-logger.js';
|
|
|
20
20
|
* }, ... ];
|
|
21
21
|
* ```
|
|
22
22
|
*/
|
|
23
|
-
async function collectFileEntries({ readdir = fs.readdir, namespaces, rootDir, limit, exts = ["
|
|
23
|
+
async function collectFileEntries({ readdir = fs.readdir, namespaces, rootDir, limit, exts = ["json"], loggerOptions, }) {
|
|
24
24
|
const baseLogger = getLogger(loggerOptions);
|
|
25
25
|
const logger = baseLogger.child({ scope: "collect-file-entries" });
|
|
26
|
+
const supportedExts = new Set(["json", ...exts]);
|
|
26
27
|
const fileEntries = [];
|
|
27
28
|
// Recursive directory walk
|
|
28
29
|
const walk = async (currentDir) => {
|
|
@@ -50,14 +51,14 @@ async function collectFileEntries({ readdir = fs.readdir, namespaces, rootDir, l
|
|
|
50
51
|
return;
|
|
51
52
|
}
|
|
52
53
|
// Skip unsupported file extensions
|
|
53
|
-
|
|
54
|
+
const ext = path.extname(entry.name).slice(1); // "json", "yaml"
|
|
55
|
+
if (!ext || !supportedExts.has(ext))
|
|
54
56
|
return;
|
|
55
57
|
// ---------------------------------------------------------------------
|
|
56
58
|
// Resolve file entry
|
|
57
59
|
// ---------------------------------------------------------------------
|
|
58
60
|
const relativePath = path.relative(rootDir, fullPath);
|
|
59
|
-
const
|
|
60
|
-
const withoutExt = relativePath.slice(0, -ext.length);
|
|
61
|
+
const withoutExt = relativePath.slice(0, relativePath.length - (ext.length + 1));
|
|
61
62
|
const segments = withoutExt.split(path.sep).filter(Boolean);
|
|
62
63
|
const namespace = segments.at(0);
|
|
63
64
|
if (!namespace)
|
|
@@ -72,7 +73,7 @@ async function collectFileEntries({ readdir = fs.readdir, namespaces, rootDir, l
|
|
|
72
73
|
fullPath,
|
|
73
74
|
relativePath,
|
|
74
75
|
segments,
|
|
75
|
-
basename: path.basename(entry.name, ext),
|
|
76
|
+
basename: path.basename(entry.name, `.${ext}`),
|
|
76
77
|
});
|
|
77
78
|
}));
|
|
78
79
|
await Promise.all(tasks);
|
|
@@ -35,7 +35,7 @@ import { nestObjectFromPath } from './utils/nest-object-from-path.js';
|
|
|
35
35
|
* }
|
|
36
36
|
* ```
|
|
37
37
|
*/
|
|
38
|
-
async function parseFileEntries({ fileEntries, limit,
|
|
38
|
+
async function parseFileEntries({ fileEntries, limit, readers, loggerOptions, }) {
|
|
39
39
|
const baseLogger = getLogger(loggerOptions);
|
|
40
40
|
const logger = baseLogger.child({ scope: "parse-file-entries" });
|
|
41
41
|
// Read and parse all file entries
|
|
@@ -45,10 +45,19 @@ async function parseFileEntries({ fileEntries, limit, messagesReader, loggerOpti
|
|
|
45
45
|
// -------------------------------------------------------------------
|
|
46
46
|
// Read and validate file content
|
|
47
47
|
// -------------------------------------------------------------------
|
|
48
|
-
const ext = path.extname(fullPath);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
48
|
+
const ext = path.extname(fullPath).slice(1); // remove dot
|
|
49
|
+
let raw;
|
|
50
|
+
if (ext === "json") {
|
|
51
|
+
raw = await jsonReader(fullPath);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
const reader = readers?.[ext];
|
|
55
|
+
if (!reader) {
|
|
56
|
+
throw new Error(`No message reader registered for .${ext} files. ` +
|
|
57
|
+
`Please register a reader for the "${ext}" extension.`);
|
|
58
|
+
}
|
|
59
|
+
raw = await reader(fullPath);
|
|
60
|
+
}
|
|
52
61
|
// Validate messages structure
|
|
53
62
|
if (!isValidMessages(raw)) {
|
|
54
63
|
throw new Error("Parsed content does not match expected Messages structure");
|
|
@@ -12,7 +12,7 @@ import { parseFileEntries } from './parse-file-entries/parse-file-entries.js';
|
|
|
12
12
|
*
|
|
13
13
|
* It does not perform validation or transformation itself.
|
|
14
14
|
*/
|
|
15
|
-
const readLocaleMessages = async ({ locale, namespaces, rootDir = "messages", limit,
|
|
15
|
+
const readLocaleMessages = async ({ locale, namespaces, rootDir = "messages", limit, readers, loggerOptions, }) => {
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
// Collect message file entries for the locale
|
|
18
18
|
// ---------------------------------------------------------------------------
|
|
@@ -20,7 +20,7 @@ const readLocaleMessages = async ({ locale, namespaces, rootDir = "messages", li
|
|
|
20
20
|
namespaces,
|
|
21
21
|
rootDir: path.resolve(rootDir, locale),
|
|
22
22
|
limit,
|
|
23
|
-
exts,
|
|
23
|
+
exts: Object.keys(readers || {}),
|
|
24
24
|
loggerOptions,
|
|
25
25
|
});
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
@@ -29,7 +29,7 @@ const readLocaleMessages = async ({ locale, namespaces, rootDir = "messages", li
|
|
|
29
29
|
const messages = await parseFileEntries({
|
|
30
30
|
fileEntries,
|
|
31
31
|
limit,
|
|
32
|
-
|
|
32
|
+
readers,
|
|
33
33
|
loggerOptions,
|
|
34
34
|
});
|
|
35
35
|
// ---------------------------------------------------------------------------
|
|
@@ -17,7 +17,7 @@ import { loadLocalMessages } from './load-local-messages/load-local-messages.js'
|
|
|
17
17
|
* Message traversal, parsing, fallback resolution, and caching logic
|
|
18
18
|
* are delegated to the selected loader.
|
|
19
19
|
*/
|
|
20
|
-
const loadMessages = async ({ config, locale,
|
|
20
|
+
const loadMessages = async ({ config, locale, readers, allowCacheWrite = false, }) => {
|
|
21
21
|
const baseLogger = getLogger(config.logger);
|
|
22
22
|
const logger = baseLogger.child({ scope: "load-messages" });
|
|
23
23
|
// ---------------------------------------------------------------------------
|
|
@@ -35,8 +35,8 @@ const loadMessages = async ({ config, locale, readOptions, allowCacheWrite = fal
|
|
|
35
35
|
loaderType: type,
|
|
36
36
|
rootDir,
|
|
37
37
|
locale,
|
|
38
|
-
fallbackLocales,
|
|
39
|
-
namespaces: namespaces && namespaces.length > 0 ? [...namespaces] :
|
|
38
|
+
fallbackLocales: fallbackLocales.join(", "),
|
|
39
|
+
namespaces: namespaces && namespaces.length > 0 ? [...namespaces] : "*",
|
|
40
40
|
});
|
|
41
41
|
// ---------------------------------------------------------------------------
|
|
42
42
|
// Dispatch to loader implementation
|
|
@@ -50,7 +50,7 @@ const loadMessages = async ({ config, locale, readOptions, allowCacheWrite = fal
|
|
|
50
50
|
namespaces,
|
|
51
51
|
rootDir,
|
|
52
52
|
concurrency: loader.concurrency,
|
|
53
|
-
|
|
53
|
+
readers,
|
|
54
54
|
allowCacheWrite,
|
|
55
55
|
loggerOptions: config.logger,
|
|
56
56
|
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { IntorError, IntorErrorCode } from '../../core/error/intor-error.js';
|
|
2
|
+
import { resolveLoaderOptions } from '../../core/utils/resolve-loader-options.js';
|
|
2
3
|
import 'logry';
|
|
3
4
|
import { loadMessages } from '../messages/load-messages.js';
|
|
4
5
|
import { createTranslator } from '../translator/create-translator.js';
|
|
@@ -13,20 +14,24 @@ import { createTranslator } from '../translator/create-translator.js';
|
|
|
13
14
|
* before a translator snapshot can be created.
|
|
14
15
|
*/
|
|
15
16
|
function createIntorRuntime(config, options) {
|
|
17
|
+
const loader = resolveLoaderOptions(config, "server");
|
|
16
18
|
// Locale that has completed the ensureMessages() phase
|
|
17
19
|
let ensuredLocale;
|
|
18
20
|
// Messages prepared during ensureMessages(); may be empty
|
|
19
21
|
let ensuredMessages;
|
|
20
22
|
return {
|
|
21
23
|
async ensureMessages(locale) {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
24
|
+
let messages;
|
|
25
|
+
if (loader) {
|
|
26
|
+
messages = await loadMessages({
|
|
27
|
+
config,
|
|
28
|
+
locale,
|
|
29
|
+
readers: options?.readers,
|
|
30
|
+
allowCacheWrite: options?.allowCacheWrite || false,
|
|
31
|
+
});
|
|
32
|
+
}
|
|
28
33
|
ensuredLocale = locale;
|
|
29
|
-
ensuredMessages = messages;
|
|
34
|
+
ensuredMessages = messages || {};
|
|
30
35
|
},
|
|
31
36
|
translator(locale, options) {
|
|
32
37
|
// Guard: translator requires ensureMessages() to be completed for this locale
|
|
@@ -8,14 +8,14 @@ import { getTranslator as getTranslator$1 } from '../../../server/helpers/get-tr
|
|
|
8
8
|
|
|
9
9
|
// Implementation
|
|
10
10
|
async function getTranslator(config, req, params) {
|
|
11
|
-
const { preKey, handlers, plugins,
|
|
11
|
+
const { preKey, handlers, plugins, readers, allowCacheWrite } = params || {};
|
|
12
12
|
return getTranslator$1(config, {
|
|
13
13
|
locale: req.intor?.locale || config.defaultLocale,
|
|
14
14
|
preKey,
|
|
15
15
|
handlers,
|
|
16
16
|
plugins,
|
|
17
|
-
|
|
18
|
-
allowCacheWrite
|
|
17
|
+
readers,
|
|
18
|
+
allowCacheWrite,
|
|
19
19
|
});
|
|
20
20
|
}
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ function createIntor(config, options) {
|
|
|
24
24
|
return async function intorMiddleware(req, _res, next) {
|
|
25
25
|
// locale from accept-language header
|
|
26
26
|
const acceptLanguage = req.headers["accept-language"];
|
|
27
|
-
const localeFromAcceptLanguage = getLocaleFromAcceptLanguage(
|
|
27
|
+
const localeFromAcceptLanguage = getLocaleFromAcceptLanguage(acceptLanguage, config.supportedLocales);
|
|
28
28
|
// ----------------------------------------------------------
|
|
29
29
|
// Resolve inbound routing decision (pure computation)
|
|
30
30
|
// ----------------------------------------------------------
|
|
@@ -43,19 +43,20 @@ function createIntor(config, options) {
|
|
|
43
43
|
// --------------------------------------------------
|
|
44
44
|
// Bind inbound routing context
|
|
45
45
|
// --------------------------------------------------
|
|
46
|
-
const { t,
|
|
46
|
+
const { hasKey, t, tRaw } = (await getTranslator(config, {
|
|
47
47
|
locale,
|
|
48
48
|
handlers: options?.handlers,
|
|
49
49
|
plugins: options?.plugins,
|
|
50
|
-
|
|
50
|
+
readers: options?.readers,
|
|
51
51
|
allowCacheWrite: true,
|
|
52
52
|
}));
|
|
53
53
|
req.intor = { locale, localeSource };
|
|
54
54
|
// DX shortcuts (optional)
|
|
55
55
|
req.locale = locale;
|
|
56
56
|
req.localeSource = localeSource;
|
|
57
|
-
req.t = t;
|
|
58
57
|
req.hasKey = hasKey;
|
|
58
|
+
req.t = t;
|
|
59
|
+
req.tRaw = tRaw;
|
|
59
60
|
return next();
|
|
60
61
|
};
|
|
61
62
|
}
|
|
@@ -3,15 +3,10 @@ function isPlainObject(value) {
|
|
|
3
3
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
4
4
|
}
|
|
5
5
|
/**
|
|
6
|
-
* Check if a value is a valid
|
|
6
|
+
* Check if a value is a valid MessageObject.
|
|
7
7
|
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
* @example
|
|
11
|
-
* ```ts
|
|
12
|
-
* isValidMessages({ en: { hello: "Hello" } }) // true
|
|
13
|
-
* isValidMessages({ en: { count: 5 } }) // false
|
|
14
|
-
* ```
|
|
8
|
+
* - Supports all MessageValue variants (primitive, array, object).
|
|
9
|
+
* - Uses an iterative approach to avoid stack overflow.
|
|
15
10
|
*/
|
|
16
11
|
function isValidMessages(value) {
|
|
17
12
|
if (!isPlainObject(value))
|
|
@@ -19,16 +14,29 @@ function isValidMessages(value) {
|
|
|
19
14
|
const stack = [value];
|
|
20
15
|
while (stack.length > 0) {
|
|
21
16
|
const current = stack.pop();
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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);
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
-
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// object → validate each value
|
|
32
|
+
if (isPlainObject(current)) {
|
|
33
|
+
for (const v of Object.values(current)) {
|
|
34
|
+
stack.push(v);
|
|
30
35
|
}
|
|
36
|
+
continue;
|
|
31
37
|
}
|
|
38
|
+
// everything else is invalid
|
|
39
|
+
return false;
|
|
32
40
|
}
|
|
33
41
|
return true;
|
|
34
42
|
}
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
import '../../core/error/intor-error.js';
|
|
2
|
-
import 'logry';
|
|
3
1
|
import { getLocaleFromPathname } from '../locale/get-locale-from-pathname.js';
|
|
4
2
|
import { getLocaleFromHost } from '../locale/get-locale-from-host.js';
|
|
5
3
|
import { getLocaleFromQuery } from '../locale/get-locale-from-query.js';
|
|
@@ -20,11 +18,13 @@ async function resolveInbound(config, rawPathname, hasRedirected, localeInputs)
|
|
|
20
18
|
// ------------------------------------------------------
|
|
21
19
|
// Resolve locale from inbound inputs
|
|
22
20
|
// ------------------------------------------------------
|
|
23
|
-
const pathLocale = getLocaleFromPathname(
|
|
21
|
+
const pathLocale = getLocaleFromPathname(rawPathname, config);
|
|
24
22
|
const { locale, localeSource } = resolveLocale(config, {
|
|
25
23
|
path: { locale: pathLocale },
|
|
26
|
-
host: { locale: getLocaleFromHost(
|
|
27
|
-
query: {
|
|
24
|
+
host: { locale: getLocaleFromHost(host) },
|
|
25
|
+
query: {
|
|
26
|
+
locale: getLocaleFromQuery(query, config.routing.inbound.queryKey),
|
|
27
|
+
},
|
|
28
28
|
cookie: { locale: cookie },
|
|
29
29
|
detected: { locale: detected },
|
|
30
30
|
});
|
|
@@ -1,23 +1,30 @@
|
|
|
1
|
+
import '../../../core/error/intor-error.js';
|
|
2
|
+
import { normalizeLocale } from '../../../core/utils/normalizers/normalize-locale.js';
|
|
3
|
+
import 'logry';
|
|
4
|
+
|
|
1
5
|
/**
|
|
2
|
-
*
|
|
6
|
+
* Resolve the active locale from inbound routing configuration.
|
|
3
7
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
8
|
+
* Iterates through configured locale sources and returns the first
|
|
9
|
+
* normalized, supported locale. Falls back to the detected locale
|
|
10
|
+
* or the default locale if none match.
|
|
6
11
|
*/
|
|
7
12
|
function resolveLocale(config, context) {
|
|
8
13
|
const { localeSources } = config.routing.inbound;
|
|
9
14
|
for (const source of localeSources) {
|
|
10
15
|
const locale = context[source]?.locale;
|
|
11
|
-
|
|
16
|
+
const normalized = normalizeLocale(locale, config.supportedLocales);
|
|
17
|
+
if (!normalized)
|
|
12
18
|
continue;
|
|
13
19
|
return {
|
|
14
|
-
locale,
|
|
20
|
+
locale: normalized,
|
|
15
21
|
localeSource: source,
|
|
16
22
|
};
|
|
17
23
|
}
|
|
18
24
|
// Fallback: detected is always available
|
|
19
25
|
return {
|
|
20
|
-
locale: context.detected.locale,
|
|
26
|
+
locale: normalizeLocale(context.detected.locale, config.supportedLocales) ||
|
|
27
|
+
config.defaultLocale,
|
|
21
28
|
localeSource: "detected",
|
|
22
29
|
};
|
|
23
30
|
}
|
|
@@ -1,29 +1,20 @@
|
|
|
1
|
-
import '../../core/error/intor-error.js';
|
|
2
|
-
import { normalizeLocale } from '../../core/utils/normalizers/normalize-locale.js';
|
|
3
|
-
import 'logry';
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - Parses language priorities (`q` values) from the header.
|
|
9
|
-
* - Selects the highest-priority language supported by the application.
|
|
10
|
-
* - Normalizes the matched locale against `supportedLocales`.
|
|
2
|
+
* Get locale candidate from the `Accept-Language` header.
|
|
11
3
|
*
|
|
12
|
-
*
|
|
4
|
+
* Parses language priorities and returns the highest-priority
|
|
5
|
+
* language present in `supportedLocales`, without normalization.
|
|
13
6
|
*
|
|
14
7
|
* @example
|
|
15
8
|
* ```ts
|
|
16
9
|
* getLocaleFromAcceptLanguage("en-US,en;q=0.8,zh-TW;q=0.9", ["en-US", "zh-TW"])
|
|
17
10
|
* // => "en-US"
|
|
11
|
+
*
|
|
18
12
|
* getLocaleFromAcceptLanguage("fr,ja;q=0.9", ["en", "zh-TW"])
|
|
19
13
|
* // => undefined
|
|
20
14
|
* ```
|
|
21
15
|
*/
|
|
22
|
-
const getLocaleFromAcceptLanguage = (
|
|
23
|
-
|
|
24
|
-
if (!acceptLanguageHeader ||
|
|
25
|
-
!supportedLocales ||
|
|
26
|
-
supportedLocales.length === 0) {
|
|
16
|
+
const getLocaleFromAcceptLanguage = (acceptLanguageHeader, supportedLocales) => {
|
|
17
|
+
if (!acceptLanguageHeader || supportedLocales.length === 0) {
|
|
27
18
|
return;
|
|
28
19
|
}
|
|
29
20
|
const supportedLocalesSet = new Set(supportedLocales);
|
|
@@ -41,7 +32,7 @@ const getLocaleFromAcceptLanguage = (config, acceptLanguageHeader) => {
|
|
|
41
32
|
const sortedByPriority = parsedLanguages.toSorted((a, b) => b.q - a.q);
|
|
42
33
|
// 3. Pick the first language explicitly supported
|
|
43
34
|
const preferred = sortedByPriority.find(({ lang }) => supportedLocalesSet.has(lang))?.lang;
|
|
44
|
-
return
|
|
35
|
+
return preferred;
|
|
45
36
|
};
|
|
46
37
|
|
|
47
38
|
export { getLocaleFromAcceptLanguage };
|
|
@@ -1,33 +1,32 @@
|
|
|
1
|
-
import '../../core/error/intor-error.js';
|
|
2
|
-
import { normalizeLocale } from '../../core/utils/normalizers/normalize-locale.js';
|
|
3
|
-
import 'logry';
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
2
|
+
* Get locale candidate from hostname.
|
|
7
3
|
*
|
|
8
|
-
*
|
|
4
|
+
* Returns the left-most hostname label, without validation or normalization.
|
|
9
5
|
*
|
|
10
6
|
* @example
|
|
11
7
|
* ```ts
|
|
12
|
-
* getLocaleFromHost(
|
|
8
|
+
* getLocaleFromHost("en.example.com")
|
|
13
9
|
* // => "en"
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* getLocaleFromHost("example.com")
|
|
12
|
+
* // => "example"
|
|
13
|
+
*
|
|
14
|
+
* getLocaleFromHost("api.jp.example.com")
|
|
15
|
+
* // => "api"
|
|
16
|
+
*
|
|
17
|
+
* getLocaleFromHost("localhost")
|
|
17
18
|
* // => undefined
|
|
18
19
|
* ```
|
|
19
20
|
*/
|
|
20
|
-
function getLocaleFromHost(
|
|
21
|
+
function getLocaleFromHost(host) {
|
|
21
22
|
if (!host)
|
|
22
23
|
return;
|
|
23
|
-
const { supportedLocales } = config;
|
|
24
24
|
// Remove port (e.g. localhost:3000)
|
|
25
25
|
const hostname = host.split(":")[0];
|
|
26
26
|
const parts = hostname.split(".");
|
|
27
27
|
if (parts.length < 2)
|
|
28
28
|
return;
|
|
29
|
-
|
|
30
|
-
return normalizeLocale(candidate, supportedLocales);
|
|
29
|
+
return parts[0];
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
export { getLocaleFromHost };
|
|
@@ -3,19 +3,10 @@ import { normalizePathname } from '../../core/utils/normalizers/normalize-pathna
|
|
|
3
3
|
import 'logry';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
6
|
+
* Get locale from pathname.
|
|
7
7
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
* - Inspects the first path segment to determine whether
|
|
11
|
-
* it matches a supported locale.
|
|
12
|
-
*
|
|
13
|
-
* If no locale segment is found, `undefined` is returned.
|
|
14
|
-
*
|
|
15
|
-
* Note:
|
|
16
|
-
* - The pathname is treated as a canonical source.
|
|
17
|
-
* - Only exact matches against `supportedLocales` are accepted.
|
|
18
|
-
* - ___Locale normalization is intentionally not applied here.___
|
|
8
|
+
* Extracts the first pathname segment (after basePath) as a locale
|
|
9
|
+
* if it exactly matches one of the supported locales.
|
|
19
10
|
*
|
|
20
11
|
* @example
|
|
21
12
|
* ```ts
|
|
@@ -31,7 +22,7 @@ import 'logry';
|
|
|
31
22
|
* // => "en"
|
|
32
23
|
* ```
|
|
33
24
|
*/
|
|
34
|
-
function getLocaleFromPathname(
|
|
25
|
+
function getLocaleFromPathname(pathname, config) {
|
|
35
26
|
const { routing, supportedLocales } = config;
|
|
36
27
|
const { basePath } = routing;
|
|
37
28
|
// 1. Normalize pathname
|
|
@@ -1,35 +1,29 @@
|
|
|
1
|
-
import '../../core/error/intor-error.js';
|
|
2
|
-
import { normalizeLocale } from '../../core/utils/normalizers/normalize-locale.js';
|
|
3
|
-
import 'logry';
|
|
4
|
-
|
|
5
1
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* - Reads the configured locale query key.
|
|
9
|
-
* - Normalizes the value against supported locales.
|
|
2
|
+
* Get locale candidate from URL query parameters.
|
|
10
3
|
*
|
|
11
|
-
*
|
|
4
|
+
* Extracts the value of the configured query key, without
|
|
5
|
+
* validation or normalization.
|
|
12
6
|
*
|
|
13
7
|
* @example
|
|
14
8
|
* ```ts
|
|
15
|
-
* getLocaleFromQuery(
|
|
9
|
+
* getLocaleFromQuery({ locale: "en" }, "locale")
|
|
16
10
|
* // => "en"
|
|
17
|
-
*
|
|
11
|
+
*
|
|
12
|
+
* getLocaleFromQuery({}, "locale")
|
|
18
13
|
* // => undefined
|
|
19
|
-
*
|
|
14
|
+
*
|
|
15
|
+
* getLocaleFromQuery({ locale: ["zh-TW"] }, "locale")
|
|
20
16
|
* // => "zh-TW"
|
|
21
17
|
* ```
|
|
22
18
|
*/
|
|
23
|
-
function getLocaleFromQuery(
|
|
19
|
+
function getLocaleFromQuery(query, queryKey) {
|
|
24
20
|
if (!query)
|
|
25
21
|
return;
|
|
26
|
-
const { supportedLocales, routing } = config;
|
|
27
|
-
const { queryKey } = routing.inbound;
|
|
28
22
|
const raw = query[queryKey];
|
|
29
23
|
if (!raw)
|
|
30
24
|
return;
|
|
31
25
|
const value = Array.isArray(raw) ? raw[0] : raw;
|
|
32
|
-
return
|
|
26
|
+
return value;
|
|
33
27
|
}
|
|
34
28
|
|
|
35
29
|
export { getLocaleFromQuery };
|