intor-translator 1.0.15 → 1.1.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/dist/index.js CHANGED
@@ -1,116 +1,29 @@
1
- // src/cache/cache.ts
2
- var Cache = class {
3
- constructor(maxSize = 100, ttl = 1e3 * 60 * 5) {
4
- this.cache = /* @__PURE__ */ new Map();
5
- this.maxSize = maxSize;
6
- this.ttl = ttl;
7
- }
8
- // Clean up expired cache entries
9
- cleanUp() {
10
- const now = Date.now();
11
- this.cache.forEach((entry, key) => {
12
- if (now - entry.timestamp > this.ttl) {
13
- this.cache.delete(key);
14
- }
15
- });
16
- }
17
- // Get cache data
18
- get(key) {
19
- this.cleanUp();
20
- const entry = this.cache.get(key);
21
- if (entry) {
22
- entry.timestamp = Date.now();
23
- return entry.value;
24
- }
25
- return void 0;
26
- }
27
- // Set cache data
28
- set(key, value) {
29
- this.cleanUp();
30
- if (this.cache.size >= this.maxSize) {
31
- const oldestKey = this.cache.keys().next().value;
32
- if (oldestKey) this.cache.delete(oldestKey);
33
- }
34
- this.cache.set(key, { value, timestamp: Date.now() });
35
- }
36
- // Check if a key exists in the cache
37
- has(key) {
38
- this.cleanUp();
39
- return this.cache.has(key);
40
- }
41
- // Clear all cache
42
- clear() {
43
- this.cache.clear();
44
- }
45
- };
46
-
47
- // src/cache/message-key-cache.ts
48
- var MESSAGE_KEY_CACHE_MAX_SIZE = 100;
49
- var MESSAGE_KEY_CACHE_EXPIRES_TIME = 1e3 * 60 * 5;
50
- var messageKeyCache;
51
- var getMessageKeyCache = () => {
52
- if (typeof window !== "undefined" && !messageKeyCache) {
53
- messageKeyCache = new Cache(
54
- MESSAGE_KEY_CACHE_MAX_SIZE,
55
- MESSAGE_KEY_CACHE_EXPIRES_TIME
56
- );
57
- }
58
- return messageKeyCache;
59
- };
60
- var clearMessageKeyCache = () => {
61
- if (messageKeyCache) {
62
- messageKeyCache.clear();
63
- messageKeyCache = void 0;
64
- }
65
- };
66
-
67
- // src/utils/get-value-by-key.ts
68
- var getValueByKey = (locale, messages, key, useCache = true) => {
69
- const cache = getMessageKeyCache();
70
- useCache = Boolean(useCache && cache);
71
- const cacheKey = `${key}`;
72
- const currentLocale = cache?.get("locale");
73
- if (currentLocale !== locale) {
74
- cache?.clear();
75
- cache?.set("locale", locale);
76
- }
77
- if (useCache && cache?.has(cacheKey)) {
78
- return cache?.get(cacheKey);
79
- }
80
- const value = key.split(".").reduce((acc, key2) => {
81
- if (acc && typeof acc === "object" && key2 in acc) {
82
- return acc[key2];
83
- }
84
- return void 0;
85
- }, messages);
86
- if (useCache && value !== void 0) {
87
- cache?.set(cacheKey, value);
88
- }
89
- return value;
90
- };
91
-
92
1
  // src/utils/find-message-in-locales.ts
93
2
  var findMessageInLocales = ({
94
3
  messages,
95
- localesToTry,
4
+ candidateLocales,
96
5
  key
97
6
  }) => {
98
- for (const loc of localesToTry) {
99
- const localeMessages = messages[loc];
100
- if (!localeMessages) {
101
- continue;
102
- }
103
- const candidate = getValueByKey(loc, localeMessages, key);
104
- if (typeof candidate === "string") {
105
- return candidate;
7
+ for (const locale of candidateLocales) {
8
+ const localeMessages = messages[locale];
9
+ if (!localeMessages) continue;
10
+ let candidate = localeMessages;
11
+ const keys = key.split(".");
12
+ for (const k of keys) {
13
+ if (candidate && typeof candidate === "object" && k in candidate) {
14
+ candidate = candidate[k];
15
+ } else {
16
+ candidate = void 0;
17
+ break;
18
+ }
106
19
  }
20
+ if (typeof candidate === "string") return candidate;
107
21
  }
108
- return void 0;
109
22
  };
110
23
 
111
- // src/utils/resolve-locales-to-try.ts
112
- var resolveLocalesToTry = (locale, fallbackLocales) => {
113
- const fallbacks = fallbackLocales?.[locale] || [];
24
+ // src/utils/resolve-candidate-locales.ts
25
+ var resolveCandidateLocales = (locale, fallbackLocalesMap) => {
26
+ const fallbacks = fallbackLocalesMap?.[locale] || [];
114
27
  const filteredFallbacks = fallbacks.filter((l) => l !== locale);
115
28
  return [locale, ...filteredFallbacks];
116
29
  };
@@ -127,11 +40,9 @@ var hasKey = ({
127
40
  if (!messages) {
128
41
  throw new Error("[intor-translator] 'messages' is required");
129
42
  }
130
- if (!locale) {
131
- throw new Error("[intor-translator] 'locale' is required");
132
- }
133
- const localesToTry = resolveLocalesToTry(targetLocale || locale);
134
- return findMessageInLocales({ messages, localesToTry, key }) ? true : false;
43
+ const candidateLocales = resolveCandidateLocales(targetLocale || locale);
44
+ const message = findMessageInLocales({ messages, candidateLocales, key });
45
+ return !!message;
135
46
  };
136
47
 
137
48
  // src/utils/replace-values.ts
@@ -139,7 +50,7 @@ var replaceValues = (message, params) => {
139
50
  if (!params || typeof params !== "object" || Object.keys(params).length === 0) {
140
51
  return message;
141
52
  }
142
- const replaced = message.replace(/{([^}]+)}/g, (match, key) => {
53
+ const replaced = message.replaceAll(/{([^}]+)}/g, (match, key) => {
143
54
  const keys = key.split(".");
144
55
  let value = params;
145
56
  for (const k of keys) {
@@ -164,85 +75,61 @@ var translate = ({
164
75
  }) => {
165
76
  const messages = messagesRef.current;
166
77
  const locale = localeRef.current;
78
+ const isLoading = isLoadingRef.current;
167
79
  if (!messages) {
168
80
  throw new Error("[intor-translator] 'messages' is required");
169
81
  }
170
- if (!locale) {
171
- throw new Error("[intor-translator] 'locale' is required");
172
- }
173
- const isLoading = isLoadingRef.current;
174
- const {
175
- fallbackLocales,
176
- loadingMessage,
177
- placeholder,
178
- handlers = {}
179
- } = translateConfig;
180
- const { formatMessage, onLoading, onMissing } = handlers;
181
- const localesToTry = resolveLocalesToTry(locale, fallbackLocales);
182
- const message = findMessageInLocales({ messages, localesToTry, key });
183
- if (isLoading) {
184
- if (onLoading) {
185
- return onLoading({
186
- key,
187
- locale,
188
- replacements
189
- });
190
- }
191
- if (loadingMessage) {
192
- return loadingMessage;
193
- }
194
- }
195
- if (message === void 0 || message === null) {
196
- if (onMissing) {
197
- return onMissing({ key, locale, replacements });
198
- }
199
- if (placeholder !== void 0 && placeholder !== null) {
200
- return placeholder;
201
- }
82
+ const { fallbackLocales, loadingMessage, placeholder, handlers } = translateConfig;
83
+ const { formatHandler, loadingHandler, missingHandler } = handlers || {};
84
+ const candidateLocales = resolveCandidateLocales(locale, fallbackLocales);
85
+ const message = findMessageInLocales({ messages, candidateLocales, key });
86
+ if (isLoading && (loadingHandler || loadingMessage)) {
87
+ if (loadingHandler)
88
+ return loadingHandler({ key, locale, replacements });
89
+ if (loadingMessage) return loadingMessage;
90
+ }
91
+ if (message === void 0) {
92
+ if (missingHandler)
93
+ return missingHandler({ key, locale, replacements });
94
+ if (placeholder) return placeholder;
202
95
  return key;
203
96
  }
204
- if (formatMessage) {
205
- return formatMessage({ message, key, locale, replacements });
206
- } else {
207
- return replacements ? replaceValues(message, replacements) : message;
97
+ if (formatHandler) {
98
+ return formatHandler({ message, key, locale, replacements });
208
99
  }
100
+ return replacements ? replaceValues(message, replacements) : message;
209
101
  };
210
102
 
211
103
  // src/translators/base-translator/base-translator.ts
212
104
  var BaseTranslator = class {
213
105
  constructor(options) {
106
+ /** Current messages for translation, updatable at runtime */
214
107
  this.messagesRef = { current: void 0 };
215
- /** Check if a key exists in the specified locale or current locale. */
216
- this.hasKey = (key, targetLocale) => {
217
- return hasKey({
218
- messagesRef: this.messagesRef,
219
- localeRef: this.localeRef,
220
- key,
221
- targetLocale
222
- });
223
- };
224
108
  this.messagesRef = { current: options.messages };
225
109
  this.localeRef = { current: options.locale };
226
110
  }
227
- /** Get all message data. */
111
+ /** Get messages. */
228
112
  get messages() {
229
113
  return this.messagesRef.current;
230
114
  }
115
+ /** Get the current active locale. */
116
+ get locale() {
117
+ return this.localeRef.current;
118
+ }
231
119
  /**
232
120
  * Replace messages with new ones.
233
121
  *
234
- * Note: This allows runtime setting of messages even if M is inferred as `never` (uninitialized).
235
- * Type cast is used to bypass TypeScript restrictions on dynamic messages.
122
+ * - Note: This allows runtime setting of messages even if `M` is inferred as `never`.
123
+ * The type cast bypasses TypeScript restrictions on dynamic messages.
236
124
  */
237
125
  setMessages(messages) {
238
126
  this.messagesRef.current = messages;
239
- clearMessageKeyCache();
240
127
  }
241
- /** Get the current active locale. */
242
- get locale() {
243
- return this.localeRef.current;
244
- }
245
- /** Change the active locale. */
128
+ /**
129
+ * Set the active locale.
130
+ *
131
+ * - Note: Unlike `setMessages`, the locale structure cannot be changed at runtime.
132
+ */
246
133
  setLocale(newLocale) {
247
134
  this.localeRef.current = newLocale;
248
135
  }
@@ -251,8 +138,17 @@ var BaseTranslator = class {
251
138
  // src/translators/core-translator/core-translator.ts
252
139
  var CoreTranslator = class extends BaseTranslator {
253
140
  constructor(options) {
254
- super(options);
141
+ super({ locale: options.locale, messages: options.messages });
255
142
  this.isLoadingRef = { current: false };
143
+ /** Check if a key exists in the specified locale or current locale. */
144
+ this.hasKey = (key, targetLocale) => {
145
+ return hasKey({
146
+ messagesRef: this.messagesRef,
147
+ localeRef: this.localeRef,
148
+ key,
149
+ targetLocale
150
+ });
151
+ };
256
152
  this.t = (key, replacements) => {
257
153
  return translate({
258
154
  messagesRef: this.messagesRef,
@@ -277,12 +173,8 @@ var CoreTranslator = class extends BaseTranslator {
277
173
 
278
174
  // src/utils/get-full-key.ts
279
175
  var getFullKey = (preKey = "", key = "") => {
280
- if (!preKey) {
281
- return key;
282
- }
283
- if (!key) {
284
- return preKey;
285
- }
176
+ if (!preKey) return key;
177
+ if (!key) return preKey;
286
178
  return `${preKey}.${key}`;
287
179
  };
288
180
 
@@ -317,4 +209,4 @@ var ScopeTranslator = class extends CoreTranslator {
317
209
  }
318
210
  };
319
211
 
320
- export { ScopeTranslator as Translator };
212
+ export { ScopeTranslator, ScopeTranslator as Translator };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "intor-translator",
3
- "version": "1.0.15",
3
+ "version": "1.1.0",
4
4
  "description": "A type safe translator that knows what to say and how to handle the rest. Supports custom messages, rich replacements, and async loading.",
5
5
  "author": "Yiming Liao",
6
6
  "license": "MIT",
@@ -51,42 +51,47 @@
51
51
  "type": "module",
52
52
  "scripts": {
53
53
  "build": "tsup",
54
+ "prepublishOnly": "yarn build",
55
+ "test": "vitest",
56
+ "type": "tsc --noEmit",
54
57
  "lint": "eslint",
55
- "type-check": "tsc --noEmit",
56
- "test": "jest",
57
- "test:coverage": "jest --coverage && open coverage/lcov-report/index.html",
58
- "prepublishOnly": "yarn build"
59
- },
60
- "sideEffects": false,
61
- "engines": {
62
- "node": ">=16.0.0"
58
+ "lint:debug": "eslint --debug",
59
+ "knip": "knip --config .config/knip.config.ts"
63
60
  },
61
+ "dependencies": {},
64
62
  "devDependencies": {
65
- "@eslint/js": "^9.27.0",
63
+ "@eslint/eslintrc": "^3.3.1",
66
64
  "@testing-library/dom": "^10.4.0",
67
65
  "@testing-library/jest-dom": "^6.6.3",
68
66
  "@testing-library/react": "^16.3.0",
69
- "@types/jest": "^29.5.14",
67
+ "@types/node": "^24.10.1",
70
68
  "@types/react": "^19.1.4",
71
69
  "@types/react-dom": "^19.1.5",
72
- "eslint": "^9.27.0",
73
- "eslint-config-prettier": "^10.1.5",
74
- "eslint-plugin-import": "^2.31.0",
75
- "eslint-plugin-prettier": "^5.4.0",
76
- "eslint-plugin-unused-imports": "^4.1.4",
70
+ "@vitest/coverage-v8": "4.0.9",
71
+ "eslint": "^9.39.1",
72
+ "eslint-config-prettier": "^10.1.8",
73
+ "eslint-import-resolver-typescript": "^4.4.4",
74
+ "eslint-plugin-import": "^2.32.0",
75
+ "eslint-plugin-jsx-a11y": "^6.10.2",
76
+ "eslint-plugin-prettier": "^5.5.4",
77
+ "eslint-plugin-unicorn": "^62.0.0",
78
+ "eslint-plugin-unused-imports": "^4.3.0",
77
79
  "globals": "^16.1.0",
78
80
  "intl-messageformat": "^10.7.16",
79
- "jest": "^29.7.0",
80
- "jest-environment-jsdom": "^29.7.0",
81
- "jest-fetch-mock": "^3.0.3",
81
+ "knip": "^5.69.1",
82
82
  "next": "^15.3.2",
83
83
  "prettier": "^3.5.3",
84
84
  "react": "^19.1.0",
85
85
  "react-dom": "^19.1.0",
86
- "ts-jest": "^29.3.2",
87
86
  "ts-node": "^10.9.2",
87
+ "tsd": "^0.33.0",
88
88
  "tsup": "^8.4.0",
89
89
  "typescript": "^5.8.3",
90
- "typescript-eslint": "^8.32.1"
90
+ "typescript-eslint": "^8.46.4",
91
+ "vitest": "^4.0.9"
92
+ },
93
+ "sideEffects": false,
94
+ "engines": {
95
+ "node": ">=16.0.0"
91
96
  }
92
97
  }