intor-translator 1.1.5 → 1.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,8 @@
2
2
 
3
3
  <div align="center">
4
4
 
5
- A type safe translator that knows what to say and how to handle the rest.
5
+ A modern **i18n engine** powered by a customizable, type-safe translation pipeline.
6
+ Easy to use, modular at its core, and fully extensible.
6
7
 
7
8
  </div>
8
9
 
@@ -10,44 +11,34 @@ A type safe translator that knows what to say and how to handle the rest.
10
11
 
11
12
  [![NPM version](https://img.shields.io/npm/v/intor-translator?style=flat&colorA=000000&colorB=000000)](https://www.npmjs.com/package/intor-translator)
12
13
  [![Bundle size](https://img.shields.io/bundlephobia/minzip/intor-translator?style=flat&colorA=000000&colorB=000000)](https://bundlephobia.com/package/intor-translator)
13
- [![License](https://img.shields.io/npm/l/intor-translator?style=flat&colorA=000000&colorB=000000)](LICENSE)
14
+ [![Coverage Status](https://img.shields.io/coveralls/github/yiming-liao/intor-translator.svg?branch=main&style=flat&colorA=000000&colorB=000000)](https://coveralls.io/github/yiming-liao/intor-translator?branch=main)
14
15
  [![TypeScript](https://img.shields.io/badge/TypeScript-%E2%9C%94-blue?style=flat&colorA=000000&colorB=000000)](https://www.typescriptlang.org/)
16
+ [![License](https://img.shields.io/npm/l/intor-translator?style=flat&colorA=000000&colorB=000000)](LICENSE)
15
17
 
16
18
  </div>
17
19
 
18
- > Translate with confidence.
19
- > A type-safe i18n engine with fallback, scoped namespaces, and graceful loading.
20
-
21
- ---
20
+ > Structured 󠁯•󠁏 Predictable 󠁯•󠁏 Beautifully simple
22
21
 
23
22
  ## Features
24
23
 
25
- - 🌍 Fallback locale support for smooth language switching
26
- - Reactive translation logic that updates on the fly
27
- - 🧠 Type-safe nested key paths with full autocomplete
28
- - 🔁 Flexible replacement and interpolation support
29
- - 🎨 Rich formatting for complex replacement content
30
- - 🌀 Graceful handling of loading and async states
31
- - 🔧 Configurable handlers for fallback, loading, and missing keys
32
- - 🧩 Scoped translators for modules and namespaces
33
-
34
- ---
24
+ - 🔧 **Modular Pipeline** A pluggable, hook-driven flow for any translation logic.
25
+ - **Typed Autocomplete** Inferred keys and locales with precise, reliable completion.
26
+ - 🌐 **Framework-Agnostic** A lightweight engine that runs anywhere in JavaScript.
35
27
 
36
- ## <img src="https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Symbols/Triangular%20Flag.png" alt="Triangular Flag" width="16" height="16" /> Installation
28
+ ## Installation
37
29
 
38
30
  ```bash
31
+ # npm
39
32
  npm install intor-translator
40
- ```
41
-
42
- or use **yarn**
43
33
 
44
- ```bash
34
+ # yarn
45
35
  yarn add intor-translator
46
- ```
47
36
 
48
- ---
37
+ # pnpm
38
+ pnpm add intor-translator
39
+ ```
49
40
 
50
- ## <img src="https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Travel%20and%20places/Rocket.png" alt="Rocket" width="25" height="25" /> Quick Start
41
+ ## Quick Start
51
42
 
52
43
  ```typescript
53
44
  import { Translator } from "intor-translator";
@@ -67,134 +58,35 @@ translator.t("hello"); // -> Hello World
67
58
  translator.t("greeting", { name: "John doe" }); // -> Hello, John doe!
68
59
  ```
69
60
 
70
- ---
61
+ ## Handlers & Hooks
71
62
 
72
- ## <img src="https://raw.githubusercontent.com/Tarikul-Islam-Anik/Animated-Fluent-Emojis/master/Emojis/Activities/Sparkles.png" alt="Sparkles" width="25" height="25" /> Advanced Features
63
+ Intor Translator is powered by **a flexible pipeline** that lets you control how translations behave and how they are rendered.
73
64
 
74
- - Fallback Locales, Placeholder & Custom Handlers
65
+ ### Handlers format the final output
75
66
 
76
- ```typescript
77
- const translator = new Translator({
78
- locale: "en",
79
- messages: {
80
- en: {
81
- welcome: "Welcome back, {name}",
82
- },
83
- zh: {
84
- welcome: "歡迎回來,{name}",
85
- notification: "你有 {count} 則新通知",
86
- },
87
- },
88
- fallbackLocales: { en: ["zh"] }, // Use zh if message not found in en
89
- placeholder: "Content unavailable", // Shown if key is missing in all locales
90
- handlers: {
91
- formatHandler: ({ locale, message }) =>
92
- locale === "zh" ? `${message}。` : `${message}.`, // Auto punctuation per locale
93
- },
94
- });
67
+ <sup>_changing how translations look_.</sup>
95
68
 
96
- // en has 'welcome'
97
- console.log(translator.t("welcome", { name: "John" })); // -> Welcome back, John.
69
+ Handlers operate on the resolved message, use them to:
98
70
 
99
- // en does not have 'notification', fallback to zh
100
- console.log(translator.t("notification", { count: 3 })); // -> 你有 3 則新通知。
71
+ - format ICU messages
72
+ - apply custom plural logic
73
+ - post-process output
74
+ - style or transform the final string
101
75
 
102
- // message does not exist in any locale
103
- console.log(translator.t("unknown.key")); // -> Content unavailable
104
- ```
76
+ ### Hooks shape the translation flow
105
77
 
106
- - With Custom ICU Formatter
78
+ <sup>_changing how translations work_.</sup>
107
79
 
108
- ```typescript
109
- import { Translator, FormatMessage } from "intor-translator";
110
- import { IntlMessageFormat } from "intl-messageformat";
80
+ Hooks run through the pipeline and can intercept any stage, use them to:
111
81
 
112
- // Create a custom handler
113
- const formatHandler: FormatMessage = ({ message, locale, replacements }) => {
114
- const formatter = new IntlMessageFormat(message, locale);
115
- return formatter.format(replacements);
116
- };
82
+ - transform keys or messages
83
+ - adjust fallback behavior
84
+ - implement loading or missing logic
85
+ - attach metadata or analytics
117
86
 
118
- const messages = {
119
- en: {
120
- notification:
121
- "{name} has {count, plural, =0 {no messages} one {1 message} other {# messages}}.",
122
- },
123
- };
124
-
125
- // Create a translator instance
126
- const translator = new Translator({
127
- locale: "en",
128
- messages,
129
- handlers: { formatHandler },
130
- });
131
-
132
- translator.t("notification", { name: "John", count: 0 }); // -> John has no messages.
133
- translator.t("notification", { name: "John", count: 5 }); // -> John has 5 messages.
134
- ```
135
-
136
- ---
137
-
138
- ## API Reference
139
-
140
- ### Translator Parameters
141
-
142
- | Option | Type | Description |
143
- | ----------------- | ------------------------------------- | ------------------------------------------------------------------------ |
144
- | `messages` | `Readonly<LocaleMessages>` | Translation messages grouped by locale and namespace |
145
- | `locale` | `string` | Active locale key |
146
- | `fallbackLocales` | `Record<Locale, Locale[]>` (optional) | Locales to fallback to when a key is missing |
147
- | `placeholder` | `string` (optional) | Message to display when a key is missing in all locales |
148
- | `loadingMessage` | `string` (optional) | Message to display during loading or async state |
149
- | `handlers` | `TranslateHandlers` (optional) | Custom functions for formatting, loading state, and missing key handling |
150
-
151
- **TranslateHandlers :**
152
-
153
- ```ts
154
- type TranslateHandlers = {
155
- formatHandler?: (
156
- ctx: TranslateHandlerContext & { message: string },
157
- ) => unknown;
158
- LoadingHandler?: (ctx: TranslateHandlerContext) => unknown;
159
- MissingHandler?: (ctx: TranslateHandlerContext) => unknown;
160
- };
161
- ```
162
-
163
- > Use handlers to control how messages are formatted, what to show during loading, and how to respond to missing keys.
164
- > Each handler receives a full translation context, including the current locale, key, and replacement values.
87
+ > Together, they form a customizable translation pipeline — structured, predictable, beautifully simple.
165
88
 
166
89
  ---
167
90
 
168
- ### Instance Properties
169
-
170
- | Property | Type | Description |
171
- | ----------- | ----------- | ------------------------------------------ |
172
- | `messages` | `M` | Current messages object |
173
- | `locale` | `Locale<M>` | Currently active locale |
174
- | `isLoading` | `boolean` | Whether the translator is in loading state |
175
-
176
- ---
177
-
178
- ### Instance Methods
179
-
180
- | Method | Signature | Description |
181
- | ------------- | ------------------------------------------------- | ------------------------------------------------------------------- |
182
- | `setMessages` | `(messages: M) => void` | Replaces the current message set |
183
- | `setLocale` | `(locale: Locale<M>) => boolean` | Sets a new locale and returns whether it changed |
184
- | `setLoading` | `(state: boolean) => void` | Sets the loading state manually |
185
- | `hasKey` | `(key, targetLocale?) => boolean` | Checks whether the given key exists in the target or current locale |
186
- | `t` | `<Result = string>(key, replacements?) => Result` | Translates a key with optional replacements |
187
- | `scoped` | `(preKey: string) => { hasKey(), t() }` | Creates a scoped translator with a namespace prefix |
188
-
189
- **translator.t(key, replacements?)**
190
-
191
- - Fully type-safe key access
192
- - Supports nested keys
193
- - Supports both string and rich replacements
194
-
195
- **translator.scoped(preKey)**
196
-
197
- - Returns a scoped translator instance based on a message subtree
198
- - Useful for organizing large sets of translations with shared prefixes
199
-
200
- ---
91
+ **_For more advanced usage, see the full examples._**
92
+ [View examples ↗](https://github.com/yiming-liao/intor-translator/tree/main/examples)
package/dist/index.cjs CHANGED
@@ -1,6 +1,10 @@
1
1
  'use strict';
2
2
 
3
- // src/utils/find-message-in-locales.ts
3
+ var rura = require('rura');
4
+
5
+ // src/pipeline/hooks/find-message.ts
6
+
7
+ // src/translators/shared/utils/find-message-in-locales.ts
4
8
  var findMessageInLocales = ({
5
9
  messages,
6
10
  candidateLocales,
@@ -23,28 +27,50 @@ var findMessageInLocales = ({
23
27
  }
24
28
  };
25
29
 
26
- // src/utils/resolve-candidate-locales.ts
27
- var resolveCandidateLocales = (locale, fallbackLocalesMap) => {
28
- const fallbacks = fallbackLocalesMap?.[locale] || [];
29
- const filteredFallbacks = fallbacks.filter((l) => l !== locale);
30
- return [locale, ...filteredFallbacks];
31
- };
30
+ // src/pipeline/hooks/find-message.ts
31
+ var findMessage = rura.rura.createHook(
32
+ "findMessage",
33
+ (ctx) => {
34
+ ctx.rawMessage = findMessageInLocales({
35
+ messages: ctx.messages,
36
+ candidateLocales: ctx.candidateLocales,
37
+ key: ctx.key
38
+ });
39
+ },
40
+ 200
41
+ );
32
42
 
33
- // src/translator-methods/has-key/has-key.ts
34
- var hasKey = ({
35
- messagesRef,
36
- localeRef,
37
- key,
38
- targetLocale
39
- }) => {
40
- const messages = messagesRef.current;
41
- const locale = localeRef.current;
42
- const candidateLocales = resolveCandidateLocales(targetLocale || locale);
43
- const message = findMessageInLocales({ messages, candidateLocales, key });
44
- return !!message;
45
- };
43
+ // src/pipeline/utils/make-handler-context.ts
44
+ function makeHandlerContext(ctx) {
45
+ return Object.freeze({
46
+ locale: ctx.locale,
47
+ key: ctx.key,
48
+ replacements: ctx.replacements,
49
+ messages: ctx.messages,
50
+ candidateLocales: ctx.candidateLocales,
51
+ config: ctx.config,
52
+ isLoading: ctx.isLoading,
53
+ rawMessage: ctx.rawMessage,
54
+ formattedMessage: ctx.formattedMessage,
55
+ meta: ctx.meta
56
+ });
57
+ }
46
58
 
47
- // src/utils/replace-values.ts
59
+ // src/pipeline/hooks/format.ts
60
+ var format = rura.rura.createHook(
61
+ "format",
62
+ (ctx) => {
63
+ const { config, rawMessage } = ctx;
64
+ const { formatHandler } = config.handlers || {};
65
+ if (!formatHandler || rawMessage === void 0) return;
66
+ ctx.formattedMessage = formatHandler(
67
+ makeHandlerContext(ctx)
68
+ );
69
+ },
70
+ 500
71
+ );
72
+
73
+ // src/translators/shared/utils/replace-values.ts
48
74
  var replaceValues = (message, params) => {
49
75
  if (!params || typeof params !== "object" || Object.keys(params).length === 0) {
50
76
  return message;
@@ -63,63 +89,113 @@ var replaceValues = (message, params) => {
63
89
  return replaced;
64
90
  };
65
91
 
66
- // src/translator-methods/translate/translate.ts
67
- var translate = ({
68
- messagesRef,
69
- localeRef,
70
- isLoadingRef,
71
- translateConfig,
72
- key,
73
- replacements
74
- }) => {
75
- const messages = messagesRef.current;
76
- const locale = localeRef.current;
77
- const isLoading = isLoadingRef.current;
78
- const { fallbackLocales, loadingMessage, placeholder, handlers } = translateConfig;
79
- const { formatHandler, loadingHandler, missingHandler } = handlers || {};
80
- const candidateLocales = resolveCandidateLocales(locale, fallbackLocales);
81
- const message = findMessageInLocales({ messages, candidateLocales, key });
82
- if (isLoading && (loadingHandler || loadingMessage)) {
83
- if (loadingHandler)
84
- return loadingHandler({ key, locale, replacements });
85
- if (loadingMessage) return loadingMessage;
86
- }
87
- if (message === void 0) {
88
- if (missingHandler)
89
- return missingHandler({ key, locale, replacements });
90
- if (placeholder) return placeholder;
91
- return key;
92
- }
93
- if (formatHandler) {
94
- return formatHandler({ message, key, locale, replacements });
95
- }
96
- return replacements ? replaceValues(message, replacements) : message;
92
+ // src/pipeline/hooks/interpolate.ts
93
+ var interpolate = rura.rura.createHook(
94
+ "interpolate",
95
+ (ctx) => {
96
+ const { rawMessage, formattedMessage, replacements } = ctx;
97
+ const message = formattedMessage ?? rawMessage;
98
+ if (typeof message !== "string" || !replacements) {
99
+ ctx.finalMessage = message;
100
+ return;
101
+ }
102
+ ctx.finalMessage = replaceValues(message, replacements);
103
+ },
104
+ 600
105
+ );
106
+ var loading = rura.rura.createHook(
107
+ "loading",
108
+ (ctx) => {
109
+ const { config, isLoading } = ctx;
110
+ if (!isLoading) return;
111
+ const { loadingHandler } = config.handlers || {};
112
+ if (loadingHandler) {
113
+ return {
114
+ early: true,
115
+ output: loadingHandler(makeHandlerContext(ctx))
116
+ };
117
+ }
118
+ const { loadingMessage } = config;
119
+ if (loadingMessage) {
120
+ return { early: true, output: loadingMessage };
121
+ }
122
+ },
123
+ 300
124
+ );
125
+ var missing = rura.rura.createHook(
126
+ "missing",
127
+ (ctx) => {
128
+ const { config, key, rawMessage } = ctx;
129
+ if (rawMessage !== void 0) return;
130
+ const { missingHandler } = config.handlers || {};
131
+ if (missingHandler) {
132
+ return {
133
+ early: true,
134
+ output: missingHandler(makeHandlerContext(ctx))
135
+ };
136
+ }
137
+ const { placeholder } = config;
138
+ if (placeholder) {
139
+ return { early: true, output: placeholder };
140
+ }
141
+ return { early: true, output: key };
142
+ },
143
+ 400
144
+ );
145
+
146
+ // src/translators/shared/utils/resolve-candidate-locales.ts
147
+ var resolveCandidateLocales = (locale, fallbackLocalesMap) => {
148
+ const fallbacks = fallbackLocalesMap?.[locale] || [];
149
+ const filteredFallbacks = fallbacks.filter((l) => l !== locale);
150
+ return [locale, ...filteredFallbacks];
97
151
  };
98
152
 
153
+ // src/pipeline/hooks/resolve-locales.ts
154
+ var resolveLocales = rura.rura.createHook(
155
+ "resolveLocales",
156
+ (ctx) => {
157
+ ctx.candidateLocales = resolveCandidateLocales(
158
+ ctx.locale,
159
+ ctx.config.fallbackLocales
160
+ );
161
+ },
162
+ 100
163
+ );
164
+
165
+ // src/pipeline/index.ts
166
+ var DEFAULT_HOOKS = [
167
+ resolveLocales,
168
+ findMessage,
169
+ loading,
170
+ missing,
171
+ format,
172
+ interpolate
173
+ ];
174
+
99
175
  // src/translators/base-translator/base-translator.ts
100
176
  var BaseTranslator = class {
101
177
  /** Current messages for translation */
102
- messagesRef;
178
+ _messages;
103
179
  /** Current active locale */
104
- localeRef;
180
+ _locale;
105
181
  /** Current loading state */
106
- isLoadingRef;
182
+ _isLoading;
107
183
  constructor(options) {
108
- this.messagesRef = { current: options.messages ?? {} };
109
- this.localeRef = { current: options.locale };
110
- this.isLoadingRef = { current: options.isLoading ?? false };
184
+ this._messages = options.messages ?? {};
185
+ this._locale = options.locale;
186
+ this._isLoading = options.isLoading ?? false;
111
187
  }
112
188
  /** Get messages. */
113
189
  get messages() {
114
- return this.messagesRef.current;
190
+ return this._messages;
115
191
  }
116
192
  /** Get the current active locale. */
117
193
  get locale() {
118
- return this.localeRef.current;
194
+ return this._locale;
119
195
  }
120
196
  /** Get the current loading state. */
121
197
  get isLoading() {
122
- return this.isLoadingRef.current;
198
+ return this._isLoading;
123
199
  }
124
200
  /**
125
201
  * Replace messages with new ones.
@@ -128,7 +204,7 @@ var BaseTranslator = class {
128
204
  * The type cast bypasses TypeScript restrictions on dynamic messages.
129
205
  */
130
206
  setMessages(messages) {
131
- this.messagesRef.current = messages;
207
+ this._messages = messages;
132
208
  }
133
209
  /**
134
210
  * Set the active locale.
@@ -136,30 +212,74 @@ var BaseTranslator = class {
136
212
  * - Note: Unlike `setMessages`, the locale structure cannot be changed at runtime.
137
213
  */
138
214
  setLocale(newLocale) {
139
- this.localeRef.current = newLocale;
215
+ this._locale = newLocale;
140
216
  }
141
217
  /** Set the loading state. */
142
218
  setLoading(state) {
143
- this.isLoadingRef.current = state;
219
+ this._isLoading = state;
144
220
  }
145
221
  };
146
222
 
223
+ // src/translators/shared/has-key.ts
224
+ var hasKey = ({
225
+ messages,
226
+ locale,
227
+ key,
228
+ targetLocale
229
+ }) => {
230
+ const candidateLocales = resolveCandidateLocales(targetLocale || locale);
231
+ const message = findMessageInLocales({
232
+ messages,
233
+ candidateLocales,
234
+ key
235
+ });
236
+ return !!message;
237
+ };
238
+ function translate(options) {
239
+ const context = {
240
+ ...options,
241
+ config: options.translateConfig,
242
+ candidateLocales: [],
243
+ meta: {}
244
+ };
245
+ const { early, ctx, output } = rura.rura.run(context, options.hooks);
246
+ if (early === true) return output;
247
+ return ctx.finalMessage;
248
+ }
249
+
147
250
  // src/translators/core-translator/core-translator.ts
148
251
  var CoreTranslator = class extends BaseTranslator {
149
- options;
252
+ /** User-provided options including messages, locale, and config. */
253
+ translateConfig;
254
+ /** Active pipeline hooks applied during translation. */
255
+ hooks = [...DEFAULT_HOOKS];
150
256
  constructor(options) {
151
- super({
152
- locale: options.locale,
153
- messages: options.messages,
154
- isLoading: options.isLoading
155
- });
156
- this.options = options;
257
+ const { locale, messages, isLoading, plugins, ...translateConfig } = options;
258
+ super({ locale, messages, isLoading });
259
+ this.translateConfig = translateConfig;
260
+ if (plugins) {
261
+ for (const plugin of plugins) this.use(plugin);
262
+ }
263
+ this.sortHooks();
264
+ }
265
+ /** Sort hooks by order value (lower runs earlier). */
266
+ sortHooks() {
267
+ this.hooks.sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
268
+ }
269
+ /** Register a plugin or a raw pipeline hook. */
270
+ use(plugin) {
271
+ if ("run" in plugin) this.hooks.push(plugin);
272
+ else if ("hook" in plugin && plugin.hook) {
273
+ const hooks = Array.isArray(plugin.hook) ? plugin.hook : [plugin.hook];
274
+ this.hooks.push(...hooks);
275
+ }
276
+ this.sortHooks();
157
277
  }
158
278
  /** Check if a key exists in the specified locale or current locale. */
159
279
  hasKey = (key, targetLocale) => {
160
280
  return hasKey({
161
- messagesRef: this.messagesRef,
162
- localeRef: this.localeRef,
281
+ messages: this._messages,
282
+ locale: this._locale,
163
283
  key,
164
284
  targetLocale
165
285
  });
@@ -167,17 +287,18 @@ var CoreTranslator = class extends BaseTranslator {
167
287
  /** Get the translated message for a key, with optional replacements. */
168
288
  t = (key, replacements) => {
169
289
  return translate({
170
- messagesRef: this.messagesRef,
171
- localeRef: this.localeRef,
172
- isLoadingRef: this.isLoadingRef,
173
- translateConfig: this.options,
290
+ hooks: this.hooks,
291
+ messages: this._messages,
292
+ locale: this._locale,
293
+ isLoading: this._isLoading,
294
+ translateConfig: this.translateConfig,
174
295
  key,
175
296
  replacements
176
297
  });
177
298
  };
178
299
  };
179
300
 
180
- // src/utils/get-full-key.ts
301
+ // src/translators/scope-translator/utils/get-full-key.ts
181
302
  var getFullKey = (preKey = "", key = "") => {
182
303
  if (!preKey) return key;
183
304
  if (!key) return preKey;
@@ -195,8 +316,8 @@ var ScopeTranslator = class extends CoreTranslator {
195
316
  hasKey: (key, targetLocale) => {
196
317
  const fullKey = getFullKey(preKey, key);
197
318
  return hasKey({
198
- messagesRef: this.messagesRef,
199
- localeRef: this.localeRef,
319
+ messages: this._messages,
320
+ locale: this._locale,
200
321
  key: fullKey,
201
322
  targetLocale
202
323
  });
@@ -204,10 +325,11 @@ var ScopeTranslator = class extends CoreTranslator {
204
325
  t: (key, replacements) => {
205
326
  const fullKey = getFullKey(preKey, key);
206
327
  return translate({
207
- messagesRef: this.messagesRef,
208
- localeRef: this.localeRef,
209
- isLoadingRef: this.isLoadingRef,
210
- translateConfig: this.options,
328
+ hooks: this.hooks,
329
+ messages: this._messages,
330
+ locale: this._locale,
331
+ isLoading: this._isLoading,
332
+ translateConfig: this.translateConfig,
211
333
  key: fullKey,
212
334
  replacements
213
335
  });
@@ -216,5 +338,4 @@ var ScopeTranslator = class extends CoreTranslator {
216
338
  }
217
339
  };
218
340
 
219
- exports.ScopeTranslator = ScopeTranslator;
220
341
  exports.Translator = ScopeTranslator;