@form-engine-ts/translator-cache 2.6.0 → 2.8.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/README.md CHANGED
@@ -12,13 +12,18 @@ pnpm add @form-engine-ts/core @form-engine-ts/translator-cache
12
12
  ## Usage
13
13
 
14
14
  ```ts
15
- import { withTranslationCache } from "@form-engine-ts/translator-cache";
15
+ import { createMemoryTranslationCache, withTranslationCache } from "@form-engine-ts/translator-cache";
16
16
 
17
- const translator = withTranslationCache(baseTranslator, cacheStorage, {
17
+ const cache = createMemoryTranslationCache({ maxEntries: 500, ttlMs: 5 * 60 * 1000 });
18
+ const translator = withTranslationCache(baseTranslator, cache, {
18
19
  ttlMs: 60 * 60 * 1000,
19
- keyPrefix: "survey-translations"
20
+ keyPrefix: "survey-translations",
21
+ adapterName: "google-v3"
20
22
  });
21
23
  ```
22
24
 
23
- Keys isolate source locale, target locale, and a deterministic UTF-8 hash of the source text. Batch misses are deduplicated,
24
- translated once in source order, cached, and restored to their original positions.
25
+ Keys isolate adapter name, source locale, target locale, and a deterministic UTF-8 hash of the source text. Batch misses
26
+ are deduplicated, translated once in source order, cached, and restored to their original positions. The built-in memory
27
+ cache applies both TTL expiration and bounded LRU eviction and exposes `size`, `evictionCount`, and `clear()` diagnostics.
28
+ Use `variant` to isolate glossary/model/configuration revisions, or `buildKey` for complete key control.
29
+ `onStatsReport` receives cumulative real cache hits, misses, evictions, and current size after successful translations.
package/dist/index.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ createMemoryTranslationCache: () => createMemoryTranslationCache,
23
24
  hashTranslationText: () => hashTranslationText,
24
25
  withTranslationCache: () => withTranslationCache
25
26
  });
@@ -40,6 +41,60 @@ function requireLocale(value, name) {
40
41
  if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${name} must not be empty.`);
41
42
  return value.trim();
42
43
  }
44
+ function nonNegativeDuration(value, fallback, name) {
45
+ const resolved = value ?? fallback;
46
+ if (!Number.isSafeInteger(resolved) || resolved < 0) {
47
+ throw new TypeError(`${name} must be a non-negative safe integer.`);
48
+ }
49
+ return resolved;
50
+ }
51
+ function createMemoryTranslationCache(options = {}) {
52
+ const maxEntries = options.maxEntries ?? 500;
53
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
54
+ throw new TypeError("maxEntries must be a positive safe integer.");
55
+ }
56
+ const defaultTtlMs = nonNegativeDuration(options.ttlMs, 5 * 60 * 1e3, "ttlMs");
57
+ const now = options.now ?? Date.now;
58
+ const entries = /* @__PURE__ */ new Map();
59
+ let evictionCount = 0;
60
+ const evictExpired = (key, entry) => {
61
+ if (entry.expiresAt > now()) return false;
62
+ entries.delete(key);
63
+ evictionCount += 1;
64
+ return true;
65
+ };
66
+ return {
67
+ get size() {
68
+ for (const [key, entry] of entries) evictExpired(key, entry);
69
+ return entries.size;
70
+ },
71
+ get evictionCount() {
72
+ return evictionCount;
73
+ },
74
+ get(key) {
75
+ const entry = entries.get(key);
76
+ if (entry === void 0 || evictExpired(key, entry)) return void 0;
77
+ entries.delete(key);
78
+ entries.set(key, entry);
79
+ return entry.value;
80
+ },
81
+ set(key, value, ttlMs) {
82
+ const resolvedTtlMs = nonNegativeDuration(ttlMs, defaultTtlMs, "ttlMs");
83
+ if (!entries.has(key) && entries.size >= maxEntries) {
84
+ const leastRecentlyUsed = entries.keys().next().value;
85
+ if (leastRecentlyUsed !== void 0) {
86
+ entries.delete(leastRecentlyUsed);
87
+ evictionCount += 1;
88
+ }
89
+ }
90
+ entries.delete(key);
91
+ entries.set(key, { value, expiresAt: now() + resolvedTtlMs });
92
+ },
93
+ clear() {
94
+ entries.clear();
95
+ }
96
+ };
97
+ }
43
98
  function withTranslationCache(baseAdapter, cache, options = {}) {
44
99
  if (typeof baseAdapter?.translateBatch !== "function") throw new TypeError("baseAdapter.translateBatch is required.");
45
100
  if (typeof cache?.get !== "function" || typeof cache.set !== "function") {
@@ -50,14 +105,46 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
50
105
  }
51
106
  const prefix = options.keyPrefix ?? "form-engine-ts";
52
107
  if (prefix.trim().length === 0) throw new TypeError("keyPrefix must not be empty.");
108
+ const adapterName = options.adapterName ?? "anonymous";
109
+ if (adapterName.trim().length === 0) throw new TypeError("adapterName must not be empty.");
110
+ if (options.variant !== void 0 && options.variant.trim().length === 0) {
111
+ throw new TypeError("variant must not be empty.");
112
+ }
113
+ if (options.buildKey !== void 0 && typeof options.buildKey !== "function") {
114
+ throw new TypeError("buildKey must be a function.");
115
+ }
116
+ const initialEvictionCount = cache.evictionCount ?? 0;
117
+ let hits = 0;
118
+ let misses = 0;
119
+ const reportStats = () => {
120
+ const size = cache.size ?? 0;
121
+ options.onStatsReport?.({
122
+ hits,
123
+ misses,
124
+ evictions: Math.max(0, (cache.evictionCount ?? initialEvictionCount) - initialEvictionCount),
125
+ size
126
+ });
127
+ };
53
128
  const translateBatch = async (texts, targetLocale, sourceLocale) => {
54
129
  if (!Array.isArray(texts) || texts.some((text) => typeof text !== "string")) {
55
130
  throw new TypeError("texts must be an array of strings.");
56
131
  }
57
132
  const target = requireLocale(targetLocale, "targetLocale");
58
133
  const source = sourceLocale === void 0 ? "auto" : requireLocale(sourceLocale, "sourceLocale");
59
- const keys = texts.map((text) => `${prefix}:${source}:${target}:${hashTranslationText(text)}`);
134
+ const keys = texts.map((text) => {
135
+ const context = {
136
+ sourceText: text,
137
+ sourceLocale: source,
138
+ targetLocale: target,
139
+ ...options.variant === void 0 ? {} : { variant: options.variant }
140
+ };
141
+ const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${options.variant === void 0 ? "" : `${options.variant}:`}${source}:${target}:${hashTranslationText(text)}`;
142
+ if (typeof key !== "string" || key.length === 0) throw new TypeError("Translation cache key must not be empty.");
143
+ return key;
144
+ });
60
145
  const cached = await Promise.all(keys.map((key) => cache.get(key)));
146
+ hits += cached.filter((value) => value !== void 0).length;
147
+ misses += cached.filter((value) => value === void 0).length;
61
148
  const missingByKey = /* @__PURE__ */ new Map();
62
149
  for (let index = 0; index < texts.length; index += 1) {
63
150
  if (cached[index] !== void 0) continue;
@@ -87,10 +174,12 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
87
174
  })
88
175
  );
89
176
  }
90
- return cached.map((value) => {
177
+ const result = cached.map((value) => {
91
178
  if (value === void 0) throw new Error("Translation cache result is unavailable.");
92
179
  return value;
93
180
  });
181
+ reportStats();
182
+ return result;
94
183
  };
95
184
  return {
96
185
  async translateText(text, targetLocale, sourceLocale) {
@@ -104,6 +193,7 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
104
193
  }
105
194
  // Annotate the CommonJS export names for ESM import in node:
106
195
  0 && (module.exports = {
196
+ createMemoryTranslationCache,
107
197
  hashTranslationText,
108
198
  withTranslationCache
109
199
  });
package/dist/index.d.cts CHANGED
@@ -1,14 +1,43 @@
1
1
  import { AsyncTranslationAdapter } from '@form-engine-ts/core';
2
2
 
3
3
  interface TranslationCacheStorage {
4
+ readonly size?: number;
5
+ readonly evictionCount?: number;
4
6
  get(key: string): Promise<string | undefined> | string | undefined;
5
7
  set(key: string, value: string, ttlMs?: number): Promise<void> | void;
6
8
  }
9
+ interface TranslationCacheKeyContext {
10
+ readonly sourceText: string;
11
+ readonly sourceLocale: string;
12
+ readonly targetLocale: string;
13
+ readonly variant?: string;
14
+ }
15
+ interface TranslationCacheStats {
16
+ readonly hits: number;
17
+ readonly misses: number;
18
+ readonly evictions: number;
19
+ readonly size: number;
20
+ }
7
21
  interface TranslationCacheOptions {
8
22
  readonly ttlMs?: number;
9
23
  readonly keyPrefix?: string;
24
+ readonly adapterName?: string;
25
+ readonly variant?: string;
26
+ readonly buildKey?: (context: TranslationCacheKeyContext) => string;
27
+ readonly onStatsReport?: (stats: TranslationCacheStats) => void;
28
+ }
29
+ interface MemoryTranslationCacheOptions {
30
+ readonly maxEntries?: number;
31
+ readonly ttlMs?: number;
32
+ readonly now?: () => number;
33
+ }
34
+ interface MemoryTranslationCache extends TranslationCacheStorage {
35
+ readonly size: number;
36
+ readonly evictionCount: number;
37
+ clear(): void;
10
38
  }
11
39
  declare function hashTranslationText(text: string): string;
40
+ declare function createMemoryTranslationCache(options?: MemoryTranslationCacheOptions): MemoryTranslationCache;
12
41
  declare function withTranslationCache(baseAdapter: AsyncTranslationAdapter, cache: TranslationCacheStorage, options?: TranslationCacheOptions): AsyncTranslationAdapter;
13
42
 
14
- export { type TranslationCacheOptions, type TranslationCacheStorage, hashTranslationText, withTranslationCache };
43
+ export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheKeyContext, type TranslationCacheOptions, type TranslationCacheStats, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
package/dist/index.d.ts CHANGED
@@ -1,14 +1,43 @@
1
1
  import { AsyncTranslationAdapter } from '@form-engine-ts/core';
2
2
 
3
3
  interface TranslationCacheStorage {
4
+ readonly size?: number;
5
+ readonly evictionCount?: number;
4
6
  get(key: string): Promise<string | undefined> | string | undefined;
5
7
  set(key: string, value: string, ttlMs?: number): Promise<void> | void;
6
8
  }
9
+ interface TranslationCacheKeyContext {
10
+ readonly sourceText: string;
11
+ readonly sourceLocale: string;
12
+ readonly targetLocale: string;
13
+ readonly variant?: string;
14
+ }
15
+ interface TranslationCacheStats {
16
+ readonly hits: number;
17
+ readonly misses: number;
18
+ readonly evictions: number;
19
+ readonly size: number;
20
+ }
7
21
  interface TranslationCacheOptions {
8
22
  readonly ttlMs?: number;
9
23
  readonly keyPrefix?: string;
24
+ readonly adapterName?: string;
25
+ readonly variant?: string;
26
+ readonly buildKey?: (context: TranslationCacheKeyContext) => string;
27
+ readonly onStatsReport?: (stats: TranslationCacheStats) => void;
28
+ }
29
+ interface MemoryTranslationCacheOptions {
30
+ readonly maxEntries?: number;
31
+ readonly ttlMs?: number;
32
+ readonly now?: () => number;
33
+ }
34
+ interface MemoryTranslationCache extends TranslationCacheStorage {
35
+ readonly size: number;
36
+ readonly evictionCount: number;
37
+ clear(): void;
10
38
  }
11
39
  declare function hashTranslationText(text: string): string;
40
+ declare function createMemoryTranslationCache(options?: MemoryTranslationCacheOptions): MemoryTranslationCache;
12
41
  declare function withTranslationCache(baseAdapter: AsyncTranslationAdapter, cache: TranslationCacheStorage, options?: TranslationCacheOptions): AsyncTranslationAdapter;
13
42
 
14
- export { type TranslationCacheOptions, type TranslationCacheStorage, hashTranslationText, withTranslationCache };
43
+ export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheKeyContext, type TranslationCacheOptions, type TranslationCacheStats, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
package/dist/index.js CHANGED
@@ -15,6 +15,60 @@ function requireLocale(value, name) {
15
15
  if (typeof value !== "string" || value.trim().length === 0) throw new TypeError(`${name} must not be empty.`);
16
16
  return value.trim();
17
17
  }
18
+ function nonNegativeDuration(value, fallback, name) {
19
+ const resolved = value ?? fallback;
20
+ if (!Number.isSafeInteger(resolved) || resolved < 0) {
21
+ throw new TypeError(`${name} must be a non-negative safe integer.`);
22
+ }
23
+ return resolved;
24
+ }
25
+ function createMemoryTranslationCache(options = {}) {
26
+ const maxEntries = options.maxEntries ?? 500;
27
+ if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
28
+ throw new TypeError("maxEntries must be a positive safe integer.");
29
+ }
30
+ const defaultTtlMs = nonNegativeDuration(options.ttlMs, 5 * 60 * 1e3, "ttlMs");
31
+ const now = options.now ?? Date.now;
32
+ const entries = /* @__PURE__ */ new Map();
33
+ let evictionCount = 0;
34
+ const evictExpired = (key, entry) => {
35
+ if (entry.expiresAt > now()) return false;
36
+ entries.delete(key);
37
+ evictionCount += 1;
38
+ return true;
39
+ };
40
+ return {
41
+ get size() {
42
+ for (const [key, entry] of entries) evictExpired(key, entry);
43
+ return entries.size;
44
+ },
45
+ get evictionCount() {
46
+ return evictionCount;
47
+ },
48
+ get(key) {
49
+ const entry = entries.get(key);
50
+ if (entry === void 0 || evictExpired(key, entry)) return void 0;
51
+ entries.delete(key);
52
+ entries.set(key, entry);
53
+ return entry.value;
54
+ },
55
+ set(key, value, ttlMs) {
56
+ const resolvedTtlMs = nonNegativeDuration(ttlMs, defaultTtlMs, "ttlMs");
57
+ if (!entries.has(key) && entries.size >= maxEntries) {
58
+ const leastRecentlyUsed = entries.keys().next().value;
59
+ if (leastRecentlyUsed !== void 0) {
60
+ entries.delete(leastRecentlyUsed);
61
+ evictionCount += 1;
62
+ }
63
+ }
64
+ entries.delete(key);
65
+ entries.set(key, { value, expiresAt: now() + resolvedTtlMs });
66
+ },
67
+ clear() {
68
+ entries.clear();
69
+ }
70
+ };
71
+ }
18
72
  function withTranslationCache(baseAdapter, cache, options = {}) {
19
73
  if (typeof baseAdapter?.translateBatch !== "function") throw new TypeError("baseAdapter.translateBatch is required.");
20
74
  if (typeof cache?.get !== "function" || typeof cache.set !== "function") {
@@ -25,14 +79,46 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
25
79
  }
26
80
  const prefix = options.keyPrefix ?? "form-engine-ts";
27
81
  if (prefix.trim().length === 0) throw new TypeError("keyPrefix must not be empty.");
82
+ const adapterName = options.adapterName ?? "anonymous";
83
+ if (adapterName.trim().length === 0) throw new TypeError("adapterName must not be empty.");
84
+ if (options.variant !== void 0 && options.variant.trim().length === 0) {
85
+ throw new TypeError("variant must not be empty.");
86
+ }
87
+ if (options.buildKey !== void 0 && typeof options.buildKey !== "function") {
88
+ throw new TypeError("buildKey must be a function.");
89
+ }
90
+ const initialEvictionCount = cache.evictionCount ?? 0;
91
+ let hits = 0;
92
+ let misses = 0;
93
+ const reportStats = () => {
94
+ const size = cache.size ?? 0;
95
+ options.onStatsReport?.({
96
+ hits,
97
+ misses,
98
+ evictions: Math.max(0, (cache.evictionCount ?? initialEvictionCount) - initialEvictionCount),
99
+ size
100
+ });
101
+ };
28
102
  const translateBatch = async (texts, targetLocale, sourceLocale) => {
29
103
  if (!Array.isArray(texts) || texts.some((text) => typeof text !== "string")) {
30
104
  throw new TypeError("texts must be an array of strings.");
31
105
  }
32
106
  const target = requireLocale(targetLocale, "targetLocale");
33
107
  const source = sourceLocale === void 0 ? "auto" : requireLocale(sourceLocale, "sourceLocale");
34
- const keys = texts.map((text) => `${prefix}:${source}:${target}:${hashTranslationText(text)}`);
108
+ const keys = texts.map((text) => {
109
+ const context = {
110
+ sourceText: text,
111
+ sourceLocale: source,
112
+ targetLocale: target,
113
+ ...options.variant === void 0 ? {} : { variant: options.variant }
114
+ };
115
+ const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${options.variant === void 0 ? "" : `${options.variant}:`}${source}:${target}:${hashTranslationText(text)}`;
116
+ if (typeof key !== "string" || key.length === 0) throw new TypeError("Translation cache key must not be empty.");
117
+ return key;
118
+ });
35
119
  const cached = await Promise.all(keys.map((key) => cache.get(key)));
120
+ hits += cached.filter((value) => value !== void 0).length;
121
+ misses += cached.filter((value) => value === void 0).length;
36
122
  const missingByKey = /* @__PURE__ */ new Map();
37
123
  for (let index = 0; index < texts.length; index += 1) {
38
124
  if (cached[index] !== void 0) continue;
@@ -62,10 +148,12 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
62
148
  })
63
149
  );
64
150
  }
65
- return cached.map((value) => {
151
+ const result = cached.map((value) => {
66
152
  if (value === void 0) throw new Error("Translation cache result is unavailable.");
67
153
  return value;
68
154
  });
155
+ reportStats();
156
+ return result;
69
157
  };
70
158
  return {
71
159
  async translateText(text, targetLocale, sourceLocale) {
@@ -78,6 +166,7 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
78
166
  };
79
167
  }
80
168
  export {
169
+ createMemoryTranslationCache,
81
170
  hashTranslationText,
82
171
  withTranslationCache
83
172
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/translator-cache",
3
- "version": "2.6.0",
3
+ "version": "2.8.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -38,7 +38,7 @@
38
38
  "typescript"
39
39
  ],
40
40
  "dependencies": {
41
- "@form-engine-ts/core": "2.6.0"
41
+ "@form-engine-ts/core": "2.8.0"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsup src/index.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",