intor-translator 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Yiming Liao
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,160 @@
1
+ # IntorTranslator
2
+
3
+ A highly flexible and type-safe i18n translation engine for modern applications — supporting fallback locales, async loading states, scoped namespaces, and both simple and rich formatting.
4
+
5
+ [![NPM version](https://img.shields.io/npm/v/intor-translator)](https://www.npmjs.com/package/intor-translator)
6
+ [![Bundle size](https://img.shields.io/bundlephobia/minzip/intor-translator)](https://bundlephobia.com/package/intor-translator)
7
+ [![License](https://img.shields.io/npm/l/intor-translator)](LICENSE)
8
+ [![TypeScript](https://img.shields.io/badge/TypeScript-%E2%9C%94-blue)](https://www.typescriptlang.org/)
9
+
10
+ ---
11
+
12
+ ## Features
13
+
14
+ - 🌍 Fallback locale support
15
+ - ⚡ Reactive translation logic
16
+ - 🧠 Type-safe nested key paths
17
+ - 🔁 Replacement support
18
+ - 🎨 Rich replacement formatting
19
+ - 🌀 Graceful loading state handling
20
+ - 🔧 Configurable handlers for fallback, loading, and placeholder cases
21
+ - 🧩 Scoped translator for modules or namespaces
22
+
23
+ ---
24
+
25
+ ## Installation
26
+
27
+ ```bash
28
+ npm install intor-translator
29
+ ```
30
+
31
+ or use **yarn**
32
+
33
+ ```bash
34
+ yarn add intor-translator
35
+ ```
36
+
37
+ ---
38
+
39
+ ## Quick Start
40
+
41
+ ```typescript
42
+ import { createTranslator } from "intor-translator";
43
+
44
+ // Create a translator instance
45
+ const translator = createTranslator({
46
+ locale: "en",
47
+ messages: {
48
+ en: {
49
+ hello: "Hello World",
50
+ greeting: "Hello, {name}!", // Use curly braces for replacements
51
+ },
52
+ },
53
+ });
54
+
55
+ // Use the translator
56
+ translator.t("hello"); // > Hello World
57
+ translator.t("greeting", { name: "John doe" }); // > Hello, John doe!
58
+ ```
59
+
60
+ ---
61
+
62
+ ## Advanced Features
63
+
64
+ - Fallback Locales, Placeholder & Custom Handlers
65
+
66
+ ```typescript
67
+ const translator = createTranslator({
68
+ locale: "en",
69
+ messages: {
70
+ en: {
71
+ greeting: "Hello, {name}!",
72
+ },
73
+ zh: {
74
+ greeting: "哈囉, {name}!",
75
+ "only-in-zh": "This message is not exist in en",
76
+ },
77
+ },
78
+ fallbackLocales: { en: ["zh"] }, // Falls back to zh if message is missing in en
79
+ placeholder: "MESSAGE NOT FOUND", // Default text shown when a key is not found in any locale
80
+ handlers: {
81
+ messageFormatter: ({ locale, message }) =>
82
+ `${message}${locale === "en" ? "." : "。"}`, // Custom message formatter
83
+ },
84
+ });
85
+
86
+ translator.t("only-in-zh"); // > This message is not exist in en.
87
+ ```
88
+
89
+ - With Custom ICU Formatter
90
+
91
+ ```typescript
92
+ import IntlMessageFormat from "intl-messageformat";
93
+ import { MessageFormatter } from "intor-translator";
94
+
95
+ const formatter: MessageFormatter = ({ message, locale, replacements }) => {
96
+ const formatter = new IntlMessageFormat(message, locale);
97
+ return formatter.format(replacements);
98
+ };
99
+
100
+ const translator = createTranslator({
101
+ locale: "en",
102
+ messages: {
103
+ en: {
104
+ notification:
105
+ "{name} has {count, plural, =0 {no messages} one {1 message} other {# messages}}.",
106
+ },
107
+ },
108
+ handlers: {
109
+ messageFormatter: formatter,
110
+ },
111
+ });
112
+
113
+ translator.t("notification", { name: "John", count: 0 }); // > John has no messages.
114
+ translator.t("notification", { name: "John", count: 5 }); // > John has 5 messages.
115
+ ```
116
+
117
+ ---
118
+
119
+ ## API Reference
120
+
121
+ | Option | Type | Description |
122
+ | ----------------- | -------------------------------- | ------------------------------------------------------------------------- |
123
+ | `messages` | `Record<Locale, Messages>` | Translation messages, structured by locale |
124
+ | `locale` | `string` | The active locale |
125
+ | `fallbackLocales` | `Record<Locale, Locale[]>` (opt) | Fallback locales used when a message is missing in the active locale |
126
+ | `placeholder` | `string`(opt) | Default message to display when a key is missing |
127
+ | `isLoading` | `boolean`(opt) | Indicates if the translator is in a loading state |
128
+ | `loadingMessage` | `string`(opt) | Message to show when inLoading is true. |
129
+ | `handlers` | `TranslatorHandlers`(opt) | Custom handler functions for formatting, loading, or placeholder messages |
130
+ | |
131
+
132
+ **translator.t(key, replacements?)**
133
+
134
+ - Fully type-safe key access
135
+ - Supports nested keys
136
+ - Supports both string and rich replacements
137
+
138
+ **translator.scope(preKey)**
139
+
140
+ - Returns a scoped translator instance based on a message subtree
141
+ - Useful for organizing large sets of translations with shared prefixes
142
+
143
+ ---
144
+
145
+ ## Project Structure
146
+
147
+ ```bash
148
+ src/
149
+ ├── index.ts # Main entry point and API
150
+ ├── create-translator.ts # Translator factory function
151
+ ├── methods/ # Helper methods for locale, messages, keys, etc.
152
+ │ ├── get-locale/
153
+ │ ├── set-locale/
154
+ │ ├── get-messages/
155
+ │ └── translate/
156
+ │ ├── has-key/
157
+ │ ├── scoped/
158
+ ├── types/ # Type definitions
159
+ └── utils/ # Utility functions
160
+ ```
package/dist/index.cjs ADDED
@@ -0,0 +1,268 @@
1
+ 'use strict';
2
+
3
+ var intorCache = require('intor-cache');
4
+
5
+ // src/methods/get-locale/get-locale.ts
6
+ var getLocale = (localeRef) => {
7
+ return localeRef.current;
8
+ };
9
+
10
+ // src/methods/get-locale/create-get-locale.ts
11
+ var createGetLocale = (localeRef) => {
12
+ return () => getLocale(localeRef);
13
+ };
14
+
15
+ // src/methods/get-messages/get-messages.ts
16
+ var getMessages = (messagesRef) => {
17
+ return messagesRef.current;
18
+ };
19
+
20
+ // src/methods/get-messages/create-get-messages.ts
21
+ var createGetMessages = (messagesRef) => {
22
+ return () => getMessages(messagesRef);
23
+ };
24
+ var getValueByKey = (locale, messages, key, useCache = true) => {
25
+ const cache = intorCache.getMessageKeyCache();
26
+ useCache = Boolean(useCache && cache);
27
+ const cacheKey = `${key}`;
28
+ const currentLocale = cache == null ? void 0 : cache.get("locale");
29
+ if (currentLocale !== locale) {
30
+ cache == null ? void 0 : cache.clear();
31
+ cache == null ? void 0 : cache.set("locale", locale);
32
+ }
33
+ if (useCache && (cache == null ? void 0 : cache.has(cacheKey))) {
34
+ return cache == null ? void 0 : cache.get(cacheKey);
35
+ }
36
+ const value = key.split(".").reduce((acc, key2) => {
37
+ if (acc && typeof acc === "object" && key2 in acc) {
38
+ return acc[key2];
39
+ }
40
+ return void 0;
41
+ }, messages);
42
+ if (useCache && value !== void 0) {
43
+ cache == null ? void 0 : cache.set(cacheKey, value);
44
+ }
45
+ return value;
46
+ };
47
+
48
+ // src/utils/find-message-in-locales.ts
49
+ var findMessageInLocales = ({
50
+ messages,
51
+ localesToTry,
52
+ key
53
+ }) => {
54
+ for (const loc of localesToTry) {
55
+ const localeMessages = messages[loc];
56
+ if (!localeMessages) {
57
+ continue;
58
+ }
59
+ const candidate = getValueByKey(loc, localeMessages, key);
60
+ if (typeof candidate === "string") {
61
+ return candidate;
62
+ }
63
+ }
64
+ return void 0;
65
+ };
66
+
67
+ // src/utils/resolve-locales-to-try.ts
68
+ var resolveLocalesToTry = (locale, fallbackLocales) => {
69
+ const fallbacks = (fallbackLocales == null ? void 0 : fallbackLocales[locale]) || [];
70
+ return [
71
+ locale,
72
+ ...fallbacks.filter((l) => l !== locale)
73
+ ];
74
+ };
75
+
76
+ // src/methods/has-key/has-key.ts
77
+ var hasKey = ({
78
+ messagesRef,
79
+ localeRef,
80
+ translatorOptions,
81
+ key,
82
+ locale
83
+ }) => {
84
+ const messages = messagesRef.current;
85
+ const { fallbackLocales } = translatorOptions;
86
+ const targetLocale = locale != null ? locale : localeRef.current;
87
+ const localesToTry = resolveLocalesToTry(targetLocale, fallbackLocales);
88
+ const message = findMessageInLocales({ messages, localesToTry, key });
89
+ return message ? true : false;
90
+ };
91
+
92
+ // src/methods/has-key/create-has-key.ts
93
+ var createHasKey = (messagesRef, localeRef, translatorOptions) => {
94
+ return (key, locale) => hasKey({
95
+ messagesRef,
96
+ localeRef,
97
+ translatorOptions,
98
+ key,
99
+ locale
100
+ });
101
+ };
102
+
103
+ // src/utils/get-full-key.ts
104
+ var getFullKey = (preKey, key) => {
105
+ if (!preKey) {
106
+ return key;
107
+ }
108
+ if (!key) {
109
+ return preKey;
110
+ }
111
+ return `${preKey}.${key}`;
112
+ };
113
+
114
+ // src/utils/replace-values.ts
115
+ var replaceValues = (message, params) => {
116
+ if (!params || typeof params !== "object" || Object.keys(params).length === 0) {
117
+ return message;
118
+ }
119
+ const replaced = message.replace(/{([^}]+)}/g, (match, key) => {
120
+ const keys = key.split(".");
121
+ let value = params;
122
+ for (const k of keys) {
123
+ if (value == null || typeof value !== "object" || !(k in value)) {
124
+ return match;
125
+ }
126
+ value = value[k];
127
+ }
128
+ return typeof value === "string" || typeof value === "number" ? String(value) : match;
129
+ });
130
+ return replaced;
131
+ };
132
+
133
+ // src/methods/translate/translate.ts
134
+ var translate = ({
135
+ messagesRef,
136
+ localeRef,
137
+ translatorOptions,
138
+ key,
139
+ replacements
140
+ }) => {
141
+ const messages = messagesRef.current;
142
+ const { fallbackLocales, isLoading, loadingMessage, placeholder } = translatorOptions;
143
+ const { messageFormatter, loadingMessageHandler, placeholderHandler } = translatorOptions.handlers || {};
144
+ const localesToTry = resolveLocalesToTry(localeRef.current, fallbackLocales);
145
+ const message = findMessageInLocales({ messages, localesToTry, key });
146
+ if (isLoading) {
147
+ if (loadingMessageHandler) {
148
+ return loadingMessageHandler({
149
+ key,
150
+ locale: localeRef.current,
151
+ replacements
152
+ });
153
+ }
154
+ if (loadingMessage) {
155
+ return loadingMessage;
156
+ }
157
+ }
158
+ if (!message) {
159
+ if (placeholderHandler) {
160
+ return placeholderHandler({
161
+ key,
162
+ locale: localeRef.current,
163
+ replacements
164
+ });
165
+ }
166
+ if (placeholder) {
167
+ return placeholder;
168
+ }
169
+ return key;
170
+ }
171
+ if (messageFormatter) {
172
+ return messageFormatter({
173
+ message,
174
+ key,
175
+ locale: localeRef.current,
176
+ replacements
177
+ });
178
+ } else {
179
+ return replacements ? replaceValues(message, replacements) : message;
180
+ }
181
+ };
182
+
183
+ // src/methods/translate/create-translate.ts
184
+ var createTranslate = (messagesRef, localeRef, translatorOptions) => {
185
+ return (key, replacements) => translate({
186
+ messagesRef,
187
+ localeRef,
188
+ translatorOptions,
189
+ key,
190
+ replacements
191
+ });
192
+ };
193
+
194
+ // src/methods/scoped/scoped.ts
195
+ var scoped = ({
196
+ messagesRef,
197
+ localeRef,
198
+ translatorOptions,
199
+ preKey
200
+ }) => {
201
+ const baseTranslate = createTranslate(
202
+ messagesRef,
203
+ localeRef,
204
+ translatorOptions
205
+ );
206
+ const baseHasKey = createHasKey(
207
+ messagesRef,
208
+ localeRef,
209
+ translatorOptions
210
+ );
211
+ return {
212
+ // t (Scoped)
213
+ t: (key, replacements) => {
214
+ const fullKey = getFullKey(preKey, key);
215
+ return baseTranslate(fullKey, replacements);
216
+ },
217
+ // hasKey (Scoped)
218
+ hasKey: (key, locale) => {
219
+ const fullKey = getFullKey(preKey, key);
220
+ return baseHasKey(fullKey, locale);
221
+ }
222
+ };
223
+ };
224
+
225
+ // src/methods/scoped/create-scoped.ts
226
+ var createScoped = (messagesRef, localeRef, translatorOptions) => {
227
+ return (preKey) => scoped({ messagesRef, localeRef, translatorOptions, preKey });
228
+ };
229
+
230
+ // src/methods/set-locale/set-locale.ts
231
+ var setLocale = ({
232
+ messagesRef,
233
+ localeRef,
234
+ newLocale
235
+ }) => {
236
+ const messages = messagesRef.current;
237
+ if (newLocale in messages) {
238
+ localeRef.current = newLocale;
239
+ }
240
+ };
241
+
242
+ // src/methods/set-locale/create-set-locale.ts
243
+ var createSetLocale = (messagesRef, localeRef) => {
244
+ return (newLocale) => setLocale({ messagesRef, localeRef, newLocale });
245
+ };
246
+
247
+ // src/create-translator.ts
248
+ function createTranslator(translatorOptions) {
249
+ const { locale } = translatorOptions;
250
+ const messagesRef = { current: translatorOptions.messages };
251
+ const localeRef = { current: locale };
252
+ const getLocale2 = createGetLocale(localeRef);
253
+ const setLocale2 = createSetLocale(messagesRef, localeRef);
254
+ const getMessages2 = createGetMessages(messagesRef);
255
+ const hasKey2 = createHasKey(messagesRef, localeRef, translatorOptions);
256
+ const t = createTranslate(messagesRef, localeRef, translatorOptions);
257
+ const scoped2 = createScoped(messagesRef, localeRef, translatorOptions);
258
+ return {
259
+ getLocale: getLocale2,
260
+ setLocale: setLocale2,
261
+ getMessages: getMessages2,
262
+ hasKey: hasKey2,
263
+ t,
264
+ scoped: scoped2
265
+ };
266
+ }
267
+
268
+ exports.createTranslator = createTranslator;
@@ -0,0 +1,265 @@
1
+ import { LocaleNamespaceMessages, Locale, RichReplacement, FallbackLocalesMap, Replacement } from 'intor-types';
2
+
3
+ /**
4
+ * Represents the available locale namespaces from the message map.
5
+ *
6
+ * Extracts top-level keys from the entire localized message structure,
7
+ * which are typically used as namespace identifiers (e.g., "common", "home", "dashboard").
8
+ *
9
+ * @template Messages - The type of the locale message map.
10
+ */
11
+ type RawLocale<Messages extends LocaleNamespaceMessages> = keyof Messages & string;
12
+ /**
13
+ * Computes all possible nested key paths of a deeply nested object.
14
+ *
15
+ * Example:
16
+ * ```ts
17
+ * {
18
+ * a: {
19
+ * b: {
20
+ * c: "hello";
21
+ * };
22
+ * };
23
+ * }
24
+ * ```
25
+ * Will generate: `"a" | "a.b" | "a.b.c"`
26
+ *
27
+ * Useful for type-safe translation key autocompletion and validation.
28
+ *
29
+ * @template Messages - The message object to extract nested key paths from.
30
+ */
31
+ type NestedKeyPaths<Messages> = Messages extends object ? {
32
+ [Key in keyof Messages]: `${Key & string}` | `${Key & string}.${NestedKeyPaths<Messages[Key]>}`;
33
+ }[keyof Messages] : never;
34
+
35
+ type TranslatorHandlers = {
36
+ /**
37
+ * A custom formatter function to format translation messages.
38
+ * You can use this to integrate ICU libraries like `intl-messageformat`.
39
+ */
40
+ messageFormatter?: MessageFormatter;
41
+ /**
42
+ * Handler for loading state of the translation message.
43
+ * Useful when translations are loaded asynchronously.
44
+ */
45
+ loadingMessageHandler?: LoadingMessageHandler;
46
+ /**
47
+ * Handler for placeholders in translation messages.
48
+ * Useful for handling missing or fallback placeholders.
49
+ */
50
+ placeholderHandler?: PlaceholderHandler;
51
+ };
52
+ /**
53
+ * Custom formatter for translation messages.
54
+ *
55
+ * This function receives the raw message and context to produce a formatted result.
56
+ * You can use this to integrate ICU-style formatting or other complex message processing.
57
+ *
58
+ * @template Result - The type of the formatted result.
59
+ *
60
+ * @param ctx - The context object containing information for formatting.
61
+ * @param ctx.message - The raw message string to format.
62
+ * @param ctx.locale - The currently active locale string (e.g. 'en-US').
63
+ * @param ctx.key - The translation key associated with this message.
64
+ * @param ctx.replacements - Optional replacement values for variables inside the message.
65
+ *
66
+ * @returns The formatted message, usually a string but can be any type.
67
+ */
68
+ type MessageFormatter<Result = unknown> = (ctx: TranslatorHandlerContext & {
69
+ message: string;
70
+ }) => Result;
71
+ /**
72
+ * Handler function called when translation message is loading.
73
+ *
74
+ * @template Result - The type of the loading handler result.
75
+ *
76
+ * @param ctx - The context object containing locale, key, and replacements.
77
+ * @returns Result to display during loading (e.g. a placeholder string or React element).
78
+ */
79
+ type LoadingMessageHandler<Result = unknown> = (ctx: TranslatorHandlerContext) => Result;
80
+ /**
81
+ * Handler function for placeholders in translation messages.
82
+ *
83
+ * @template Result - The type of the placeholder handler result.
84
+ *
85
+ * @param ctx - The context object containing locale, key, and replacements.
86
+ * @returns Result to display for placeholders (e.g. a default string or React element).
87
+ */
88
+ type PlaceholderHandler<Result = unknown> = (ctx: TranslatorHandlerContext) => Result;
89
+ /**
90
+ * Context object passed to translation handlers.
91
+ * Contains necessary information for formatting or handling translation messages.
92
+ */
93
+ type TranslatorHandlerContext = {
94
+ locale: Locale;
95
+ key: string;
96
+ replacements?: RichReplacement;
97
+ };
98
+
99
+ /**
100
+ * - Options for creating a translator instance.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const options: TranslatorOptions = {
105
+ * locale: 'en',
106
+ * messages: {
107
+ * en: { common: { hello: "Hello" } },
108
+ * zh: { common: { hello: "你好" } },
109
+ * },
110
+ * fallbackLocales: { zh: ['en'] },
111
+ * handlers: {
112
+ * messageFormatter: ({ message }) => message.toUpperCase(),
113
+ * },
114
+ * };
115
+ * ```
116
+ */
117
+ type TranslatorOptions<Messages extends LocaleNamespaceMessages> = {
118
+ /**
119
+ * - The message definitions to be used by the translator.
120
+ * - These should be pre-loaded and structured by locale and namespace.
121
+ */
122
+ messages: Readonly<Messages>;
123
+ /**
124
+ * - The current active locale, e.g., "en" or "zh-TW".
125
+ */
126
+ locale: RawLocale<Messages>;
127
+ /**
128
+ * - Optional fallback locale(s) to use when a message is missing in the primary locale.
129
+ */
130
+ fallbackLocales?: FallbackLocalesMap;
131
+ /**
132
+ * - Whether the translator is currently in a loading state.
133
+ * - Useful for SSR or async loading scenarios.
134
+ */
135
+ isLoading?: boolean;
136
+ /**
137
+ * - The message string to return while in loading state.
138
+ * - Will be overridden if you provide a `loadingMessageHandler` in handlers.
139
+ */
140
+ loadingMessage?: string;
141
+ /**
142
+ * - A fallback string to show when the message key is missing.
143
+ * - Will be overridden if you provide a `placeholderHandler` in handlers.
144
+ */
145
+ placeholder?: string;
146
+ /**
147
+ * - Optional handlers to customize translation behavior (formatting, placeholders, etc).
148
+ */
149
+ handlers?: TranslatorHandlers;
150
+ };
151
+
152
+ type GetLocale<Messages extends LocaleNamespaceMessages> = () => RawLocale<Messages>;
153
+
154
+ type GetMessages<Messages extends LocaleNamespaceMessages> = () => Readonly<Messages>;
155
+
156
+ type HasKey<Messages extends LocaleNamespaceMessages> = {
157
+ <Locale extends RawLocale<Messages>>(key: NestedKeyPaths<Messages[Locale]>, locale?: Locale): boolean;
158
+ (key: string, locale?: Locale): boolean;
159
+ };
160
+ type LooseHasKey = (key?: string, locale?: Locale) => boolean;
161
+
162
+ type Translate<Messages extends LocaleNamespaceMessages> = {
163
+ <Locale extends RawLocale<Messages>>(key: NestedKeyPaths<Messages[Locale]>, replacements?: Replacement | RichReplacement): string;
164
+ (key: string, replacements?: Replacement | RichReplacement): string;
165
+ };
166
+ type LooseTranslate = (key?: string, replacements?: Replacement | RichReplacement) => string;
167
+
168
+ type Scoped<Messages extends LocaleNamespaceMessages> = <Locale extends RawLocale<Messages>>(preKey?: NestedKeyPaths<Messages[Locale]>) => {
169
+ t: LooseTranslate;
170
+ hasKey: LooseHasKey;
171
+ };
172
+
173
+ type SetLocale<Messages extends LocaleNamespaceMessages> = (newLocale: RawLocale<Messages>) => void;
174
+
175
+ /**
176
+ * The main Translator interface providing all core i18n methods.
177
+ *
178
+ * This interface is the foundation of the `intor` i18n engine, giving access
179
+ * to all translation utilities and state management.
180
+ *
181
+ * @typeParam Messages - The full shape of all available locale messages.
182
+ * Defaults to `LocaleNamespaceMessages`.
183
+ */
184
+ type Translator<Messages extends LocaleNamespaceMessages = LocaleNamespaceMessages> = {
185
+ /**
186
+ * Get the current active locale.
187
+ *
188
+ * @returns The current locale string (e.g., "en", "zh-TW").
189
+ */
190
+ getLocale: GetLocale<Messages>;
191
+ /**
192
+ * Set the current locale.
193
+ *
194
+ * @param locale - The new locale to switch to.
195
+ */
196
+ setLocale: SetLocale<Messages>;
197
+ /**
198
+ * Get all messages for the current locale.
199
+ *
200
+ * @returns The messages object containing all translation namespaces and keys.
201
+ */
202
+ getMessages: GetMessages<Messages>;
203
+ /**
204
+ * Translate a message by its key with optional replacements.
205
+ *
206
+ * This function is fully type-safe, ensuring that translation keys are valid
207
+ * according to the loaded locale messages.
208
+ *
209
+ * You can use autocompletion and strict checking for nested message keys
210
+ * by providing the locale type.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * t("home.welcome.title"); // Fully typed with key autocompletion
215
+ * t("dashboard.stats.count", { count: 5 });
216
+ * ```
217
+ *
218
+ * @param key - A dot-separated translation key (e.g., `"common.hello"`).
219
+ * @param replacements - Optional values to replace placeholders in the message.
220
+ * @returns The translated message string.
221
+ */
222
+ t: Translate<Messages>;
223
+ /**
224
+ * Check whether a strongly-typed translation key exists in the loaded messages.
225
+ *
226
+ * This method ensures the key path is valid according to the message schema
227
+ * for a given locale.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * hasKey("home.welcome.title"); // true or false
232
+ * hasKey("dashboard.stats.count", "zh");
233
+ * ```
234
+ *
235
+ * @param key - A dot-separated message key defined in the locale messages.
236
+ * @param locale - Optional locale to check against. If omitted, defaults to current locale.
237
+ * @returns A boolean indicating whether the key exists.
238
+ */
239
+ hasKey: HasKey<Messages>;
240
+ /**
241
+ * Create a scoped translator bound to a specific namespace.
242
+ *
243
+ * Useful for modular translation logic (e.g., per-page or per-component).
244
+ *
245
+ * @param namespace - The namespace to scope to (e.g., "auth").
246
+ * @returns A new translator with scoped `t()` and helpers.
247
+ */
248
+ scoped: Scoped<Messages>;
249
+ };
250
+
251
+ /**
252
+ * Factory function to create a translator instance.
253
+ *
254
+ * This function sets up a full-featured translator object based on the provided options,
255
+ * including locale management, message retrieval, key existence checking, translation,
256
+ * and scoped translations with prefixes.
257
+ *
258
+ * @template Messages - The shape of the locale namespace messages supported by this translator.
259
+ *
260
+ * @param translatorOptions - Configuration options including initial locale and messages.
261
+ * @returns A translator instance exposing locale control and translation methods.
262
+ */
263
+ declare function createTranslator<Messages extends LocaleNamespaceMessages>(translatorOptions: TranslatorOptions<Messages>): Translator<Messages>;
264
+
265
+ export { type LoadingMessageHandler, type MessageFormatter, type PlaceholderHandler, type Translator, createTranslator };
@@ -0,0 +1,265 @@
1
+ import { LocaleNamespaceMessages, Locale, RichReplacement, FallbackLocalesMap, Replacement } from 'intor-types';
2
+
3
+ /**
4
+ * Represents the available locale namespaces from the message map.
5
+ *
6
+ * Extracts top-level keys from the entire localized message structure,
7
+ * which are typically used as namespace identifiers (e.g., "common", "home", "dashboard").
8
+ *
9
+ * @template Messages - The type of the locale message map.
10
+ */
11
+ type RawLocale<Messages extends LocaleNamespaceMessages> = keyof Messages & string;
12
+ /**
13
+ * Computes all possible nested key paths of a deeply nested object.
14
+ *
15
+ * Example:
16
+ * ```ts
17
+ * {
18
+ * a: {
19
+ * b: {
20
+ * c: "hello";
21
+ * };
22
+ * };
23
+ * }
24
+ * ```
25
+ * Will generate: `"a" | "a.b" | "a.b.c"`
26
+ *
27
+ * Useful for type-safe translation key autocompletion and validation.
28
+ *
29
+ * @template Messages - The message object to extract nested key paths from.
30
+ */
31
+ type NestedKeyPaths<Messages> = Messages extends object ? {
32
+ [Key in keyof Messages]: `${Key & string}` | `${Key & string}.${NestedKeyPaths<Messages[Key]>}`;
33
+ }[keyof Messages] : never;
34
+
35
+ type TranslatorHandlers = {
36
+ /**
37
+ * A custom formatter function to format translation messages.
38
+ * You can use this to integrate ICU libraries like `intl-messageformat`.
39
+ */
40
+ messageFormatter?: MessageFormatter;
41
+ /**
42
+ * Handler for loading state of the translation message.
43
+ * Useful when translations are loaded asynchronously.
44
+ */
45
+ loadingMessageHandler?: LoadingMessageHandler;
46
+ /**
47
+ * Handler for placeholders in translation messages.
48
+ * Useful for handling missing or fallback placeholders.
49
+ */
50
+ placeholderHandler?: PlaceholderHandler;
51
+ };
52
+ /**
53
+ * Custom formatter for translation messages.
54
+ *
55
+ * This function receives the raw message and context to produce a formatted result.
56
+ * You can use this to integrate ICU-style formatting or other complex message processing.
57
+ *
58
+ * @template Result - The type of the formatted result.
59
+ *
60
+ * @param ctx - The context object containing information for formatting.
61
+ * @param ctx.message - The raw message string to format.
62
+ * @param ctx.locale - The currently active locale string (e.g. 'en-US').
63
+ * @param ctx.key - The translation key associated with this message.
64
+ * @param ctx.replacements - Optional replacement values for variables inside the message.
65
+ *
66
+ * @returns The formatted message, usually a string but can be any type.
67
+ */
68
+ type MessageFormatter<Result = unknown> = (ctx: TranslatorHandlerContext & {
69
+ message: string;
70
+ }) => Result;
71
+ /**
72
+ * Handler function called when translation message is loading.
73
+ *
74
+ * @template Result - The type of the loading handler result.
75
+ *
76
+ * @param ctx - The context object containing locale, key, and replacements.
77
+ * @returns Result to display during loading (e.g. a placeholder string or React element).
78
+ */
79
+ type LoadingMessageHandler<Result = unknown> = (ctx: TranslatorHandlerContext) => Result;
80
+ /**
81
+ * Handler function for placeholders in translation messages.
82
+ *
83
+ * @template Result - The type of the placeholder handler result.
84
+ *
85
+ * @param ctx - The context object containing locale, key, and replacements.
86
+ * @returns Result to display for placeholders (e.g. a default string or React element).
87
+ */
88
+ type PlaceholderHandler<Result = unknown> = (ctx: TranslatorHandlerContext) => Result;
89
+ /**
90
+ * Context object passed to translation handlers.
91
+ * Contains necessary information for formatting or handling translation messages.
92
+ */
93
+ type TranslatorHandlerContext = {
94
+ locale: Locale;
95
+ key: string;
96
+ replacements?: RichReplacement;
97
+ };
98
+
99
+ /**
100
+ * - Options for creating a translator instance.
101
+ *
102
+ * @example
103
+ * ```ts
104
+ * const options: TranslatorOptions = {
105
+ * locale: 'en',
106
+ * messages: {
107
+ * en: { common: { hello: "Hello" } },
108
+ * zh: { common: { hello: "你好" } },
109
+ * },
110
+ * fallbackLocales: { zh: ['en'] },
111
+ * handlers: {
112
+ * messageFormatter: ({ message }) => message.toUpperCase(),
113
+ * },
114
+ * };
115
+ * ```
116
+ */
117
+ type TranslatorOptions<Messages extends LocaleNamespaceMessages> = {
118
+ /**
119
+ * - The message definitions to be used by the translator.
120
+ * - These should be pre-loaded and structured by locale and namespace.
121
+ */
122
+ messages: Readonly<Messages>;
123
+ /**
124
+ * - The current active locale, e.g., "en" or "zh-TW".
125
+ */
126
+ locale: RawLocale<Messages>;
127
+ /**
128
+ * - Optional fallback locale(s) to use when a message is missing in the primary locale.
129
+ */
130
+ fallbackLocales?: FallbackLocalesMap;
131
+ /**
132
+ * - Whether the translator is currently in a loading state.
133
+ * - Useful for SSR or async loading scenarios.
134
+ */
135
+ isLoading?: boolean;
136
+ /**
137
+ * - The message string to return while in loading state.
138
+ * - Will be overridden if you provide a `loadingMessageHandler` in handlers.
139
+ */
140
+ loadingMessage?: string;
141
+ /**
142
+ * - A fallback string to show when the message key is missing.
143
+ * - Will be overridden if you provide a `placeholderHandler` in handlers.
144
+ */
145
+ placeholder?: string;
146
+ /**
147
+ * - Optional handlers to customize translation behavior (formatting, placeholders, etc).
148
+ */
149
+ handlers?: TranslatorHandlers;
150
+ };
151
+
152
+ type GetLocale<Messages extends LocaleNamespaceMessages> = () => RawLocale<Messages>;
153
+
154
+ type GetMessages<Messages extends LocaleNamespaceMessages> = () => Readonly<Messages>;
155
+
156
+ type HasKey<Messages extends LocaleNamespaceMessages> = {
157
+ <Locale extends RawLocale<Messages>>(key: NestedKeyPaths<Messages[Locale]>, locale?: Locale): boolean;
158
+ (key: string, locale?: Locale): boolean;
159
+ };
160
+ type LooseHasKey = (key?: string, locale?: Locale) => boolean;
161
+
162
+ type Translate<Messages extends LocaleNamespaceMessages> = {
163
+ <Locale extends RawLocale<Messages>>(key: NestedKeyPaths<Messages[Locale]>, replacements?: Replacement | RichReplacement): string;
164
+ (key: string, replacements?: Replacement | RichReplacement): string;
165
+ };
166
+ type LooseTranslate = (key?: string, replacements?: Replacement | RichReplacement) => string;
167
+
168
+ type Scoped<Messages extends LocaleNamespaceMessages> = <Locale extends RawLocale<Messages>>(preKey?: NestedKeyPaths<Messages[Locale]>) => {
169
+ t: LooseTranslate;
170
+ hasKey: LooseHasKey;
171
+ };
172
+
173
+ type SetLocale<Messages extends LocaleNamespaceMessages> = (newLocale: RawLocale<Messages>) => void;
174
+
175
+ /**
176
+ * The main Translator interface providing all core i18n methods.
177
+ *
178
+ * This interface is the foundation of the `intor` i18n engine, giving access
179
+ * to all translation utilities and state management.
180
+ *
181
+ * @typeParam Messages - The full shape of all available locale messages.
182
+ * Defaults to `LocaleNamespaceMessages`.
183
+ */
184
+ type Translator<Messages extends LocaleNamespaceMessages = LocaleNamespaceMessages> = {
185
+ /**
186
+ * Get the current active locale.
187
+ *
188
+ * @returns The current locale string (e.g., "en", "zh-TW").
189
+ */
190
+ getLocale: GetLocale<Messages>;
191
+ /**
192
+ * Set the current locale.
193
+ *
194
+ * @param locale - The new locale to switch to.
195
+ */
196
+ setLocale: SetLocale<Messages>;
197
+ /**
198
+ * Get all messages for the current locale.
199
+ *
200
+ * @returns The messages object containing all translation namespaces and keys.
201
+ */
202
+ getMessages: GetMessages<Messages>;
203
+ /**
204
+ * Translate a message by its key with optional replacements.
205
+ *
206
+ * This function is fully type-safe, ensuring that translation keys are valid
207
+ * according to the loaded locale messages.
208
+ *
209
+ * You can use autocompletion and strict checking for nested message keys
210
+ * by providing the locale type.
211
+ *
212
+ * @example
213
+ * ```ts
214
+ * t("home.welcome.title"); // Fully typed with key autocompletion
215
+ * t("dashboard.stats.count", { count: 5 });
216
+ * ```
217
+ *
218
+ * @param key - A dot-separated translation key (e.g., `"common.hello"`).
219
+ * @param replacements - Optional values to replace placeholders in the message.
220
+ * @returns The translated message string.
221
+ */
222
+ t: Translate<Messages>;
223
+ /**
224
+ * Check whether a strongly-typed translation key exists in the loaded messages.
225
+ *
226
+ * This method ensures the key path is valid according to the message schema
227
+ * for a given locale.
228
+ *
229
+ * @example
230
+ * ```ts
231
+ * hasKey("home.welcome.title"); // true or false
232
+ * hasKey("dashboard.stats.count", "zh");
233
+ * ```
234
+ *
235
+ * @param key - A dot-separated message key defined in the locale messages.
236
+ * @param locale - Optional locale to check against. If omitted, defaults to current locale.
237
+ * @returns A boolean indicating whether the key exists.
238
+ */
239
+ hasKey: HasKey<Messages>;
240
+ /**
241
+ * Create a scoped translator bound to a specific namespace.
242
+ *
243
+ * Useful for modular translation logic (e.g., per-page or per-component).
244
+ *
245
+ * @param namespace - The namespace to scope to (e.g., "auth").
246
+ * @returns A new translator with scoped `t()` and helpers.
247
+ */
248
+ scoped: Scoped<Messages>;
249
+ };
250
+
251
+ /**
252
+ * Factory function to create a translator instance.
253
+ *
254
+ * This function sets up a full-featured translator object based on the provided options,
255
+ * including locale management, message retrieval, key existence checking, translation,
256
+ * and scoped translations with prefixes.
257
+ *
258
+ * @template Messages - The shape of the locale namespace messages supported by this translator.
259
+ *
260
+ * @param translatorOptions - Configuration options including initial locale and messages.
261
+ * @returns A translator instance exposing locale control and translation methods.
262
+ */
263
+ declare function createTranslator<Messages extends LocaleNamespaceMessages>(translatorOptions: TranslatorOptions<Messages>): Translator<Messages>;
264
+
265
+ export { type LoadingMessageHandler, type MessageFormatter, type PlaceholderHandler, type Translator, createTranslator };
package/dist/index.js ADDED
@@ -0,0 +1,266 @@
1
+ import { getMessageKeyCache } from 'intor-cache';
2
+
3
+ // src/methods/get-locale/get-locale.ts
4
+ var getLocale = (localeRef) => {
5
+ return localeRef.current;
6
+ };
7
+
8
+ // src/methods/get-locale/create-get-locale.ts
9
+ var createGetLocale = (localeRef) => {
10
+ return () => getLocale(localeRef);
11
+ };
12
+
13
+ // src/methods/get-messages/get-messages.ts
14
+ var getMessages = (messagesRef) => {
15
+ return messagesRef.current;
16
+ };
17
+
18
+ // src/methods/get-messages/create-get-messages.ts
19
+ var createGetMessages = (messagesRef) => {
20
+ return () => getMessages(messagesRef);
21
+ };
22
+ var getValueByKey = (locale, messages, key, useCache = true) => {
23
+ const cache = getMessageKeyCache();
24
+ useCache = Boolean(useCache && cache);
25
+ const cacheKey = `${key}`;
26
+ const currentLocale = cache == null ? void 0 : cache.get("locale");
27
+ if (currentLocale !== locale) {
28
+ cache == null ? void 0 : cache.clear();
29
+ cache == null ? void 0 : cache.set("locale", locale);
30
+ }
31
+ if (useCache && (cache == null ? void 0 : cache.has(cacheKey))) {
32
+ return cache == null ? void 0 : cache.get(cacheKey);
33
+ }
34
+ const value = key.split(".").reduce((acc, key2) => {
35
+ if (acc && typeof acc === "object" && key2 in acc) {
36
+ return acc[key2];
37
+ }
38
+ return void 0;
39
+ }, messages);
40
+ if (useCache && value !== void 0) {
41
+ cache == null ? void 0 : cache.set(cacheKey, value);
42
+ }
43
+ return value;
44
+ };
45
+
46
+ // src/utils/find-message-in-locales.ts
47
+ var findMessageInLocales = ({
48
+ messages,
49
+ localesToTry,
50
+ key
51
+ }) => {
52
+ for (const loc of localesToTry) {
53
+ const localeMessages = messages[loc];
54
+ if (!localeMessages) {
55
+ continue;
56
+ }
57
+ const candidate = getValueByKey(loc, localeMessages, key);
58
+ if (typeof candidate === "string") {
59
+ return candidate;
60
+ }
61
+ }
62
+ return void 0;
63
+ };
64
+
65
+ // src/utils/resolve-locales-to-try.ts
66
+ var resolveLocalesToTry = (locale, fallbackLocales) => {
67
+ const fallbacks = (fallbackLocales == null ? void 0 : fallbackLocales[locale]) || [];
68
+ return [
69
+ locale,
70
+ ...fallbacks.filter((l) => l !== locale)
71
+ ];
72
+ };
73
+
74
+ // src/methods/has-key/has-key.ts
75
+ var hasKey = ({
76
+ messagesRef,
77
+ localeRef,
78
+ translatorOptions,
79
+ key,
80
+ locale
81
+ }) => {
82
+ const messages = messagesRef.current;
83
+ const { fallbackLocales } = translatorOptions;
84
+ const targetLocale = locale != null ? locale : localeRef.current;
85
+ const localesToTry = resolveLocalesToTry(targetLocale, fallbackLocales);
86
+ const message = findMessageInLocales({ messages, localesToTry, key });
87
+ return message ? true : false;
88
+ };
89
+
90
+ // src/methods/has-key/create-has-key.ts
91
+ var createHasKey = (messagesRef, localeRef, translatorOptions) => {
92
+ return (key, locale) => hasKey({
93
+ messagesRef,
94
+ localeRef,
95
+ translatorOptions,
96
+ key,
97
+ locale
98
+ });
99
+ };
100
+
101
+ // src/utils/get-full-key.ts
102
+ var getFullKey = (preKey, key) => {
103
+ if (!preKey) {
104
+ return key;
105
+ }
106
+ if (!key) {
107
+ return preKey;
108
+ }
109
+ return `${preKey}.${key}`;
110
+ };
111
+
112
+ // src/utils/replace-values.ts
113
+ var replaceValues = (message, params) => {
114
+ if (!params || typeof params !== "object" || Object.keys(params).length === 0) {
115
+ return message;
116
+ }
117
+ const replaced = message.replace(/{([^}]+)}/g, (match, key) => {
118
+ const keys = key.split(".");
119
+ let value = params;
120
+ for (const k of keys) {
121
+ if (value == null || typeof value !== "object" || !(k in value)) {
122
+ return match;
123
+ }
124
+ value = value[k];
125
+ }
126
+ return typeof value === "string" || typeof value === "number" ? String(value) : match;
127
+ });
128
+ return replaced;
129
+ };
130
+
131
+ // src/methods/translate/translate.ts
132
+ var translate = ({
133
+ messagesRef,
134
+ localeRef,
135
+ translatorOptions,
136
+ key,
137
+ replacements
138
+ }) => {
139
+ const messages = messagesRef.current;
140
+ const { fallbackLocales, isLoading, loadingMessage, placeholder } = translatorOptions;
141
+ const { messageFormatter, loadingMessageHandler, placeholderHandler } = translatorOptions.handlers || {};
142
+ const localesToTry = resolveLocalesToTry(localeRef.current, fallbackLocales);
143
+ const message = findMessageInLocales({ messages, localesToTry, key });
144
+ if (isLoading) {
145
+ if (loadingMessageHandler) {
146
+ return loadingMessageHandler({
147
+ key,
148
+ locale: localeRef.current,
149
+ replacements
150
+ });
151
+ }
152
+ if (loadingMessage) {
153
+ return loadingMessage;
154
+ }
155
+ }
156
+ if (!message) {
157
+ if (placeholderHandler) {
158
+ return placeholderHandler({
159
+ key,
160
+ locale: localeRef.current,
161
+ replacements
162
+ });
163
+ }
164
+ if (placeholder) {
165
+ return placeholder;
166
+ }
167
+ return key;
168
+ }
169
+ if (messageFormatter) {
170
+ return messageFormatter({
171
+ message,
172
+ key,
173
+ locale: localeRef.current,
174
+ replacements
175
+ });
176
+ } else {
177
+ return replacements ? replaceValues(message, replacements) : message;
178
+ }
179
+ };
180
+
181
+ // src/methods/translate/create-translate.ts
182
+ var createTranslate = (messagesRef, localeRef, translatorOptions) => {
183
+ return (key, replacements) => translate({
184
+ messagesRef,
185
+ localeRef,
186
+ translatorOptions,
187
+ key,
188
+ replacements
189
+ });
190
+ };
191
+
192
+ // src/methods/scoped/scoped.ts
193
+ var scoped = ({
194
+ messagesRef,
195
+ localeRef,
196
+ translatorOptions,
197
+ preKey
198
+ }) => {
199
+ const baseTranslate = createTranslate(
200
+ messagesRef,
201
+ localeRef,
202
+ translatorOptions
203
+ );
204
+ const baseHasKey = createHasKey(
205
+ messagesRef,
206
+ localeRef,
207
+ translatorOptions
208
+ );
209
+ return {
210
+ // t (Scoped)
211
+ t: (key, replacements) => {
212
+ const fullKey = getFullKey(preKey, key);
213
+ return baseTranslate(fullKey, replacements);
214
+ },
215
+ // hasKey (Scoped)
216
+ hasKey: (key, locale) => {
217
+ const fullKey = getFullKey(preKey, key);
218
+ return baseHasKey(fullKey, locale);
219
+ }
220
+ };
221
+ };
222
+
223
+ // src/methods/scoped/create-scoped.ts
224
+ var createScoped = (messagesRef, localeRef, translatorOptions) => {
225
+ return (preKey) => scoped({ messagesRef, localeRef, translatorOptions, preKey });
226
+ };
227
+
228
+ // src/methods/set-locale/set-locale.ts
229
+ var setLocale = ({
230
+ messagesRef,
231
+ localeRef,
232
+ newLocale
233
+ }) => {
234
+ const messages = messagesRef.current;
235
+ if (newLocale in messages) {
236
+ localeRef.current = newLocale;
237
+ }
238
+ };
239
+
240
+ // src/methods/set-locale/create-set-locale.ts
241
+ var createSetLocale = (messagesRef, localeRef) => {
242
+ return (newLocale) => setLocale({ messagesRef, localeRef, newLocale });
243
+ };
244
+
245
+ // src/create-translator.ts
246
+ function createTranslator(translatorOptions) {
247
+ const { locale } = translatorOptions;
248
+ const messagesRef = { current: translatorOptions.messages };
249
+ const localeRef = { current: locale };
250
+ const getLocale2 = createGetLocale(localeRef);
251
+ const setLocale2 = createSetLocale(messagesRef, localeRef);
252
+ const getMessages2 = createGetMessages(messagesRef);
253
+ const hasKey2 = createHasKey(messagesRef, localeRef, translatorOptions);
254
+ const t = createTranslate(messagesRef, localeRef, translatorOptions);
255
+ const scoped2 = createScoped(messagesRef, localeRef, translatorOptions);
256
+ return {
257
+ getLocale: getLocale2,
258
+ setLocale: setLocale2,
259
+ getMessages: getMessages2,
260
+ hasKey: hasKey2,
261
+ t,
262
+ scoped: scoped2
263
+ };
264
+ }
265
+
266
+ export { createTranslator };
package/package.json ADDED
@@ -0,0 +1,83 @@
1
+ {
2
+ "name": "intor-translator",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight, type-safe i18n translator for JavaScript and TypeScript. Provides runtime translation, scoped messages, ICU formatting, and custom handlers.",
5
+ "author": "Yiming Liao",
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/yiming-liao/intor-translator#readme",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/yiming-liao/intor-translator"
11
+ },
12
+ "bugs": {
13
+ "url": "https://github.com/yiming-liao/intor-translator/issues"
14
+ },
15
+ "keywords": [
16
+ "i18n",
17
+ "internationalization",
18
+ "translation",
19
+ "typescript",
20
+ "node",
21
+ "nextjs",
22
+ "react",
23
+ "translator",
24
+ "i18n core",
25
+ "custom messages"
26
+ ],
27
+ "main": "dist/index.js",
28
+ "exports": {
29
+ ".": {
30
+ "import": "./dist/index.js",
31
+ "require": "./dist/index.cjs"
32
+ }
33
+ },
34
+ "types": "./dist/index.d.ts",
35
+ "files": [
36
+ "dist"
37
+ ],
38
+ "type": "module",
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "lint": "eslint",
42
+ "type-check": "tsc --noEmit",
43
+ "test": "jest",
44
+ "test:coverage": "jest --coverage && open coverage/lcov-report/index.html",
45
+ "prepublishOnly": "yarn build"
46
+ },
47
+ "sideEffects": false,
48
+ "engines": {
49
+ "node": ">=16.0.0"
50
+ },
51
+ "dependencies": {
52
+ "intor-cache": "^1.0.1",
53
+ "intor-types": "^1.0.1"
54
+ },
55
+ "devDependencies": {
56
+ "@eslint/js": "^9.27.0",
57
+ "@testing-library/dom": "^10.4.0",
58
+ "@testing-library/jest-dom": "^6.6.3",
59
+ "@testing-library/react": "^16.3.0",
60
+ "@types/jest": "^29.5.14",
61
+ "@types/react": "^19.1.4",
62
+ "@types/react-dom": "^19.1.5",
63
+ "eslint": "^9.27.0",
64
+ "eslint-config-prettier": "^10.1.5",
65
+ "eslint-plugin-import": "^2.31.0",
66
+ "eslint-plugin-prettier": "^5.4.0",
67
+ "eslint-plugin-unused-imports": "^4.1.4",
68
+ "globals": "^16.1.0",
69
+ "intl-messageformat": "^10.7.16",
70
+ "jest": "^29.7.0",
71
+ "jest-environment-jsdom": "^29.7.0",
72
+ "jest-fetch-mock": "^3.0.3",
73
+ "next": "^15.3.2",
74
+ "prettier": "^3.5.3",
75
+ "react": "^19.1.0",
76
+ "react-dom": "^19.1.0",
77
+ "ts-jest": "^29.3.2",
78
+ "ts-node": "^10.9.2",
79
+ "tsup": "^8.4.0",
80
+ "typescript": "^5.8.3",
81
+ "typescript-eslint": "^8.32.1"
82
+ }
83
+ }