@form-engine-ts/translator-cache 2.7.0 → 2.9.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 +4 -0
- package/dist/index.cjs +65 -4
- package/dist/index.d.cts +20 -1
- package/dist/index.d.ts +20 -1
- package/dist/index.js +65 -4
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -25,3 +25,7 @@ const translator = withTranslationCache(baseTranslator, cache, {
|
|
|
25
25
|
Keys isolate adapter name, source locale, target locale, and a deterministic UTF-8 hash of the source text. Batch misses
|
|
26
26
|
are deduplicated, translated once in source order, cached, and restored to their original positions. The built-in memory
|
|
27
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.
|
|
30
|
+
Cache backend failures default to `cacheErrorPolicy: "bypass"`: `onCacheError` is notified and translation continues
|
|
31
|
+
through the wrapped adapter. Use `cacheErrorPolicy: "throw"` when cache availability is mandatory.
|
package/dist/index.cjs
CHANGED
|
@@ -107,14 +107,73 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
|
|
|
107
107
|
if (prefix.trim().length === 0) throw new TypeError("keyPrefix must not be empty.");
|
|
108
108
|
const adapterName = options.adapterName ?? "anonymous";
|
|
109
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
|
+
if (options.cacheErrorPolicy !== void 0 && options.cacheErrorPolicy !== "bypass" && options.cacheErrorPolicy !== "throw") {
|
|
117
|
+
throw new TypeError('cacheErrorPolicy must be "bypass" or "throw".');
|
|
118
|
+
}
|
|
119
|
+
const cacheErrorPolicy = options.cacheErrorPolicy ?? "bypass";
|
|
120
|
+
const initialEvictionCount = cache.evictionCount ?? 0;
|
|
121
|
+
let hits = 0;
|
|
122
|
+
let misses = 0;
|
|
123
|
+
const handleCacheError = (cause, operation) => {
|
|
124
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
125
|
+
try {
|
|
126
|
+
options.onCacheError?.(error, operation);
|
|
127
|
+
} catch {
|
|
128
|
+
}
|
|
129
|
+
return error;
|
|
130
|
+
};
|
|
131
|
+
const getCached = async (key) => {
|
|
132
|
+
try {
|
|
133
|
+
return await cache.get(key);
|
|
134
|
+
} catch (cause) {
|
|
135
|
+
const error = handleCacheError(cause, "get");
|
|
136
|
+
if (cacheErrorPolicy === "throw") throw error;
|
|
137
|
+
return void 0;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const setCached = async (key, value) => {
|
|
141
|
+
try {
|
|
142
|
+
await cache.set(key, value, options.ttlMs);
|
|
143
|
+
} catch (cause) {
|
|
144
|
+
const error = handleCacheError(cause, "set");
|
|
145
|
+
if (cacheErrorPolicy === "throw") throw error;
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
const reportStats = () => {
|
|
149
|
+
const size = cache.size ?? 0;
|
|
150
|
+
options.onStatsReport?.({
|
|
151
|
+
hits,
|
|
152
|
+
misses,
|
|
153
|
+
evictions: Math.max(0, (cache.evictionCount ?? initialEvictionCount) - initialEvictionCount),
|
|
154
|
+
size
|
|
155
|
+
});
|
|
156
|
+
};
|
|
110
157
|
const translateBatch = async (texts, targetLocale, sourceLocale) => {
|
|
111
158
|
if (!Array.isArray(texts) || texts.some((text) => typeof text !== "string")) {
|
|
112
159
|
throw new TypeError("texts must be an array of strings.");
|
|
113
160
|
}
|
|
114
161
|
const target = requireLocale(targetLocale, "targetLocale");
|
|
115
162
|
const source = sourceLocale === void 0 ? "auto" : requireLocale(sourceLocale, "sourceLocale");
|
|
116
|
-
const keys = texts.map((text) =>
|
|
117
|
-
|
|
163
|
+
const keys = texts.map((text) => {
|
|
164
|
+
const context = {
|
|
165
|
+
sourceText: text,
|
|
166
|
+
sourceLocale: source,
|
|
167
|
+
targetLocale: target,
|
|
168
|
+
...options.variant === void 0 ? {} : { variant: options.variant }
|
|
169
|
+
};
|
|
170
|
+
const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${options.variant === void 0 ? "" : `${options.variant}:`}${source}:${target}:${hashTranslationText(text)}`;
|
|
171
|
+
if (typeof key !== "string" || key.length === 0) throw new TypeError("Translation cache key must not be empty.");
|
|
172
|
+
return key;
|
|
173
|
+
});
|
|
174
|
+
const cached = await Promise.all(keys.map(getCached));
|
|
175
|
+
hits += cached.filter((value) => value !== void 0).length;
|
|
176
|
+
misses += cached.filter((value) => value === void 0).length;
|
|
118
177
|
const missingByKey = /* @__PURE__ */ new Map();
|
|
119
178
|
for (let index = 0; index < texts.length; index += 1) {
|
|
120
179
|
if (cached[index] !== void 0) continue;
|
|
@@ -139,15 +198,17 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
|
|
|
139
198
|
missing.map(async ([key, value], translatedIndex) => {
|
|
140
199
|
const translation = translated[translatedIndex];
|
|
141
200
|
if (translation === void 0) throw new Error("Translation adapter result is unavailable.");
|
|
142
|
-
await
|
|
201
|
+
await setCached(key, translation);
|
|
143
202
|
for (const index of value.indices) cached[index] = translation;
|
|
144
203
|
})
|
|
145
204
|
);
|
|
146
205
|
}
|
|
147
|
-
|
|
206
|
+
const result = cached.map((value) => {
|
|
148
207
|
if (value === void 0) throw new Error("Translation cache result is unavailable.");
|
|
149
208
|
return value;
|
|
150
209
|
});
|
|
210
|
+
reportStats();
|
|
211
|
+
return result;
|
|
151
212
|
};
|
|
152
213
|
return {
|
|
153
214
|
async translateText(text, targetLocale, sourceLocale) {
|
package/dist/index.d.cts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
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;
|
|
10
24
|
readonly adapterName?: string;
|
|
25
|
+
readonly variant?: string;
|
|
26
|
+
readonly buildKey?: (context: TranslationCacheKeyContext) => string;
|
|
27
|
+
readonly onStatsReport?: (stats: TranslationCacheStats) => void;
|
|
28
|
+
readonly cacheErrorPolicy?: "bypass" | "throw";
|
|
29
|
+
readonly onCacheError?: (error: Error, operation: "get" | "set") => void;
|
|
11
30
|
}
|
|
12
31
|
interface MemoryTranslationCacheOptions {
|
|
13
32
|
readonly maxEntries?: number;
|
|
@@ -23,4 +42,4 @@ declare function hashTranslationText(text: string): string;
|
|
|
23
42
|
declare function createMemoryTranslationCache(options?: MemoryTranslationCacheOptions): MemoryTranslationCache;
|
|
24
43
|
declare function withTranslationCache(baseAdapter: AsyncTranslationAdapter, cache: TranslationCacheStorage, options?: TranslationCacheOptions): AsyncTranslationAdapter;
|
|
25
44
|
|
|
26
|
-
export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheOptions, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
|
|
45
|
+
export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheKeyContext, type TranslationCacheOptions, type TranslationCacheStats, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,13 +1,32 @@
|
|
|
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;
|
|
10
24
|
readonly adapterName?: string;
|
|
25
|
+
readonly variant?: string;
|
|
26
|
+
readonly buildKey?: (context: TranslationCacheKeyContext) => string;
|
|
27
|
+
readonly onStatsReport?: (stats: TranslationCacheStats) => void;
|
|
28
|
+
readonly cacheErrorPolicy?: "bypass" | "throw";
|
|
29
|
+
readonly onCacheError?: (error: Error, operation: "get" | "set") => void;
|
|
11
30
|
}
|
|
12
31
|
interface MemoryTranslationCacheOptions {
|
|
13
32
|
readonly maxEntries?: number;
|
|
@@ -23,4 +42,4 @@ declare function hashTranslationText(text: string): string;
|
|
|
23
42
|
declare function createMemoryTranslationCache(options?: MemoryTranslationCacheOptions): MemoryTranslationCache;
|
|
24
43
|
declare function withTranslationCache(baseAdapter: AsyncTranslationAdapter, cache: TranslationCacheStorage, options?: TranslationCacheOptions): AsyncTranslationAdapter;
|
|
25
44
|
|
|
26
|
-
export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheOptions, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
|
|
45
|
+
export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheKeyContext, type TranslationCacheOptions, type TranslationCacheStats, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
|
package/dist/index.js
CHANGED
|
@@ -81,14 +81,73 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
|
|
|
81
81
|
if (prefix.trim().length === 0) throw new TypeError("keyPrefix must not be empty.");
|
|
82
82
|
const adapterName = options.adapterName ?? "anonymous";
|
|
83
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
|
+
if (options.cacheErrorPolicy !== void 0 && options.cacheErrorPolicy !== "bypass" && options.cacheErrorPolicy !== "throw") {
|
|
91
|
+
throw new TypeError('cacheErrorPolicy must be "bypass" or "throw".');
|
|
92
|
+
}
|
|
93
|
+
const cacheErrorPolicy = options.cacheErrorPolicy ?? "bypass";
|
|
94
|
+
const initialEvictionCount = cache.evictionCount ?? 0;
|
|
95
|
+
let hits = 0;
|
|
96
|
+
let misses = 0;
|
|
97
|
+
const handleCacheError = (cause, operation) => {
|
|
98
|
+
const error = cause instanceof Error ? cause : new Error(String(cause));
|
|
99
|
+
try {
|
|
100
|
+
options.onCacheError?.(error, operation);
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
return error;
|
|
104
|
+
};
|
|
105
|
+
const getCached = async (key) => {
|
|
106
|
+
try {
|
|
107
|
+
return await cache.get(key);
|
|
108
|
+
} catch (cause) {
|
|
109
|
+
const error = handleCacheError(cause, "get");
|
|
110
|
+
if (cacheErrorPolicy === "throw") throw error;
|
|
111
|
+
return void 0;
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
const setCached = async (key, value) => {
|
|
115
|
+
try {
|
|
116
|
+
await cache.set(key, value, options.ttlMs);
|
|
117
|
+
} catch (cause) {
|
|
118
|
+
const error = handleCacheError(cause, "set");
|
|
119
|
+
if (cacheErrorPolicy === "throw") throw error;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
const reportStats = () => {
|
|
123
|
+
const size = cache.size ?? 0;
|
|
124
|
+
options.onStatsReport?.({
|
|
125
|
+
hits,
|
|
126
|
+
misses,
|
|
127
|
+
evictions: Math.max(0, (cache.evictionCount ?? initialEvictionCount) - initialEvictionCount),
|
|
128
|
+
size
|
|
129
|
+
});
|
|
130
|
+
};
|
|
84
131
|
const translateBatch = async (texts, targetLocale, sourceLocale) => {
|
|
85
132
|
if (!Array.isArray(texts) || texts.some((text) => typeof text !== "string")) {
|
|
86
133
|
throw new TypeError("texts must be an array of strings.");
|
|
87
134
|
}
|
|
88
135
|
const target = requireLocale(targetLocale, "targetLocale");
|
|
89
136
|
const source = sourceLocale === void 0 ? "auto" : requireLocale(sourceLocale, "sourceLocale");
|
|
90
|
-
const keys = texts.map((text) =>
|
|
91
|
-
|
|
137
|
+
const keys = texts.map((text) => {
|
|
138
|
+
const context = {
|
|
139
|
+
sourceText: text,
|
|
140
|
+
sourceLocale: source,
|
|
141
|
+
targetLocale: target,
|
|
142
|
+
...options.variant === void 0 ? {} : { variant: options.variant }
|
|
143
|
+
};
|
|
144
|
+
const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${options.variant === void 0 ? "" : `${options.variant}:`}${source}:${target}:${hashTranslationText(text)}`;
|
|
145
|
+
if (typeof key !== "string" || key.length === 0) throw new TypeError("Translation cache key must not be empty.");
|
|
146
|
+
return key;
|
|
147
|
+
});
|
|
148
|
+
const cached = await Promise.all(keys.map(getCached));
|
|
149
|
+
hits += cached.filter((value) => value !== void 0).length;
|
|
150
|
+
misses += cached.filter((value) => value === void 0).length;
|
|
92
151
|
const missingByKey = /* @__PURE__ */ new Map();
|
|
93
152
|
for (let index = 0; index < texts.length; index += 1) {
|
|
94
153
|
if (cached[index] !== void 0) continue;
|
|
@@ -113,15 +172,17 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
|
|
|
113
172
|
missing.map(async ([key, value], translatedIndex) => {
|
|
114
173
|
const translation = translated[translatedIndex];
|
|
115
174
|
if (translation === void 0) throw new Error("Translation adapter result is unavailable.");
|
|
116
|
-
await
|
|
175
|
+
await setCached(key, translation);
|
|
117
176
|
for (const index of value.indices) cached[index] = translation;
|
|
118
177
|
})
|
|
119
178
|
);
|
|
120
179
|
}
|
|
121
|
-
|
|
180
|
+
const result = cached.map((value) => {
|
|
122
181
|
if (value === void 0) throw new Error("Translation cache result is unavailable.");
|
|
123
182
|
return value;
|
|
124
183
|
});
|
|
184
|
+
reportStats();
|
|
185
|
+
return result;
|
|
125
186
|
};
|
|
126
187
|
return {
|
|
127
188
|
async translateText(text, targetLocale, sourceLocale) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@form-engine-ts/translator-cache",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.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.
|
|
41
|
+
"@form-engine-ts/core": "2.9.0"
|
|
42
42
|
},
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "tsup src/index.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",
|