@proveanything/smartlinks 1.9.3 → 1.9.5

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.
@@ -0,0 +1,38 @@
1
+ import type { ResolvedTranslationItem, TranslationContext, TranslationHashOptions } from './types/translations';
2
+ interface LocalTranslationCacheEntry {
3
+ collectionId: string;
4
+ sourceLanguage: string;
5
+ targetLanguage: string;
6
+ contentType: string;
7
+ contextKey: string | null;
8
+ hash: string;
9
+ item: ResolvedTranslationItem;
10
+ cachedAt: number;
11
+ expiresAt: number;
12
+ }
13
+ export declare function getDefaultTranslationCacheTtlMs(): number;
14
+ export declare function deriveTranslationContextKey(context?: TranslationContext): string | null;
15
+ export declare function normalizeTranslationText(text: string, options?: TranslationHashOptions): string;
16
+ export declare function hashTranslationText(text: string, options?: TranslationHashOptions): Promise<string>;
17
+ export declare function getCachedTranslations(params: {
18
+ collectionId: string;
19
+ sourceLanguage: string;
20
+ targetLanguage: string;
21
+ contentType: string;
22
+ contextKey: string | null;
23
+ hashes: string[];
24
+ }): Promise<Map<string, LocalTranslationCacheEntry>>;
25
+ export declare function setCachedTranslations(params: {
26
+ collectionId: string;
27
+ sourceLanguage: string;
28
+ targetLanguage: string;
29
+ contentType: string;
30
+ contextKey: string | null;
31
+ items: Array<{
32
+ hash: string;
33
+ item: ResolvedTranslationItem;
34
+ }>;
35
+ ttlMs?: number;
36
+ }): Promise<void>;
37
+ export declare function clearCachedTranslations(collectionId?: string): Promise<void>;
38
+ export {};
@@ -0,0 +1,298 @@
1
+ const DB_NAME = 'smartlinks-sdk-translations';
2
+ const STORE_NAME = 'translations';
3
+ const DB_VERSION = 1;
4
+ const DEFAULT_LOCAL_TRANSLATION_TTL_MS = 90 * 24 * 60 * 60000;
5
+ let dbPromise = null;
6
+ const memoryTranslationCache = new Map();
7
+ export function getDefaultTranslationCacheTtlMs() {
8
+ return DEFAULT_LOCAL_TRANSLATION_TTL_MS;
9
+ }
10
+ function isIndexedDbAvailable() {
11
+ try {
12
+ return typeof indexedDB !== 'undefined' && indexedDB !== null;
13
+ }
14
+ catch (_a) {
15
+ return false;
16
+ }
17
+ }
18
+ function openDb() {
19
+ if (dbPromise)
20
+ return dbPromise;
21
+ dbPromise = new Promise((resolve, reject) => {
22
+ try {
23
+ const request = indexedDB.open(DB_NAME, DB_VERSION);
24
+ request.onupgradeneeded = (event) => {
25
+ const db = event.target.result;
26
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
27
+ db.createObjectStore(STORE_NAME);
28
+ }
29
+ };
30
+ request.onsuccess = (event) => {
31
+ resolve(event.target.result);
32
+ };
33
+ request.onerror = () => {
34
+ dbPromise = null;
35
+ reject(request.error);
36
+ };
37
+ request.onblocked = () => {
38
+ dbPromise = null;
39
+ reject(new Error('[smartlinks] Translation IndexedDB open blocked'));
40
+ };
41
+ }
42
+ catch (error) {
43
+ dbPromise = null;
44
+ reject(error);
45
+ }
46
+ });
47
+ return dbPromise;
48
+ }
49
+ function stableContextKey(context) {
50
+ if (!context)
51
+ return null;
52
+ const entries = Object.entries(context)
53
+ .filter(([, value]) => value !== undefined)
54
+ .sort(([left], [right]) => left.localeCompare(right));
55
+ if (entries.length === 0)
56
+ return null;
57
+ if (typeof context.surface === 'string' && typeof context.field === 'string') {
58
+ return `${context.surface}:${context.field}`;
59
+ }
60
+ return entries
61
+ .map(([key, value]) => `${key}=${String(value)}`)
62
+ .join('|');
63
+ }
64
+ export function deriveTranslationContextKey(context) {
65
+ return stableContextKey(context);
66
+ }
67
+ export function normalizeTranslationText(text, options = {}) {
68
+ const { trim = true, collapseWhitespace = false, unicodeNormalization = 'NFC', } = options;
69
+ let normalized = text.replace(/\r\n?/g, '\n');
70
+ if (trim) {
71
+ normalized = normalized.trim();
72
+ }
73
+ if (collapseWhitespace) {
74
+ normalized = normalized.replace(/[\t\f\v ]+/g, ' ');
75
+ }
76
+ if (unicodeNormalization) {
77
+ normalized = normalized.normalize(unicodeNormalization);
78
+ }
79
+ return normalized;
80
+ }
81
+ function toHex(buffer) {
82
+ const bytes = new Uint8Array(buffer);
83
+ let output = '';
84
+ for (const byte of bytes) {
85
+ output += byte.toString(16).padStart(2, '0');
86
+ }
87
+ return output;
88
+ }
89
+ export async function hashTranslationText(text, options = {}) {
90
+ var _a;
91
+ const normalized = normalizeTranslationText(text, options);
92
+ const subtle = (_a = globalThis.crypto) === null || _a === void 0 ? void 0 : _a.subtle;
93
+ if (!subtle) {
94
+ throw new Error('[smartlinks] Web Crypto is required for translation hashing');
95
+ }
96
+ const encoded = new TextEncoder().encode(normalized);
97
+ const digest = await subtle.digest('SHA-256', encoded);
98
+ return `sha256:${toHex(digest)}`;
99
+ }
100
+ function buildLocalCacheKey(params) {
101
+ var _a;
102
+ return [
103
+ params.collectionId,
104
+ params.sourceLanguage,
105
+ params.targetLanguage,
106
+ params.contentType,
107
+ (_a = params.contextKey) !== null && _a !== void 0 ? _a : '',
108
+ params.hash,
109
+ ].join('::');
110
+ }
111
+ function isExpired(entry) {
112
+ return entry.expiresAt <= Date.now();
113
+ }
114
+ async function getEntry(key) {
115
+ const memoryEntry = memoryTranslationCache.get(key);
116
+ if (memoryEntry) {
117
+ if (isExpired(memoryEntry)) {
118
+ memoryTranslationCache.delete(key);
119
+ return null;
120
+ }
121
+ return memoryEntry;
122
+ }
123
+ if (!isIndexedDbAvailable())
124
+ return null;
125
+ try {
126
+ const db = await openDb();
127
+ return await new Promise((resolve) => {
128
+ try {
129
+ const tx = db.transaction(STORE_NAME, 'readonly');
130
+ const request = tx.objectStore(STORE_NAME).get(key);
131
+ request.onsuccess = () => {
132
+ var _a;
133
+ const result = (_a = request.result) !== null && _a !== void 0 ? _a : null;
134
+ if (!result) {
135
+ resolve(null);
136
+ return;
137
+ }
138
+ if (isExpired(result)) {
139
+ memoryTranslationCache.delete(key);
140
+ void deleteEntries([key]);
141
+ resolve(null);
142
+ return;
143
+ }
144
+ memoryTranslationCache.set(key, result);
145
+ resolve(result);
146
+ };
147
+ request.onerror = () => resolve(null);
148
+ }
149
+ catch (_a) {
150
+ resolve(null);
151
+ }
152
+ });
153
+ }
154
+ catch (_a) {
155
+ return null;
156
+ }
157
+ }
158
+ export async function getCachedTranslations(params) {
159
+ const entries = await Promise.all(params.hashes.map(async (hash) => {
160
+ const key = buildLocalCacheKey(Object.assign(Object.assign({}, params), { hash }));
161
+ const entry = await getEntry(key);
162
+ return entry ? [hash, entry] : null;
163
+ }));
164
+ return new Map(entries.filter((entry) => entry !== null));
165
+ }
166
+ async function setEntries(entries) {
167
+ for (const [key, entry] of entries) {
168
+ memoryTranslationCache.set(key, entry);
169
+ }
170
+ if (!isIndexedDbAvailable())
171
+ return;
172
+ try {
173
+ const db = await openDb();
174
+ await new Promise((resolve) => {
175
+ try {
176
+ const tx = db.transaction(STORE_NAME, 'readwrite');
177
+ const store = tx.objectStore(STORE_NAME);
178
+ for (const [key, entry] of entries) {
179
+ store.put(entry, key);
180
+ }
181
+ tx.oncomplete = () => resolve();
182
+ tx.onerror = () => resolve();
183
+ tx.onabort = () => resolve();
184
+ }
185
+ catch (_a) {
186
+ resolve();
187
+ }
188
+ });
189
+ }
190
+ catch (_a) {
191
+ // Best-effort persistence only.
192
+ }
193
+ }
194
+ export async function setCachedTranslations(params) {
195
+ var _a;
196
+ const ttlMs = (_a = params.ttlMs) !== null && _a !== void 0 ? _a : DEFAULT_LOCAL_TRANSLATION_TTL_MS;
197
+ const now = Date.now();
198
+ const entries = params.items.map(({ hash, item }) => {
199
+ const key = buildLocalCacheKey({
200
+ collectionId: params.collectionId,
201
+ sourceLanguage: params.sourceLanguage,
202
+ targetLanguage: params.targetLanguage,
203
+ contentType: params.contentType,
204
+ contextKey: params.contextKey,
205
+ hash,
206
+ });
207
+ const entry = {
208
+ collectionId: params.collectionId,
209
+ sourceLanguage: params.sourceLanguage,
210
+ targetLanguage: params.targetLanguage,
211
+ contentType: params.contentType,
212
+ contextKey: params.contextKey,
213
+ hash,
214
+ item: Object.assign(Object.assign({}, item), { expiresAt: now + ttlMs }),
215
+ cachedAt: now,
216
+ expiresAt: now + ttlMs,
217
+ };
218
+ return [key, entry];
219
+ });
220
+ await setEntries(entries);
221
+ }
222
+ async function deleteEntries(keys) {
223
+ for (const key of keys) {
224
+ memoryTranslationCache.delete(key);
225
+ }
226
+ if (!isIndexedDbAvailable() || keys.length === 0)
227
+ return;
228
+ try {
229
+ const db = await openDb();
230
+ await new Promise((resolve) => {
231
+ try {
232
+ const tx = db.transaction(STORE_NAME, 'readwrite');
233
+ const store = tx.objectStore(STORE_NAME);
234
+ for (const key of keys) {
235
+ store.delete(key);
236
+ }
237
+ tx.oncomplete = () => resolve();
238
+ tx.onerror = () => resolve();
239
+ tx.onabort = () => resolve();
240
+ }
241
+ catch (_a) {
242
+ resolve();
243
+ }
244
+ });
245
+ }
246
+ catch (_a) {
247
+ // Ignore deletion failures.
248
+ }
249
+ }
250
+ export async function clearCachedTranslations(collectionId) {
251
+ const memoryKeys = Array.from(memoryTranslationCache.keys());
252
+ const keysToDelete = collectionId
253
+ ? memoryKeys.filter((key) => key.startsWith(`${collectionId}::`))
254
+ : memoryKeys;
255
+ await deleteEntries(keysToDelete);
256
+ if (!isIndexedDbAvailable())
257
+ return;
258
+ try {
259
+ const db = await openDb();
260
+ if (!collectionId) {
261
+ await new Promise((resolve) => {
262
+ try {
263
+ const tx = db.transaction(STORE_NAME, 'readwrite');
264
+ tx.objectStore(STORE_NAME).clear();
265
+ tx.oncomplete = () => resolve();
266
+ tx.onerror = () => resolve();
267
+ tx.onabort = () => resolve();
268
+ }
269
+ catch (_a) {
270
+ resolve();
271
+ }
272
+ });
273
+ return;
274
+ }
275
+ await new Promise((resolve) => {
276
+ try {
277
+ const tx = db.transaction(STORE_NAME, 'readwrite');
278
+ const store = tx.objectStore(STORE_NAME);
279
+ const request = store.getAllKeys();
280
+ request.onsuccess = () => {
281
+ for (const key of request.result) {
282
+ if (key.startsWith(`${collectionId}::`)) {
283
+ store.delete(key);
284
+ }
285
+ }
286
+ resolve();
287
+ };
288
+ request.onerror = () => resolve();
289
+ }
290
+ catch (_a) {
291
+ resolve();
292
+ }
293
+ });
294
+ }
295
+ catch (_a) {
296
+ // Ignore deletion failures.
297
+ }
298
+ }
@@ -32,3 +32,4 @@ export * from "./ai";
32
32
  export * from "./appManifest";
33
33
  export * from "./appObjects";
34
34
  export * from "./loyalty";
35
+ export * from "./translations";
@@ -34,3 +34,4 @@ export * from "./ai";
34
34
  export * from "./appManifest";
35
35
  export * from "./appObjects";
36
36
  export * from "./loyalty";
37
+ export * from "./translations";
@@ -0,0 +1,107 @@
1
+ export type TranslationLookupMode = 'cache-fill' | 'cache-only';
2
+ export type TranslationContentType = 'text/plain' | 'text/html' | 'text/x-liquid' | (string & {});
3
+ export type TranslationQuality = 'machine' | 'human' | 'passthrough' | (string & {});
4
+ export type TranslationItemStatus = 'cached' | 'generated' | 'miss' | 'passthrough' | 'local-cache' | (string & {});
5
+ export type TranslationContextValue = string | number | boolean | null;
6
+ export interface TranslationContext {
7
+ surface?: string;
8
+ field?: string;
9
+ [key: string]: TranslationContextValue | undefined;
10
+ }
11
+ export interface TranslationLookupRequestBase {
12
+ targetLanguage: string;
13
+ sourceLanguage?: string;
14
+ mode?: TranslationLookupMode;
15
+ contentType?: TranslationContentType;
16
+ context?: TranslationContext;
17
+ returnMeta?: boolean;
18
+ }
19
+ export interface TranslationLookupSingleRequest extends TranslationLookupRequestBase {
20
+ text: string;
21
+ texts?: never;
22
+ }
23
+ export interface TranslationLookupBatchRequest extends TranslationLookupRequestBase {
24
+ text?: never;
25
+ texts: string[];
26
+ }
27
+ export type TranslationLookupRequest = TranslationLookupSingleRequest | TranslationLookupBatchRequest;
28
+ export interface TranslationLookupItem {
29
+ index: number;
30
+ hash: string;
31
+ sourceText: string;
32
+ translatedText?: string;
33
+ status?: TranslationItemStatus;
34
+ provider?: string;
35
+ model?: string;
36
+ isOverride?: boolean;
37
+ quality?: TranslationQuality;
38
+ createdAt?: string;
39
+ updatedAt?: string;
40
+ }
41
+ export interface TranslationLookupResponse {
42
+ targetLanguage: string;
43
+ sourceLanguage?: string;
44
+ mode?: TranslationLookupMode;
45
+ items: TranslationLookupItem[];
46
+ }
47
+ export interface ResolvedTranslationItem extends TranslationLookupItem {
48
+ cacheSource?: 'local' | 'remote';
49
+ expiresAt?: number;
50
+ }
51
+ export interface ResolvedTranslationResponse {
52
+ targetLanguage: string;
53
+ sourceLanguage?: string;
54
+ mode?: TranslationLookupMode;
55
+ items: ResolvedTranslationItem[];
56
+ }
57
+ export interface TranslationHashOptions {
58
+ trim?: boolean;
59
+ collapseWhitespace?: boolean;
60
+ unicodeNormalization?: 'NFC' | 'NFKC' | false;
61
+ }
62
+ export interface TranslationResolveOptions {
63
+ useLocalCache?: boolean;
64
+ refreshLocalCache?: boolean;
65
+ localCacheTtlMs?: number;
66
+ hashOptions?: TranslationHashOptions;
67
+ }
68
+ export interface TranslationRecord {
69
+ id: string;
70
+ collectionId: string;
71
+ sourceHash: string;
72
+ sourceText: string;
73
+ sourceLanguage?: string;
74
+ targetLanguage: string;
75
+ contentType: string;
76
+ contextKey?: string | null;
77
+ translatedText: string;
78
+ provider?: string | null;
79
+ model?: string | null;
80
+ quality: TranslationQuality;
81
+ isOverride: boolean;
82
+ metadata?: Record<string, any>;
83
+ createdAt: string;
84
+ updatedAt: string;
85
+ }
86
+ export interface TranslationListParams {
87
+ targetLanguage?: string;
88
+ sourceLanguage?: string;
89
+ contentType?: string;
90
+ contextKey?: string;
91
+ q?: string;
92
+ isOverride?: boolean;
93
+ limit?: number;
94
+ offset?: number;
95
+ }
96
+ export interface TranslationListResponse {
97
+ items: TranslationRecord[];
98
+ total?: number;
99
+ limit?: number;
100
+ offset?: number;
101
+ }
102
+ export interface TranslationUpdateRequest {
103
+ translatedText?: string;
104
+ isOverride?: boolean;
105
+ quality?: TranslationQuality;
106
+ metadata?: Record<string, any>;
107
+ }
@@ -0,0 +1 @@
1
+ export {};