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 +21 -0
- package/README.md +160 -0
- package/dist/index.cjs +268 -0
- package/dist/index.d.cts +265 -0
- package/dist/index.d.ts +265 -0
- package/dist/index.js +266 -0
- package/package.json +83 -0
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
|
+
[](https://www.npmjs.com/package/intor-translator)
|
|
6
|
+
[](https://bundlephobia.com/package/intor-translator)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](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;
|
package/dist/index.d.cts
ADDED
|
@@ -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.d.ts
ADDED
|
@@ -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
|
+
}
|