intor 2.3.35 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (200) hide show
  1. package/README.md +16 -3
  2. package/dist/core/export/edge/index.js +6 -0
  3. package/dist/core/src/{server → core}/translator/create-translator.js +1 -1
  4. package/dist/core/src/core/utils/normalizers/normalize-locale.js +59 -0
  5. package/dist/core/src/core/utils/normalizers/normalize-query.js +25 -0
  6. package/dist/core/src/edge/helpers/get-translator.js +29 -0
  7. package/dist/core/src/edge/translator/init-translator.js +35 -0
  8. package/dist/core/src/routing/inbound/helpers/resolve-inbound-from-request.js +29 -0
  9. package/dist/core/src/routing/inbound/resolve-inbound.js +45 -0
  10. package/dist/core/src/routing/inbound/resolve-locale/resolve-locale.js +35 -0
  11. package/dist/{next/src/routing/inbound/resolve-pathname/resolve-pathname.js → core/src/routing/inbound/resolve-path/resolve-path.js} +2 -2
  12. package/dist/core/src/routing/locale/get-locale-from-accept-language.js +38 -0
  13. package/dist/core/src/routing/locale/get-locale-from-host.js +32 -0
  14. package/dist/core/src/routing/locale/get-locale-from-pathname.js +47 -0
  15. package/dist/core/src/routing/locale/get-locale-from-query.js +21 -0
  16. package/dist/core/src/server/helpers/get-translator.js +4 -5
  17. package/dist/core/src/server/translator/init-translator.js +1 -1
  18. package/dist/express/src/adapters/express/create-intor-handler.js +9 -9
  19. package/dist/express/src/adapters/express/get-translator.js +7 -1
  20. package/dist/express/src/{server → core}/translator/create-translator.js +1 -1
  21. package/dist/express/src/core/utils/parse-cookie-header.js +22 -0
  22. package/dist/express/src/routing/inbound/resolve-inbound.js +2 -2
  23. package/dist/express/src/routing/inbound/{resolve-pathname/resolve-pathname.js → resolve-path/resolve-path.js} +2 -2
  24. package/dist/express/src/server/helpers/get-translator.js +4 -5
  25. package/dist/express/src/server/translator/init-translator.js +1 -1
  26. package/dist/fastify/export/fastify/index.js +3 -0
  27. package/dist/fastify/src/adapters/fastify/create-intor-handler.js +56 -0
  28. package/dist/fastify/src/adapters/fastify/get-translator.js +28 -0
  29. package/dist/fastify/src/adapters/fastify/intor-fastify-plugin.js +34 -0
  30. package/dist/fastify/src/core/constants/prefix-placeholder.js +4 -0
  31. package/dist/fastify/src/core/error/intor-error.js +9 -0
  32. package/dist/fastify/src/core/logger/get-logger.js +39 -0
  33. package/dist/fastify/src/core/logger/global-logger-pool.js +8 -0
  34. package/dist/fastify/src/core/messages/load-remote-messages/collect-remote-resources.js +25 -0
  35. package/dist/fastify/src/core/messages/load-remote-messages/fetch-remote-resource.js +47 -0
  36. package/dist/fastify/src/core/messages/load-remote-messages/load-remote-messages.js +93 -0
  37. package/dist/fastify/src/core/messages/load-remote-messages/resolve-remote-resources.js +24 -0
  38. package/dist/fastify/src/core/messages/merge-messages.js +33 -0
  39. package/dist/fastify/src/core/messages/utils/is-valid-messages.js +44 -0
  40. package/dist/fastify/src/core/messages/utils/nest-object-from-path.js +21 -0
  41. package/dist/fastify/src/core/render/create-html-renderer.js +44 -0
  42. package/dist/fastify/src/core/render/utils/escape-html.js +10 -0
  43. package/dist/fastify/src/core/render/utils/render-attributes.js +17 -0
  44. package/dist/fastify/src/core/translator/create-t-rich.js +22 -0
  45. package/dist/{svelte-kit/src/server → fastify/src/core}/translator/create-translator.js +1 -1
  46. package/dist/fastify/src/core/utils/deep-merge.js +47 -0
  47. package/dist/fastify/src/core/utils/normalizers/normalize-cache-key.js +45 -0
  48. package/dist/fastify/src/core/utils/normalizers/normalize-locale.js +59 -0
  49. package/dist/fastify/src/core/utils/normalizers/normalize-pathname.js +43 -0
  50. package/dist/fastify/src/core/utils/normalizers/normalize-query.js +25 -0
  51. package/dist/fastify/src/core/utils/parse-cookie-header.js +22 -0
  52. package/dist/fastify/src/core/utils/resolve-loader-options.js +27 -0
  53. package/dist/fastify/src/routing/inbound/resolve-inbound.js +45 -0
  54. package/dist/fastify/src/routing/inbound/resolve-locale/resolve-locale.js +35 -0
  55. package/dist/{svelte-kit/src/routing/inbound/resolve-pathname/resolve-pathname.js → fastify/src/routing/inbound/resolve-path/resolve-path.js} +2 -2
  56. package/dist/fastify/src/routing/inbound/resolve-path/strategies/all.js +28 -0
  57. package/dist/fastify/src/routing/inbound/resolve-path/strategies/except-default.js +29 -0
  58. package/dist/fastify/src/routing/locale/get-locale-from-accept-language.js +38 -0
  59. package/dist/fastify/src/routing/locale/get-locale-from-host.js +32 -0
  60. package/dist/fastify/src/routing/locale/get-locale-from-pathname.js +47 -0
  61. package/dist/fastify/src/routing/locale/get-locale-from-query.js +21 -0
  62. package/dist/fastify/src/routing/pathname/canonicalize-pathname.js +47 -0
  63. package/dist/fastify/src/routing/pathname/localize-pathname.js +36 -0
  64. package/dist/fastify/src/routing/pathname/materialize-pathname.js +42 -0
  65. package/dist/fastify/src/routing/pathname/standardize-pathname.js +34 -0
  66. package/dist/fastify/src/server/helpers/get-translator.js +31 -0
  67. package/dist/fastify/src/server/messages/load-local-messages/cache/messages-pool.js +11 -0
  68. package/dist/fastify/src/server/messages/load-local-messages/load-local-messages.js +108 -0
  69. package/dist/fastify/src/server/messages/load-local-messages/read-locale-messages/collect-file-entries/collect-file-entries.js +91 -0
  70. package/dist/fastify/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/parse-file-entries.js +103 -0
  71. package/dist/fastify/src/server/messages/load-local-messages/read-locale-messages/parse-file-entries/utils/json-reader.js +12 -0
  72. package/dist/fastify/src/server/messages/load-local-messages/read-locale-messages/read-locale-messages.js +42 -0
  73. package/dist/fastify/src/server/messages/load-messages.js +78 -0
  74. package/dist/fastify/src/server/translator/init-translator.js +36 -0
  75. package/dist/hono/export/hono/index.js +3 -0
  76. package/dist/hono/src/adapters/hono/create-intor-handler.js +53 -0
  77. package/dist/hono/src/adapters/hono/get-translator.js +20 -0
  78. package/dist/hono/src/core/constants/prefix-placeholder.js +4 -0
  79. package/dist/hono/src/core/error/intor-error.js +9 -0
  80. package/dist/hono/src/core/logger/get-logger.js +39 -0
  81. package/dist/hono/src/core/logger/global-logger-pool.js +8 -0
  82. package/dist/hono/src/core/messages/load-remote-messages/collect-remote-resources.js +25 -0
  83. package/dist/hono/src/core/messages/load-remote-messages/fetch-remote-resource.js +47 -0
  84. package/dist/hono/src/core/messages/load-remote-messages/load-remote-messages.js +93 -0
  85. package/dist/hono/src/core/messages/load-remote-messages/resolve-remote-resources.js +24 -0
  86. package/dist/hono/src/core/messages/merge-messages.js +33 -0
  87. package/dist/hono/src/core/messages/utils/is-valid-messages.js +44 -0
  88. package/dist/hono/src/core/messages/utils/nest-object-from-path.js +21 -0
  89. package/dist/hono/src/core/render/create-html-renderer.js +44 -0
  90. package/dist/hono/src/core/render/utils/escape-html.js +10 -0
  91. package/dist/hono/src/core/render/utils/render-attributes.js +17 -0
  92. package/dist/hono/src/core/translator/create-t-rich.js +22 -0
  93. package/dist/{next/src/server → hono/src/core}/translator/create-translator.js +1 -1
  94. package/dist/hono/src/core/utils/deep-merge.js +47 -0
  95. package/dist/hono/src/core/utils/normalizers/normalize-locale.js +59 -0
  96. package/dist/hono/src/core/utils/normalizers/normalize-pathname.js +43 -0
  97. package/dist/hono/src/core/utils/normalizers/normalize-query.js +25 -0
  98. package/dist/hono/src/core/utils/parse-cookie-header.js +22 -0
  99. package/dist/hono/src/edge/helpers/get-translator.js +29 -0
  100. package/dist/hono/src/edge/translator/init-translator.js +35 -0
  101. package/dist/hono/src/routing/inbound/resolve-inbound.js +45 -0
  102. package/dist/hono/src/routing/inbound/resolve-locale/resolve-locale.js +35 -0
  103. package/dist/hono/src/routing/inbound/resolve-path/resolve-path.js +42 -0
  104. package/dist/hono/src/routing/inbound/resolve-path/strategies/all.js +28 -0
  105. package/dist/hono/src/routing/inbound/resolve-path/strategies/except-default.js +29 -0
  106. package/dist/hono/src/routing/inbound/resolve-path/strategies/none.js +8 -0
  107. package/dist/hono/src/routing/locale/get-locale-from-accept-language.js +38 -0
  108. package/dist/hono/src/routing/locale/get-locale-from-host.js +32 -0
  109. package/dist/hono/src/routing/locale/get-locale-from-pathname.js +47 -0
  110. package/dist/hono/src/routing/locale/get-locale-from-query.js +21 -0
  111. package/dist/hono/src/routing/pathname/canonicalize-pathname.js +47 -0
  112. package/dist/hono/src/routing/pathname/localize-pathname.js +36 -0
  113. package/dist/hono/src/routing/pathname/materialize-pathname.js +42 -0
  114. package/dist/hono/src/routing/pathname/standardize-pathname.js +34 -0
  115. package/dist/next/src/adapters/next/server/get-translator.js +7 -1
  116. package/dist/next/src/core/translator/create-translator.js +30 -0
  117. package/dist/next/src/routing/inbound/resolve-inbound.js +2 -2
  118. package/dist/next/src/routing/inbound/resolve-path/resolve-path.js +42 -0
  119. package/dist/next/src/routing/inbound/resolve-path/strategies/none.js +8 -0
  120. package/dist/next/src/server/helpers/get-translator.js +4 -5
  121. package/dist/next/src/server/translator/init-translator.js +1 -1
  122. package/dist/react/src/client/react/translator/use-translator.js +5 -5
  123. package/dist/react/src/client/shared/helpers/get-client-locale.js +1 -7
  124. package/dist/svelte/src/client/shared/helpers/get-client-locale.js +1 -7
  125. package/dist/svelte/src/client/svelte/translator/use-translator.js +6 -6
  126. package/dist/svelte-kit/src/adapters/svelte-kit/server/create-intor-handler.js +1 -3
  127. package/dist/svelte-kit/src/core/translator/create-translator.js +30 -0
  128. package/dist/svelte-kit/src/routing/inbound/resolve-inbound.js +2 -2
  129. package/dist/svelte-kit/src/routing/inbound/resolve-path/resolve-path.js +42 -0
  130. package/dist/svelte-kit/src/routing/inbound/resolve-path/strategies/all.js +28 -0
  131. package/dist/svelte-kit/src/routing/inbound/resolve-path/strategies/except-default.js +29 -0
  132. package/dist/svelte-kit/src/routing/inbound/resolve-path/strategies/none.js +8 -0
  133. package/dist/svelte-kit/src/server/translator/init-translator.js +1 -1
  134. package/dist/types/export/edge/index.d.ts +2 -0
  135. package/dist/types/export/fastify/index.d.ts +1 -0
  136. package/dist/types/export/hono/index.d.ts +1 -0
  137. package/dist/types/src/adapters/express/create-intor-handler.d.ts +4 -4
  138. package/dist/types/src/adapters/express/get-translator.d.ts +5 -6
  139. package/dist/types/src/adapters/express/global.d.ts +6 -6
  140. package/dist/types/src/adapters/fastify/create-intor-handler.d.ts +14 -0
  141. package/dist/types/src/adapters/fastify/get-translator.d.ts +17 -0
  142. package/dist/types/src/adapters/fastify/global.d.ts +17 -0
  143. package/dist/types/src/adapters/fastify/index.d.ts +3 -0
  144. package/dist/types/src/adapters/fastify/intor-fastify-plugin.d.ts +21 -0
  145. package/dist/types/src/adapters/hono/create-intor-handler.d.ts +13 -0
  146. package/dist/types/src/adapters/hono/get-translator.d.ts +17 -0
  147. package/dist/types/src/adapters/hono/global.d.ts +17 -0
  148. package/dist/types/src/adapters/hono/index.d.ts +3 -0
  149. package/dist/types/src/adapters/next/server/get-translator.d.ts +5 -6
  150. package/dist/types/src/adapters/svelte-kit/server/create-intor-handler.d.ts +0 -2
  151. package/dist/types/src/client/react/translator/translator-instance.d.ts +1 -5
  152. package/dist/types/src/client/react/translator/use-translator.d.ts +4 -3
  153. package/dist/types/src/client/shared/helpers/get-client-locale.d.ts +3 -8
  154. package/dist/types/src/client/svelte/translator/translator-instance.d.ts +7 -7
  155. package/dist/types/src/client/svelte/translator/use-translator.d.ts +5 -4
  156. package/dist/types/src/client/vue/translator/translator-instance.d.ts +2 -2
  157. package/dist/types/src/client/vue/translator/use-translator.d.ts +5 -4
  158. package/dist/types/src/core/index.d.ts +2 -2
  159. package/dist/types/src/core/translator/index.d.ts +1 -0
  160. package/dist/types/src/core/types/translator-instance.d.ts +9 -2
  161. package/dist/types/src/core/utils/index.d.ts +1 -0
  162. package/dist/types/src/edge/helpers/get-translator.d.ts +15 -0
  163. package/dist/types/src/edge/helpers/index.d.ts +1 -0
  164. package/dist/types/src/edge/index.d.ts +1 -0
  165. package/dist/types/src/edge/translator/index.d.ts +1 -0
  166. package/dist/types/src/edge/translator/init-translator.d.ts +14 -0
  167. package/dist/types/src/routing/inbound/helpers/index.d.ts +1 -0
  168. package/dist/types/src/routing/inbound/helpers/resolve-inbound-from-request.d.ts +3 -0
  169. package/dist/types/src/routing/inbound/index.d.ts +1 -0
  170. package/dist/types/src/routing/inbound/resolve-path/index.d.ts +1 -0
  171. package/dist/types/src/routing/inbound/{resolve-pathname/resolve-pathname.d.ts → resolve-path/resolve-path.d.ts} +2 -2
  172. package/dist/types/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/all.d.ts +2 -2
  173. package/dist/types/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/except-default.d.ts +2 -2
  174. package/dist/types/src/routing/inbound/resolve-path/strategies/none.d.ts +5 -0
  175. package/dist/types/src/routing/inbound/{resolve-pathname → resolve-path}/types.d.ts +5 -5
  176. package/dist/types/src/routing/index.d.ts +1 -1
  177. package/dist/types/src/server/helpers/get-translator.d.ts +6 -8
  178. package/dist/types/src/server/index.d.ts +0 -2
  179. package/dist/types/src/server/translator/index.d.ts +1 -2
  180. package/dist/types/src/server/translator/init-translator.d.ts +3 -2
  181. package/dist/vue/src/client/shared/helpers/get-client-locale.js +1 -7
  182. package/dist/vue/src/client/vue/translator/use-translator.js +5 -5
  183. package/package.json +18 -1
  184. package/dist/types/src/routing/inbound/resolve-pathname/index.d.ts +0 -1
  185. package/dist/types/src/routing/inbound/resolve-pathname/strategies/none.d.ts +0 -5
  186. package/dist/types/src/server/shared/utils/index.d.ts +0 -1
  187. package/dist/types/src/server/translator/translator-instance.d.ts +0 -10
  188. /package/dist/{express/src/server/shared → core/src/core}/utils/parse-cookie-header.js +0 -0
  189. /package/dist/{express/src/routing/inbound/resolve-pathname → core/src/routing/inbound/resolve-path}/strategies/all.js +0 -0
  190. /package/dist/{express/src/routing/inbound/resolve-pathname → core/src/routing/inbound/resolve-path}/strategies/except-default.js +0 -0
  191. /package/dist/{express/src/routing/inbound/resolve-pathname → core/src/routing/inbound/resolve-path}/strategies/none.js +0 -0
  192. /package/dist/{svelte-kit/src/routing/inbound/resolve-pathname → express/src/routing/inbound/resolve-path}/strategies/all.js +0 -0
  193. /package/dist/{svelte-kit/src/routing/inbound/resolve-pathname → express/src/routing/inbound/resolve-path}/strategies/except-default.js +0 -0
  194. /package/dist/{next/src/routing/inbound/resolve-pathname → express/src/routing/inbound/resolve-path}/strategies/none.js +0 -0
  195. /package/dist/{svelte-kit/src/routing/inbound/resolve-pathname → fastify/src/routing/inbound/resolve-path}/strategies/none.js +0 -0
  196. /package/dist/next/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/all.js +0 -0
  197. /package/dist/next/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/except-default.js +0 -0
  198. /package/dist/types/src/{server → core}/translator/create-translator.d.ts +0 -0
  199. /package/dist/types/src/{server/shared → core}/utils/parse-cookie-header.d.ts +0 -0
  200. /package/dist/types/src/routing/inbound/{resolve-pathname → resolve-path}/strategies/index.d.ts +0 -0
@@ -0,0 +1,34 @@
1
+ import { PREFIX_PLACEHOLDER } from '../../core/constants/prefix-placeholder.js';
2
+ import '../../core/error/intor-error.js';
3
+ import { normalizePathname } from '../../core/utils/normalizers/normalize-pathname.js';
4
+ import 'logry';
5
+ import 'p-limit';
6
+ import 'intor-translator';
7
+
8
+ /**
9
+ * Standardizes a canonical pathname into an internal routing template
10
+ * by applying the base path and injecting a locale placeholder.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * // config.routing.basePath: "/app",
15
+ * // config.routing.prefix: "all"
16
+ * standardizePathname("/about", config);
17
+ * // => "/app/{locale}/about"
18
+ * ```
19
+ */
20
+ const standardizePathname = (canonicalizedPathname, config) => {
21
+ const { basePath } = config.routing;
22
+ // Normalize each segment before join to avoid redundant slashes
23
+ const parts = [
24
+ normalizePathname(basePath),
25
+ PREFIX_PLACEHOLDER,
26
+ normalizePathname(canonicalizedPathname),
27
+ ];
28
+ // Avoid double slashes between segments
29
+ const standardizedPathname = parts.join("/").replaceAll(/\/{2,}/g, "/");
30
+ // Final normalization to ensure leading slash, no trailing
31
+ return normalizePathname(standardizedPathname);
32
+ };
33
+
34
+ export { standardizePathname };
@@ -0,0 +1,31 @@
1
+ import '../../core/error/intor-error.js';
2
+ import 'logry';
3
+ import 'p-limit';
4
+ import 'intor-translator';
5
+ import { createTRich } from '../../core/translator/create-t-rich.js';
6
+ import { initTranslator } from '../translator/init-translator.js';
7
+
8
+ /**
9
+ * Get a server-side translator for the current execution context.
10
+ */
11
+ async function getTranslator(config, params) {
12
+ const { locale, readers, allowCacheWrite, fetch, preKey, handlers, plugins } = params;
13
+ // Initialize a locale-bound translator snapshot with messages loaded
14
+ const translator = await initTranslator(config, locale, {
15
+ readers,
16
+ allowCacheWrite,
17
+ fetch: fetch || globalThis.fetch,
18
+ plugins,
19
+ handlers,
20
+ });
21
+ const scoped = translator.scoped(preKey);
22
+ return {
23
+ messages: translator.messages,
24
+ locale: translator.locale,
25
+ hasKey: scoped.hasKey,
26
+ t: scoped.t,
27
+ tRich: createTRich(scoped.t),
28
+ };
29
+ }
30
+
31
+ export { getTranslator };
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Get the messages pool.
3
+ */
4
+ function getMessagesPool() {
5
+ if (!globalThis.__INTOR_MESSAGES_POOL__) {
6
+ globalThis.__INTOR_MESSAGES_POOL__ = new Map();
7
+ }
8
+ return globalThis.__INTOR_MESSAGES_POOL__;
9
+ }
10
+
11
+ export { getMessagesPool };
@@ -0,0 +1,108 @@
1
+ import path from 'node:path';
2
+ import pLimit from 'p-limit';
3
+ import '../../../core/error/intor-error.js';
4
+ import { normalizeCacheKey } from '../../../core/utils/normalizers/normalize-cache-key.js';
5
+ import { getLogger } from '../../../core/logger/get-logger.js';
6
+ import 'intor-translator';
7
+ import { getMessagesPool } from './cache/messages-pool.js';
8
+ import { readLocaleMessages } from './read-locale-messages/read-locale-messages.js';
9
+
10
+ /**
11
+ * Load locale messages from the local file system.
12
+ *
13
+ * This function serves as the orchestration layer for local message loading.
14
+ * It coordinates:
15
+ *
16
+ * - Locale resolution with fallbacks
17
+ * - Concurrency control for file system access
18
+ * - Process-level memoization (read by default, write by ownership)
19
+ *
20
+ * Local messages are cached for the lifetime of the process.
21
+ * Cache writes are restricted to the primary initialization flow.
22
+ *
23
+ * File traversal, parsing, and validation are delegated to lower-level utilities.
24
+ */
25
+ const loadLocalMessages = async ({ id, locale, fallbackLocales, namespaces, rootDir = "messages", concurrency = 10, readers, pool = getMessagesPool(), allowCacheWrite = false, loggerOptions, }) => {
26
+ const baseLogger = getLogger(loggerOptions);
27
+ const logger = baseLogger.child({ scope: "load-local-messages" });
28
+ const start = performance.now();
29
+ logger.debug("Loading local messages.", {
30
+ rootDir,
31
+ resolvedRootDir: path.resolve(rootDir),
32
+ });
33
+ // ---------------------------------------------------------------------------
34
+ // Cache key resolution
35
+ // ---------------------------------------------------------------------------
36
+ const cacheKey = normalizeCacheKey([
37
+ id,
38
+ "loaderType:local",
39
+ rootDir,
40
+ locale,
41
+ fallbackLocales?.toSorted().join(","),
42
+ namespaces?.toSorted().join(","),
43
+ ].filter(Boolean));
44
+ // ---------------------------------------------------------------------------
45
+ // Cache read
46
+ // ---------------------------------------------------------------------------
47
+ if (cacheKey) {
48
+ const cached = pool.get(cacheKey);
49
+ if (cached) {
50
+ logger.debug("Messages cache hit.", { key: cacheKey });
51
+ return cached;
52
+ }
53
+ }
54
+ // ---------------------------------------------------------------------------
55
+ // Resolve locale messages with ordered fallback strategy
56
+ // ---------------------------------------------------------------------------
57
+ const limit = pLimit(concurrency);
58
+ const candidateLocales = [locale, ...(fallbackLocales || [])];
59
+ let messages;
60
+ for (let i = 0; i < candidateLocales.length; i++) {
61
+ const candidateLocale = candidateLocales[i];
62
+ const isLast = i === candidateLocales.length - 1;
63
+ try {
64
+ const result = await readLocaleMessages({
65
+ locale: candidateLocale,
66
+ namespaces,
67
+ rootDir,
68
+ limit,
69
+ readers,
70
+ loggerOptions,
71
+ });
72
+ // Stop at the first locale that yields non-empty messages
73
+ if (Object.values(result[candidateLocale] || {}).length > 0) {
74
+ messages = result;
75
+ break;
76
+ }
77
+ }
78
+ catch {
79
+ if (isLast) {
80
+ logger.warn("Failed to load messages for all candidate locales.", {
81
+ locale,
82
+ fallbackLocales,
83
+ });
84
+ }
85
+ else {
86
+ logger.warn(`Failed to load locale messages for "${candidateLocale}", trying next fallback.`);
87
+ }
88
+ }
89
+ }
90
+ // ---------------------------------------------------------------------------
91
+ // Cache write (explicitly permitted)
92
+ // ---------------------------------------------------------------------------
93
+ const isProd = process.env.NODE_ENV === "production";
94
+ const canWriteCache = isProd && allowCacheWrite;
95
+ if (canWriteCache && cacheKey && messages) {
96
+ pool.set(cacheKey, messages);
97
+ }
98
+ // Final success log with resolved locale and timing
99
+ if (messages) {
100
+ logger.trace("Finished loading local messages.", {
101
+ loadedLocale: Object.keys(messages)[0],
102
+ duration: `${Math.round(performance.now() - start)} ms`,
103
+ });
104
+ }
105
+ return messages;
106
+ };
107
+
108
+ export { loadLocalMessages };
@@ -0,0 +1,91 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import '../../../../../core/error/intor-error.js';
4
+ import { getLogger } from '../../../../../core/logger/get-logger.js';
5
+ import 'p-limit';
6
+ import 'intor-translator';
7
+
8
+ /**
9
+ * Recursively collects message file metadata under a given locale root.
10
+ *
11
+ * - Traverses directories and collects matching message files
12
+ * - Supports filtering by file extensions and optional namespaces
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * [{
17
+ * namespace: "auth", // If messages under locale root (no namespace) -> "index"
18
+ * fullPath: "/Users/john/my-app/messages/en-US/auth/login.json",
19
+ * relativePath: "auth/login.json",
20
+ * segments: ["auth", "login"],
21
+ * basename: "login",
22
+ * }, ... ];
23
+ * ```
24
+ */
25
+ async function collectFileEntries({ readdir = fs.readdir, namespaces, rootDir, limit, exts = [], loggerOptions, }) {
26
+ const baseLogger = getLogger(loggerOptions);
27
+ const logger = baseLogger.child({ scope: "collect-file-entries" });
28
+ const supportedExts = new Set(["json", ...exts]);
29
+ const fileEntries = [];
30
+ // Recursive directory walk
31
+ const walk = async (currentDir) => {
32
+ // -------------------------------------------------------------------------
33
+ // Read directory entries
34
+ // -------------------------------------------------------------------------
35
+ let entries = [];
36
+ try {
37
+ entries = await readdir(currentDir, { withFileTypes: true });
38
+ }
39
+ catch {
40
+ logger.debug("Locale directory not found, skipping locale.", {
41
+ localeDir: currentDir,
42
+ });
43
+ return;
44
+ }
45
+ // -------------------------------------------------------------------------
46
+ // 1. Recurse into sub-directories (control flow, no limit)
47
+ // -------------------------------------------------------------------------
48
+ for (const entry of entries) {
49
+ if (!entry.isDirectory())
50
+ continue;
51
+ await walk(path.join(currentDir, entry.name));
52
+ }
53
+ // -------------------------------------------------------------------------
54
+ // 2. Process files (IO-bound, concurrency-limited)
55
+ // -------------------------------------------------------------------------
56
+ const tasks = entries
57
+ .filter((entry) => entry.isFile())
58
+ .map((entry) => limit(async () => {
59
+ const fullPath = path.join(currentDir, entry.name);
60
+ const ext = path.extname(entry.name).slice(1);
61
+ if (!ext || !supportedExts.has(ext))
62
+ return;
63
+ const relativePath = path.relative(rootDir, fullPath);
64
+ const withoutExt = relativePath.slice(0, relativePath.length - (ext.length + 1));
65
+ const segments = withoutExt.split(path.sep).filter(Boolean);
66
+ const namespace = segments.at(0);
67
+ if (!namespace)
68
+ return;
69
+ // Apply namespace filter (always allow "index")
70
+ if (namespaces && namespace !== "index") {
71
+ if (!namespaces.includes(namespace))
72
+ return;
73
+ }
74
+ fileEntries.push({
75
+ namespace,
76
+ fullPath,
77
+ relativePath,
78
+ segments,
79
+ basename: path.basename(entry.name, `.${ext}`),
80
+ });
81
+ }));
82
+ await Promise.all(tasks);
83
+ };
84
+ await walk(rootDir);
85
+ if (fileEntries.length > 0) {
86
+ logger.trace(`Collected ${fileEntries.length} local message files for locale "${path.basename(rootDir)}".`);
87
+ }
88
+ return fileEntries;
89
+ }
90
+
91
+ export { collectFileEntries };
@@ -0,0 +1,103 @@
1
+ import path from 'node:path';
2
+ import '../../../../../core/error/intor-error.js';
3
+ import { deepMerge } from '../../../../../core/utils/deep-merge.js';
4
+ import { getLogger } from '../../../../../core/logger/get-logger.js';
5
+ import 'p-limit';
6
+ import { isValidMessages } from '../../../../../core/messages/utils/is-valid-messages.js';
7
+ import { nestObjectFromPath } from '../../../../../core/messages/utils/nest-object-from-path.js';
8
+ import 'intor-translator';
9
+ import { jsonReader } from './utils/json-reader.js';
10
+
11
+ /**
12
+ * Parse locale message files into a unified Messages object (single-locale).
13
+ *
14
+ * - Reads JSON or custom formats (via `messagesReader`)
15
+ * - Validates message structure
16
+ * - Builds nested objects based on file path segments
17
+ * - Deep-merges entries by namespace
18
+ *
19
+ * @example
20
+ * ```plain
21
+ * File paths:
22
+ * - en/index.json = { a: "A" }
23
+ * - en/ui.json = { b: "B" }
24
+ * - en/auth/index.json = { c: "C" }
25
+ * - en/auth/verify.json = { d: "D" }
26
+ *```
27
+
28
+ * The final return value is a `Messages` object:
29
+ * ```ts
30
+ * {
31
+ * a: "A",
32
+ * ui: { b: "B" },
33
+ * auth: {
34
+ * c: "C",
35
+ * verify: { d: "D" },
36
+ * },
37
+ * }
38
+ * ```
39
+ */
40
+ async function parseFileEntries({ fileEntries, limit, readers, loggerOptions, }) {
41
+ const baseLogger = getLogger(loggerOptions);
42
+ const logger = baseLogger.child({ scope: "parse-file-entries" });
43
+ // Read and parse all file entries
44
+ const parsedFileEntries = [];
45
+ const tasks = fileEntries.map(({ namespace, segments, basename, fullPath }) => limit(async () => {
46
+ try {
47
+ // -------------------------------------------------------------------
48
+ // Read and validate file content
49
+ // -------------------------------------------------------------------
50
+ const ext = path.extname(fullPath).slice(1); // remove dot
51
+ let raw;
52
+ if (ext === "json") {
53
+ raw = await jsonReader(fullPath);
54
+ }
55
+ else {
56
+ const reader = readers?.[ext];
57
+ if (!reader) {
58
+ throw new Error(`No message reader registered for .${ext} files. ` +
59
+ `Please register a reader for the "${ext}" extension.`);
60
+ }
61
+ raw = await reader(fullPath);
62
+ }
63
+ // Validate messages structure
64
+ if (!isValidMessages(raw)) {
65
+ throw new Error("Parsed content does not match expected Messages structure");
66
+ }
67
+ // -------------------------------------------------------------------
68
+ // Build nested message object from path segments
69
+ // -------------------------------------------------------------------
70
+ const segmentsWithoutNamespace = segments.slice(1);
71
+ const isIndexFile = basename === "index";
72
+ const keyPath = isIndexFile
73
+ ? segmentsWithoutNamespace.slice(0, -1)
74
+ : segmentsWithoutNamespace;
75
+ // Nest the parsed content based on the path segments
76
+ const nestedMessages = nestObjectFromPath(keyPath, raw);
77
+ parsedFileEntries.push({ namespace, messages: nestedMessages });
78
+ }
79
+ catch (error) {
80
+ logger.warn("Failed to read or parse file.", {
81
+ path: fullPath,
82
+ error,
83
+ });
84
+ }
85
+ }));
86
+ await Promise.all(tasks);
87
+ // ---------------------------------------------------------------------------
88
+ // Merge parsed entries by namespace
89
+ // ---------------------------------------------------------------------------
90
+ const result = {};
91
+ for (const { namespace, messages } of parsedFileEntries) {
92
+ // Root-level namespace (e.g. [locale]/index.json)
93
+ if (namespace === "index") {
94
+ Object.assign(result, deepMerge(result, messages));
95
+ }
96
+ else {
97
+ result[namespace] = deepMerge(result[namespace], messages);
98
+ }
99
+ }
100
+ return result;
101
+ }
102
+
103
+ export { parseFileEntries };
@@ -0,0 +1,12 @@
1
+ import fs from 'node:fs/promises';
2
+
3
+ /**
4
+ * Read & parse a JSON file
5
+ */
6
+ async function jsonReader(filePath, readFile = fs.readFile) {
7
+ const raw = await readFile(filePath, "utf8");
8
+ const parsed = JSON.parse(raw);
9
+ return parsed;
10
+ }
11
+
12
+ export { jsonReader };
@@ -0,0 +1,42 @@
1
+ import path from 'node:path';
2
+ import { collectFileEntries } from './collect-file-entries/collect-file-entries.js';
3
+ import { parseFileEntries } from './parse-file-entries/parse-file-entries.js';
4
+
5
+ /**
6
+ * Read and assemble messages for a single locale from the file system.
7
+ *
8
+ * This function acts as a thin orchestration layer:
9
+ * - Collects message file metadata for the locale
10
+ * - Parses files into a single Messages object
11
+ * - Wraps the result under the locale key
12
+ *
13
+ * It does not perform validation or transformation itself.
14
+ */
15
+ const readLocaleMessages = async ({ locale, namespaces, rootDir = "messages", limit, readers, loggerOptions, }) => {
16
+ // ---------------------------------------------------------------------------
17
+ // Collect message file entries for the locale
18
+ // ---------------------------------------------------------------------------
19
+ const fileEntries = await collectFileEntries({
20
+ namespaces,
21
+ rootDir: path.resolve(rootDir, locale),
22
+ limit,
23
+ exts: Object.keys(readers || {}),
24
+ loggerOptions,
25
+ });
26
+ // ---------------------------------------------------------------------------
27
+ // Parse collected files into a Messages object (single-locale)
28
+ // ---------------------------------------------------------------------------
29
+ const messages = await parseFileEntries({
30
+ fileEntries,
31
+ limit,
32
+ readers,
33
+ loggerOptions,
34
+ });
35
+ // ---------------------------------------------------------------------------
36
+ // Wrap parsed messages under the locale key
37
+ // ---------------------------------------------------------------------------
38
+ const localeMessages = { [locale]: messages };
39
+ return localeMessages;
40
+ };
41
+
42
+ export { readLocaleMessages };
@@ -0,0 +1,78 @@
1
+ import '../../core/error/intor-error.js';
2
+ import { resolveLoaderOptions } from '../../core/utils/resolve-loader-options.js';
3
+ import { getLogger } from '../../core/logger/get-logger.js';
4
+ import { loadRemoteMessages } from '../../core/messages/load-remote-messages/load-remote-messages.js';
5
+ import 'intor-translator';
6
+ import { loadLocalMessages } from './load-local-messages/load-local-messages.js';
7
+
8
+ /**
9
+ * Load locale messages according to the resolved Intor loader configuration.
10
+ *
11
+ * This function is the top-level orchestration entry for message loading.
12
+ * It is responsible for:
13
+ *
14
+ * - Resolving the active loader strategy (local or remote)
15
+ * - Dispatching to the appropriate loader implementation
16
+ * - Passing through cache and read-related options
17
+ *
18
+ * Message traversal, parsing, fallback resolution, and caching logic
19
+ * are delegated to the selected loader.
20
+ */
21
+ const loadMessages = async ({ config, locale, readers, allowCacheWrite = false, fetch, }) => {
22
+ const baseLogger = getLogger(config.logger);
23
+ const logger = baseLogger.child({ scope: "load-messages" });
24
+ // ---------------------------------------------------------------------------
25
+ // Resolve loader configuration
26
+ // ---------------------------------------------------------------------------
27
+ const loader = resolveLoaderOptions(config);
28
+ if (!loader) {
29
+ logger.warn("No loader options have been configured in the current config.");
30
+ return;
31
+ }
32
+ const { mode, namespaces, concurrency } = loader;
33
+ const fallbackLocales = config.fallbackLocales[locale] || [];
34
+ logger.info(`Loading messages for locale "${locale}".`);
35
+ logger.trace("Starting to load messages with runtime context.", {
36
+ loaderMode: mode,
37
+ ...(mode === "local" ? { rootDir: loader.rootDir } : {}),
38
+ locale,
39
+ fallbackLocales: fallbackLocales.join(", "),
40
+ namespaces: namespaces && namespaces.length > 0 ? [...namespaces] : "*",
41
+ });
42
+ // ---------------------------------------------------------------------------
43
+ // Dispatch to loader implementation
44
+ // ---------------------------------------------------------------------------
45
+ let loadedMessages;
46
+ if (mode === "local") {
47
+ loadedMessages = await loadLocalMessages({
48
+ id: config.id,
49
+ locale,
50
+ fallbackLocales,
51
+ namespaces,
52
+ rootDir: loader.rootDir,
53
+ concurrency,
54
+ readers,
55
+ allowCacheWrite,
56
+ loggerOptions: config.logger,
57
+ });
58
+ }
59
+ else if (mode === "remote") {
60
+ loadedMessages = await loadRemoteMessages({
61
+ locale,
62
+ fallbackLocales,
63
+ namespaces,
64
+ concurrency,
65
+ fetch,
66
+ url: loader.url,
67
+ headers: loader.headers,
68
+ loggerOptions: config.logger,
69
+ });
70
+ }
71
+ // Final sanity check
72
+ if (!loadedMessages || Object.keys(loadedMessages).length === 0) {
73
+ logger.warn("No messages found.", { locale, fallbackLocales, namespaces });
74
+ }
75
+ return loadedMessages;
76
+ };
77
+
78
+ export { loadMessages };
@@ -0,0 +1,36 @@
1
+ import '../../core/error/intor-error.js';
2
+ import { resolveLoaderOptions } from '../../core/utils/resolve-loader-options.js';
3
+ import 'logry';
4
+ import 'p-limit';
5
+ import { createTranslator } from '../../core/translator/create-translator.js';
6
+ import 'intor-translator';
7
+ import { loadMessages } from '../messages/load-messages.js';
8
+ import 'node:path';
9
+ import 'node:fs/promises';
10
+
11
+ /**
12
+ * Initialize a locale-bound Translator snapshot.
13
+ *
14
+ * - Resolves loader options and loads messages if configured
15
+ * - Creates an immutable Translator instance for server usage
16
+ */
17
+ async function initTranslator(config, locale, options) {
18
+ const { readers, allowCacheWrite = false, fetch, handlers, plugins, } = options;
19
+ const loader = resolveLoaderOptions(config);
20
+ // Load messages
21
+ let messages = {};
22
+ if (loader) {
23
+ const loaded = await loadMessages({
24
+ config,
25
+ locale,
26
+ readers,
27
+ allowCacheWrite,
28
+ fetch,
29
+ });
30
+ messages = loaded || {};
31
+ }
32
+ // Create immutable translator snapshot
33
+ return createTranslator({ config, locale, messages, handlers, plugins });
34
+ }
35
+
36
+ export { initTranslator };
@@ -0,0 +1,3 @@
1
+ export { createIntorHandler } from '../../src/adapters/hono/create-intor-handler.js';
2
+ export { getTranslator } from '../../src/adapters/hono/get-translator.js';
3
+ import 'hono';
@@ -0,0 +1,53 @@
1
+ import '../../core/error/intor-error.js';
2
+ import { parseCookieHeader } from '../../core/utils/parse-cookie-header.js';
3
+ import { normalizeQuery } from '../../core/utils/normalizers/normalize-query.js';
4
+ import 'logry';
5
+ import 'p-limit';
6
+ import 'intor-translator';
7
+ import { getTranslator } from '../../edge/helpers/get-translator.js';
8
+ import { resolveInbound } from '../../routing/inbound/resolve-inbound.js';
9
+ import { getLocaleFromAcceptLanguage } from '../../routing/locale/get-locale-from-accept-language.js';
10
+
11
+ /**
12
+ * Resolves locale-aware routing for the current execution context.
13
+ *
14
+ * - Binds resolved routing state to the request.
15
+ *
16
+ * @platform Hono
17
+ */
18
+ function createIntorHandler(config, options) {
19
+ return async function intorHandler(c, next) {
20
+ // Locale from Accept-Language header
21
+ const acceptLanguage = c.req.header("accept-language");
22
+ const localeFromAcceptLanguage = getLocaleFromAcceptLanguage(acceptLanguage, config.supportedLocales);
23
+ // ----------------------------------------------------------
24
+ // Resolve inbound routing decision (pure computation)
25
+ // ----------------------------------------------------------
26
+ const url = new URL(c.req.url);
27
+ const { locale, localeSource, pathname } = await resolveInbound(config, url.pathname, {
28
+ host: url.hostname,
29
+ query: normalizeQuery(Object.fromEntries(url.searchParams)),
30
+ cookie: parseCookieHeader(c.req.header("cookie"))[config.cookie.name],
31
+ detected: localeFromAcceptLanguage || config.defaultLocale,
32
+ });
33
+ // --------------------------------------------------
34
+ // Bind inbound routing context
35
+ // --------------------------------------------------
36
+ c.set("intor", { locale, localeSource, pathname });
37
+ const { hasKey, t, tRich } = await getTranslator(config, {
38
+ locale,
39
+ handlers: options?.handlers,
40
+ plugins: options?.plugins,
41
+ });
42
+ // DX shortcuts (enabled by default)
43
+ if (options?.shortcuts !== false) {
44
+ c.set("locale", locale);
45
+ c.set("hasKey", hasKey);
46
+ c.set("t", t);
47
+ c.set("tRich", tRich);
48
+ }
49
+ await next();
50
+ };
51
+ }
52
+
53
+ export { createIntorHandler };
@@ -0,0 +1,20 @@
1
+ import { getTranslator as getTranslator$1 } from '../../edge/helpers/get-translator.js';
2
+
3
+ /**
4
+ * Get a edge-runtime translator for the current execution context.
5
+ *
6
+ * - Automatically resolves the locale from the framework context.
7
+ *
8
+ * @platform Hono
9
+ */
10
+ async function getTranslator(config, c, params) {
11
+ const { preKey, handlers, plugins } = params || {};
12
+ return getTranslator$1(config, {
13
+ locale: c.get("intor")?.locale || config.defaultLocale,
14
+ preKey,
15
+ handlers,
16
+ plugins,
17
+ });
18
+ }
19
+
20
+ export { getTranslator };
@@ -0,0 +1,4 @@
1
+ // Default prefix placeholder
2
+ const PREFIX_PLACEHOLDER = "{locale}";
3
+
4
+ export { PREFIX_PLACEHOLDER };
@@ -0,0 +1,9 @@
1
+ var IntorErrorCode;
2
+ (function (IntorErrorCode) {
3
+ // config
4
+ IntorErrorCode["INVALID_CONFIG_ID"] = "INTOR_INVALID_CONFIG_ID";
5
+ IntorErrorCode["MISSING_SUPPORTED_LOCALES"] = "INTOR_MISSING_SUPPORTED_LOCALES";
6
+ IntorErrorCode["UNSUPPORTED_DEFAULT_LOCALE"] = "INTOR_UNSUPPORTED_DEFAULT_LOCALE";
7
+ })(IntorErrorCode || (IntorErrorCode = {}));
8
+
9
+ export { IntorErrorCode };