@happyvertical/smrt-languages 0.30.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.
@@ -0,0 +1,509 @@
1
+ import { field, smrt, SmrtObject, SmrtCollection } from "@happyvertical/smrt-core";
2
+ import { createHash } from "node:crypto";
3
+ function isPlainObject(value) {
4
+ return !!value && typeof value === "object" && !Array.isArray(value);
5
+ }
6
+ function computeSourceHash(template) {
7
+ return `sha256:${createHash("sha256").update(template, "utf8").digest("hex")}`;
8
+ }
9
+ function normalizeLocale(locale) {
10
+ if (!locale) return "";
11
+ const trimmed = locale.trim();
12
+ if (!trimmed) return "";
13
+ const [language, ...rest] = trimmed.split("-");
14
+ if (rest.length === 0) {
15
+ return language.toLowerCase();
16
+ }
17
+ return [
18
+ language.toLowerCase(),
19
+ ...rest.map((part) => part.toUpperCase())
20
+ ].join("-");
21
+ }
22
+ function buildLocaleFallbackChain(requested, defaultLocale) {
23
+ const chain = [];
24
+ const push = (value) => {
25
+ const normalized = normalizeLocale(value);
26
+ if (normalized && !chain.includes(normalized)) {
27
+ chain.push(normalized);
28
+ }
29
+ };
30
+ if (requested) {
31
+ let current = normalizeLocale(requested);
32
+ while (current) {
33
+ push(current);
34
+ const idx = current.lastIndexOf("-");
35
+ if (idx === -1) break;
36
+ current = current.slice(0, idx);
37
+ }
38
+ }
39
+ if (defaultLocale) {
40
+ push(defaultLocale);
41
+ }
42
+ return chain;
43
+ }
44
+ function renderTemplate(template, variables) {
45
+ if (!variables || Object.keys(variables).length === 0) {
46
+ return template;
47
+ }
48
+ return template.replace(/\{([^{}]+)\}/g, (_match, rawKey) => {
49
+ const key = rawKey.trim();
50
+ const value = variables[key];
51
+ if (value === void 0 || value === null) {
52
+ return "";
53
+ }
54
+ if (value instanceof Date) {
55
+ return value.toISOString();
56
+ }
57
+ if (Array.isArray(value) || isPlainObject(value)) {
58
+ try {
59
+ return JSON.stringify(value);
60
+ } catch {
61
+ return "";
62
+ }
63
+ }
64
+ return String(value);
65
+ });
66
+ }
67
+ function buildTranslationJobId(key, targetLocale) {
68
+ return `smrt-languages.translate:${key}:${normalizeLocale(targetLocale)}`;
69
+ }
70
+ const LANGUAGE_CACHE_TTL_MS = 3e4;
71
+ const languageCache = /* @__PURE__ */ new Map();
72
+ const dbInstanceIds = /* @__PURE__ */ new WeakMap();
73
+ let nextDbId = 1;
74
+ function getDbNamespace(db) {
75
+ if (!db) {
76
+ return "no-db";
77
+ }
78
+ if (typeof db === "string") {
79
+ return `db:${db}`;
80
+ }
81
+ if (typeof db === "object") {
82
+ const dbObject = db;
83
+ if (typeof dbObject.query === "function") {
84
+ if (!dbInstanceIds.has(dbObject)) {
85
+ dbInstanceIds.set(dbObject, `db-instance:${nextDbId++}`);
86
+ }
87
+ const namespace = dbInstanceIds.get(dbObject);
88
+ if (namespace) {
89
+ return namespace;
90
+ }
91
+ return "db-instance:unknown";
92
+ }
93
+ try {
94
+ return `db-config:${JSON.stringify(dbObject)}`;
95
+ } catch {
96
+ return "db-config:opaque";
97
+ }
98
+ }
99
+ return "db:unknown";
100
+ }
101
+ function buildCacheKey(key, locale, tenantId, db) {
102
+ return `${getDbNamespace(db)}::${key}::${normalizeLocale(locale)}::${tenantId ?? "app"}`;
103
+ }
104
+ function getLanguageCacheTtlMs() {
105
+ return LANGUAGE_CACHE_TTL_MS;
106
+ }
107
+ function getCachedLanguage(key, locale, tenantId, db) {
108
+ const cacheKey = buildCacheKey(key, locale, tenantId, db);
109
+ const cached = languageCache.get(cacheKey);
110
+ if (!cached) {
111
+ return null;
112
+ }
113
+ if (cached.expiresAt <= Date.now()) {
114
+ languageCache.delete(cacheKey);
115
+ return null;
116
+ }
117
+ return cached.value;
118
+ }
119
+ function setCachedLanguage(key, locale, tenantId, db, value) {
120
+ languageCache.set(buildCacheKey(key, locale, tenantId, db), {
121
+ expiresAt: Date.now() + LANGUAGE_CACHE_TTL_MS,
122
+ value
123
+ });
124
+ }
125
+ function invalidateLanguageCache(key, locale, tenantId, db) {
126
+ const dbNamespace = getDbNamespace(db);
127
+ const normalizedLocale = normalizeLocale(locale);
128
+ if (tenantId !== null && tenantId !== void 0) {
129
+ languageCache.delete(buildCacheKey(key, normalizedLocale, tenantId, db));
130
+ return;
131
+ }
132
+ const keyPrefix = `${dbNamespace}::${key}::${normalizedLocale}::`;
133
+ for (const cacheKey of languageCache.keys()) {
134
+ if (cacheKey.startsWith(keyPrefix)) {
135
+ languageCache.delete(cacheKey);
136
+ }
137
+ }
138
+ }
139
+ function clearLanguageCache() {
140
+ languageCache.clear();
141
+ }
142
+ var __defProp = Object.defineProperty;
143
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
144
+ var __decorateClass = (decorators, target, key, kind) => {
145
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key) : target;
146
+ for (var i = decorators.length - 1, decorator; i >= 0; i--)
147
+ if (decorator = decorators[i])
148
+ result = (kind ? decorator(target, key, result) : decorator(result)) || result;
149
+ if (kind && result) __defProp(target, key, result);
150
+ return result;
151
+ };
152
+ let LanguageOverride = class extends SmrtObject {
153
+ key = "";
154
+ locale = "";
155
+ tenantId = null;
156
+ template = "";
157
+ auto_generated = false;
158
+ source_hash = null;
159
+ ai_model = null;
160
+ reviewed_at = null;
161
+ reviewed_by = null;
162
+ constructor(options = {}) {
163
+ super(options);
164
+ if (options.key !== void 0) this.key = options.key;
165
+ if (options.locale !== void 0)
166
+ this.locale = normalizeLocale(options.locale);
167
+ if (options.tenantId !== void 0) this.tenantId = options.tenantId;
168
+ if (options.template !== void 0) this.template = options.template;
169
+ if (options.auto_generated !== void 0) {
170
+ this.auto_generated = options.auto_generated;
171
+ }
172
+ if (options.source_hash !== void 0)
173
+ this.source_hash = options.source_hash;
174
+ if (options.ai_model !== void 0) this.ai_model = options.ai_model;
175
+ if (options.reviewed_at !== void 0)
176
+ this.reviewed_at = options.reviewed_at;
177
+ if (options.reviewed_by !== void 0)
178
+ this.reviewed_by = options.reviewed_by;
179
+ }
180
+ async save() {
181
+ if (!this.key || this.key.trim() === "") {
182
+ throw new Error("LanguageOverride.key is required");
183
+ }
184
+ if (!this.locale || this.locale.trim() === "") {
185
+ throw new Error("LanguageOverride.locale is required");
186
+ }
187
+ if (typeof this.template !== "string") {
188
+ throw new Error("LanguageOverride.template must be a string");
189
+ }
190
+ this.locale = normalizeLocale(this.locale);
191
+ this.context = this.tenantId ?? "__app__";
192
+ const previousIdentity = await this.getPersistedIdentity();
193
+ const identityChanged = !!previousIdentity && (previousIdentity.key !== this.key || previousIdentity.locale !== this.locale || previousIdentity.tenantId !== this.tenantId);
194
+ const result = identityChanged && previousIdentity ? await this.saveAfterIdentityChange() : await super.save();
195
+ if (identityChanged && previousIdentity) {
196
+ invalidateLanguageCache(
197
+ previousIdentity.key,
198
+ previousIdentity.locale,
199
+ previousIdentity.tenantId,
200
+ this.db
201
+ );
202
+ }
203
+ invalidateLanguageCache(this.key, this.locale, this.tenantId, this.db);
204
+ return result;
205
+ }
206
+ async saveAfterIdentityChange() {
207
+ if (typeof this.db.beginTransaction === "function") {
208
+ return this.saveAfterIdentityChangeInTransaction();
209
+ }
210
+ return this.saveAfterIdentityChangeWithDeferredDelete();
211
+ }
212
+ async saveAfterIdentityChangeInTransaction() {
213
+ const originalDb = this._db;
214
+ const originalOptionsDb = this.options.db;
215
+ const tx = await this.db.beginTransaction?.();
216
+ if (!tx) {
217
+ return this.saveAfterIdentityChangeWithDeferredDelete();
218
+ }
219
+ try {
220
+ this._db = tx;
221
+ this.options.db = tx;
222
+ await super.delete();
223
+ const result = await super.save();
224
+ await tx.commit();
225
+ return result;
226
+ } catch (error) {
227
+ try {
228
+ await tx.rollback();
229
+ } catch {
230
+ }
231
+ throw error;
232
+ } finally {
233
+ this._db = originalDb;
234
+ this.options.db = originalOptionsDb;
235
+ }
236
+ }
237
+ async saveAfterIdentityChangeWithDeferredDelete() {
238
+ const previousId = this.id;
239
+ if (!previousId) {
240
+ return super.save();
241
+ }
242
+ const replacementId = crypto.randomUUID();
243
+ let replacementSaved = false;
244
+ this.id = replacementId;
245
+ try {
246
+ const result = await super.save();
247
+ replacementSaved = true;
248
+ await this.db.delete(this.tableName, { id: previousId });
249
+ return result;
250
+ } catch (error) {
251
+ if (replacementSaved) {
252
+ try {
253
+ await this.db.delete(this.tableName, { id: replacementId });
254
+ } catch {
255
+ }
256
+ }
257
+ this.id = previousId;
258
+ throw error;
259
+ }
260
+ }
261
+ async delete() {
262
+ const key = this.key;
263
+ const locale = this.locale;
264
+ const tenantId = this.tenantId;
265
+ await super.delete();
266
+ invalidateLanguageCache(key, locale, tenantId, this.db);
267
+ }
268
+ /**
269
+ * Mark this auto-generated row as reviewed by an admin. Useful for the
270
+ * admin review queue surfaced via `smrt languages approve <id>`.
271
+ */
272
+ async approve(reviewerId) {
273
+ this.reviewed_at = (/* @__PURE__ */ new Date()).toISOString();
274
+ this.reviewed_by = reviewerId;
275
+ return this.save();
276
+ }
277
+ async getPersistedIdentity() {
278
+ if (!this.id) {
279
+ return null;
280
+ }
281
+ const existing = await this.db.get(this.tableName, { id: this.id });
282
+ if (!existing) {
283
+ return null;
284
+ }
285
+ const row = existing;
286
+ return {
287
+ key: String(row.key ?? this.key),
288
+ locale: String(row.locale ?? this.locale),
289
+ tenantId: row.tenantId !== void 0 ? row.tenantId : row.tenant_id ?? null
290
+ };
291
+ }
292
+ };
293
+ __decorateClass([
294
+ field({ required: true })
295
+ ], LanguageOverride.prototype, "key", 2);
296
+ __decorateClass([
297
+ field({ required: true })
298
+ ], LanguageOverride.prototype, "locale", 2);
299
+ __decorateClass([
300
+ field({ type: "text", nullable: true })
301
+ ], LanguageOverride.prototype, "tenantId", 2);
302
+ __decorateClass([
303
+ field({ type: "text", required: true })
304
+ ], LanguageOverride.prototype, "template", 2);
305
+ __decorateClass([
306
+ field({ type: "boolean", required: true, default: false })
307
+ ], LanguageOverride.prototype, "auto_generated", 2);
308
+ __decorateClass([
309
+ field({ type: "text", nullable: true })
310
+ ], LanguageOverride.prototype, "source_hash", 2);
311
+ __decorateClass([
312
+ field({ type: "text", nullable: true })
313
+ ], LanguageOverride.prototype, "ai_model", 2);
314
+ __decorateClass([
315
+ field({ type: "text", nullable: true })
316
+ ], LanguageOverride.prototype, "reviewed_at", 2);
317
+ __decorateClass([
318
+ field({ type: "text", nullable: true })
319
+ ], LanguageOverride.prototype, "reviewed_by", 2);
320
+ LanguageOverride = __decorateClass([
321
+ smrt({
322
+ tableName: "_smrt_language_overrides",
323
+ conflictColumns: ["key", "locale", "context"],
324
+ api: { include: ["list", "get", "create", "update", "delete"] },
325
+ cli: {
326
+ include: ["list", "get", "create", "update", "delete", "approve"],
327
+ // approve is an admin review action invoked in-process via the CLI;
328
+ // it intentionally isn't exposed over HTTP today.
329
+ skipApiCheck: true
330
+ },
331
+ mcp: { include: [] }
332
+ })
333
+ ], LanguageOverride);
334
+ class LanguageOverrideCollection extends SmrtCollection {
335
+ static _itemClass = LanguageOverride;
336
+ /**
337
+ * Look up the app-level (tenantId = null) override for a (key, locale) pair.
338
+ * App-level rows hold AI auto-translations and ops-curated app-wide strings.
339
+ */
340
+ async getAppOverride(key, locale) {
341
+ const items = await this.list({
342
+ where: { key, locale: normalizeLocale(locale), tenantId: null }
343
+ });
344
+ return items[0] ?? null;
345
+ }
346
+ async getTenantOverride(key, locale, tenantId) {
347
+ const items = await this.list({
348
+ where: { key, locale: normalizeLocale(locale), tenantId }
349
+ });
350
+ return items[0] ?? null;
351
+ }
352
+ /**
353
+ * Convenience helper for the resolver: returns the app and (optional) tenant
354
+ * override rows for a (key, locale).
355
+ */
356
+ async getResolutionLayers(key, locale, tenantId) {
357
+ const [app, tenant] = await Promise.all([
358
+ this.getAppOverride(key, locale),
359
+ tenantId != null ? this.getTenantOverride(key, locale, tenantId) : Promise.resolve(null)
360
+ ]);
361
+ return { app, tenant };
362
+ }
363
+ /** All overrides for a tenant (used to build the AI translation glossary). */
364
+ async listTenantOverrides(tenantId) {
365
+ return this.list({ where: { tenantId } });
366
+ }
367
+ /** Auto-generated overrides that no admin has approved yet. */
368
+ async listUnreviewedAutoTranslations(options = {}) {
369
+ const where = {
370
+ auto_generated: true,
371
+ reviewed_at: null
372
+ };
373
+ if (options.locale) {
374
+ where.locale = normalizeLocale(options.locale);
375
+ }
376
+ return this.list({
377
+ where,
378
+ orderBy: "createdAt ASC",
379
+ limit: options.limit
380
+ });
381
+ }
382
+ }
383
+ function buildTenantGlossary(overrides, options) {
384
+ const sourceLocale = normalizeLocale(options.sourceLocale);
385
+ const targetLocale = normalizeLocale(options.targetLocale);
386
+ const max = options.max ?? 25;
387
+ const sourceByKey = /* @__PURE__ */ new Map();
388
+ const targetByKey = /* @__PURE__ */ new Map();
389
+ for (const override of overrides) {
390
+ if (!override.template) continue;
391
+ const locale = normalizeLocale(override.locale);
392
+ if (locale === sourceLocale) {
393
+ sourceByKey.set(override.key, override.template);
394
+ } else if (locale === targetLocale) {
395
+ targetByKey.set(override.key, override.template);
396
+ }
397
+ }
398
+ const pairs = [];
399
+ for (const [key, source] of sourceByKey.entries()) {
400
+ const target = targetByKey.get(key);
401
+ if (target) {
402
+ pairs.push(`- "${source}" → "${target}"`);
403
+ if (pairs.length >= max) break;
404
+ }
405
+ }
406
+ return pairs.join("\n");
407
+ }
408
+ function getRegistry() {
409
+ if (!globalThis.__smrtLanguageRegistry) {
410
+ globalThis.__smrtLanguageRegistry = /* @__PURE__ */ new Map();
411
+ }
412
+ return globalThis.__smrtLanguageRegistry;
413
+ }
414
+ function buildIndex(key, locale) {
415
+ return { key, locale: normalizeLocale(locale) };
416
+ }
417
+ const LanguageRegistry = {
418
+ register(input) {
419
+ if (!input.key || input.key.trim() === "") {
420
+ throw new Error("Language string definitions require a non-empty key");
421
+ }
422
+ if (!input.locale || input.locale.trim() === "") {
423
+ throw new Error(
424
+ `Language string "${input.key}" requires a non-empty locale`
425
+ );
426
+ }
427
+ if (typeof input.template !== "string") {
428
+ throw new Error(
429
+ `Language string "${input.key}" template must be a string`
430
+ );
431
+ }
432
+ const { key, locale } = buildIndex(input.key, input.locale);
433
+ const definition = {
434
+ key,
435
+ locale,
436
+ template: input.template,
437
+ sourceHash: computeSourceHash(input.template)
438
+ };
439
+ const registry = getRegistry();
440
+ let byLocale = registry.get(key);
441
+ if (!byLocale) {
442
+ byLocale = /* @__PURE__ */ new Map();
443
+ registry.set(key, byLocale);
444
+ }
445
+ const existing = byLocale.get(locale);
446
+ if (existing) {
447
+ if (existing.template !== definition.template) {
448
+ throw new Error(
449
+ `Language string "${key}" is already registered for locale "${locale}" with a different template`
450
+ );
451
+ }
452
+ return existing;
453
+ }
454
+ byLocale.set(locale, definition);
455
+ return definition;
456
+ },
457
+ get(key, locale) {
458
+ return getRegistry().get(key)?.get(normalizeLocale(locale));
459
+ },
460
+ has(key, locale) {
461
+ return Boolean(this.get(key, locale));
462
+ },
463
+ hasKey(key) {
464
+ return getRegistry().has(key);
465
+ },
466
+ /** Return the locales that have a registered code default for the given key. */
467
+ getLocalesForKey(key) {
468
+ const byLocale = getRegistry().get(key);
469
+ return byLocale ? Array.from(byLocale.keys()) : [];
470
+ },
471
+ /** All registered (key, locale, template) triples. Used by the batch translator. */
472
+ getAll() {
473
+ const all = [];
474
+ for (const byLocale of getRegistry().values()) {
475
+ for (const definition of byLocale.values()) {
476
+ all.push(definition);
477
+ }
478
+ }
479
+ return all;
480
+ },
481
+ /** Distinct keys currently registered. */
482
+ getKeys() {
483
+ return Array.from(getRegistry().keys());
484
+ },
485
+ clear() {
486
+ getRegistry().clear();
487
+ }
488
+ };
489
+ function defineLanguageString(input) {
490
+ return LanguageRegistry.register(input);
491
+ }
492
+ export {
493
+ LanguageOverrideCollection as L,
494
+ LanguageRegistry as a,
495
+ buildTenantGlossary as b,
496
+ computeSourceHash as c,
497
+ buildTranslationJobId as d,
498
+ buildLocaleFallbackChain as e,
499
+ LanguageOverride as f,
500
+ getCachedLanguage as g,
501
+ clearLanguageCache as h,
502
+ invalidateLanguageCache as i,
503
+ defineLanguageString as j,
504
+ getLanguageCacheTtlMs as k,
505
+ normalizeLocale as n,
506
+ renderTemplate as r,
507
+ setCachedLanguage as s
508
+ };
509
+ //# sourceMappingURL=language-registry-CgsuwQo6.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-registry-CgsuwQo6.js","sources":["../../src/utils.ts","../../src/cache.ts","../../src/models/LanguageOverride.ts","../../src/collections/LanguageOverrideCollection.ts","../../src/glossary.ts","../../src/language-registry.ts"],"sourcesContent":["import { createHash } from 'node:crypto';\nimport type { LanguageVariables } from './types.js';\n\nexport function isPlainObject(\n value: unknown,\n): value is Record<string, unknown> {\n return !!value && typeof value === 'object' && !Array.isArray(value);\n}\n\n/** Stable hash used to gate auto-translation re-runs against a specific source. */\nexport function computeSourceHash(template: string): string {\n return `sha256:${createHash('sha256').update(template, 'utf8').digest('hex')}`;\n}\n\n/**\n * Normalize a BCP-47-ish locale tag for consistent matching across the\n * registry, the cache, and DB lookups.\n *\n * Lowercases the language subtag and uppercases everything after it so\n * `fr-ca`, `fr-CA`, and `Fr-Ca` collapse to the same key. This is **not**\n * canonical BCP-47 casing (proper BCP-47 lower-cases variants and uses\n * Title-case for script subtags like `Hans`); it is an internal\n * normalization shared by every layer in this package and intentionally\n * stricter than what BCP-47 mandates.\n */\nexport function normalizeLocale(locale: string): string {\n if (!locale) return '';\n const trimmed = locale.trim();\n if (!trimmed) return '';\n const [language, ...rest] = trimmed.split('-');\n if (rest.length === 0) {\n return language.toLowerCase();\n }\n return [\n language.toLowerCase(),\n ...rest.map((part) => part.toUpperCase()),\n ].join('-');\n}\n\n/**\n * Build the locale fallback chain for a requested locale.\n *\n * Example: `fr-CA` with default `en` → `['fr-CA', 'fr', 'en']`.\n * Duplicates and empty segments are removed.\n */\nexport function buildLocaleFallbackChain(\n requested: string,\n defaultLocale: string,\n): string[] {\n const chain: string[] = [];\n const push = (value: string) => {\n const normalized = normalizeLocale(value);\n if (normalized && !chain.includes(normalized)) {\n chain.push(normalized);\n }\n };\n\n if (requested) {\n let current = normalizeLocale(requested);\n while (current) {\n push(current);\n const idx = current.lastIndexOf('-');\n if (idx === -1) break;\n current = current.slice(0, idx);\n }\n }\n\n if (defaultLocale) {\n push(defaultLocale);\n }\n\n return chain;\n}\n\n/**\n * Render `{var}` placeholders against the supplied variables.\n *\n * When at least one variable is provided, every `{...}` placeholder in the\n * template is replaced; missing or `undefined`/`null` variables collapse to\n * empty strings (callers that pass `name` but not `title` get the `{title}`\n * placeholder removed).\n *\n * When `variables` is `undefined` or `{}` the template is returned unchanged\n * — that fast path avoids the regex pass for the common \"no interpolation\n * needed\" case. If you need every placeholder collapsed regardless, pass at\n * least one variable (e.g. `{ __forceRender: true }`).\n */\nexport function renderTemplate(\n template: string,\n variables?: LanguageVariables,\n): string {\n if (!variables || Object.keys(variables).length === 0) {\n return template;\n }\n\n return template.replace(/\\{([^{}]+)\\}/g, (_match, rawKey: string) => {\n const key = rawKey.trim();\n const value = variables[key];\n\n if (value === undefined || value === null) {\n return '';\n }\n\n if (value instanceof Date) {\n return value.toISOString();\n }\n\n if (Array.isArray(value) || isPlainObject(value)) {\n try {\n return JSON.stringify(value);\n } catch {\n return '';\n }\n }\n\n return String(value);\n });\n}\n\n/** Deterministic translation-job ID — used for stampede protection. */\nexport function buildTranslationJobId(\n key: string,\n targetLocale: string,\n): string {\n return `smrt-languages.translate:${key}:${normalizeLocale(targetLocale)}`;\n}\n","import type { DatabaseInterface } from '@happyvertical/sql';\nimport type { LanguageCacheValue } from './types.js';\nimport { normalizeLocale } from './utils.js';\n\nconst LANGUAGE_CACHE_TTL_MS = 30_000;\n\ntype CacheEntry = {\n expiresAt: number;\n value: LanguageCacheValue;\n};\n\nconst languageCache = new Map<string, CacheEntry>();\nconst dbInstanceIds = new WeakMap<object, string>();\nlet nextDbId = 1;\n\nfunction getDbNamespace(db: unknown): string {\n if (!db) {\n return 'no-db';\n }\n\n if (typeof db === 'string') {\n return `db:${db}`;\n }\n\n if (typeof db === 'object') {\n const dbObject = db as Record<string, unknown>;\n if (typeof dbObject.query === 'function') {\n if (!dbInstanceIds.has(dbObject)) {\n dbInstanceIds.set(dbObject, `db-instance:${nextDbId++}`);\n }\n const namespace = dbInstanceIds.get(dbObject);\n if (namespace) {\n return namespace;\n }\n\n return 'db-instance:unknown';\n }\n\n try {\n return `db-config:${JSON.stringify(dbObject)}`;\n } catch {\n return 'db-config:opaque';\n }\n }\n\n return 'db:unknown';\n}\n\nfunction buildCacheKey(\n key: string,\n locale: string,\n tenantId: string | null | undefined,\n db: unknown,\n): string {\n return `${getDbNamespace(db)}::${key}::${normalizeLocale(locale)}::${\n tenantId ?? 'app'\n }`;\n}\n\nexport function getLanguageCacheTtlMs(): number {\n return LANGUAGE_CACHE_TTL_MS;\n}\n\nexport function getCachedLanguage(\n key: string,\n locale: string,\n tenantId: string | null | undefined,\n db: DatabaseInterface | unknown,\n): LanguageCacheValue | null {\n const cacheKey = buildCacheKey(key, locale, tenantId, db);\n const cached = languageCache.get(cacheKey);\n\n if (!cached) {\n return null;\n }\n\n if (cached.expiresAt <= Date.now()) {\n languageCache.delete(cacheKey);\n return null;\n }\n\n return cached.value;\n}\n\nexport function setCachedLanguage(\n key: string,\n locale: string,\n tenantId: string | null | undefined,\n db: DatabaseInterface | unknown,\n value: LanguageCacheValue,\n): void {\n languageCache.set(buildCacheKey(key, locale, tenantId, db), {\n expiresAt: Date.now() + LANGUAGE_CACHE_TTL_MS,\n value,\n });\n}\n\n/**\n * Drop cached entries that match the given (key, locale).\n *\n * - When `tenantId` is provided, only that tenant's entries for this DB are dropped.\n * - When `tenantId` is null/undefined, every (key, locale, *) entry across tenants\n * is dropped — used for app-level writes that affect all tenants.\n */\nexport function invalidateLanguageCache(\n key: string,\n locale: string,\n tenantId: string | null | undefined,\n db: DatabaseInterface | unknown,\n): void {\n const dbNamespace = getDbNamespace(db);\n const normalizedLocale = normalizeLocale(locale);\n\n if (tenantId !== null && tenantId !== undefined) {\n languageCache.delete(buildCacheKey(key, normalizedLocale, tenantId, db));\n return;\n }\n\n const keyPrefix = `${dbNamespace}::${key}::${normalizedLocale}::`;\n for (const cacheKey of languageCache.keys()) {\n if (cacheKey.startsWith(keyPrefix)) {\n languageCache.delete(cacheKey);\n }\n }\n}\n\nexport function clearLanguageCache(): void {\n languageCache.clear();\n}\n","import {\n field,\n SmrtObject,\n type SmrtObjectOptions,\n smrt,\n} from '@happyvertical/smrt-core';\nimport type { DatabaseInterface } from '@happyvertical/sql';\nimport { invalidateLanguageCache } from '../cache.js';\nimport type { LanguageOverrideOptions } from '../types.js';\nimport { normalizeLocale } from '../utils.js';\n\ntype LanguageTransactionHandle = DatabaseInterface & {\n commit: () => Promise<void>;\n rollback: () => Promise<void>;\n};\n\nexport interface LanguageOverrideCtorOptions\n extends SmrtObjectOptions,\n LanguageOverrideOptions {}\n\n@smrt({\n tableName: '_smrt_language_overrides',\n conflictColumns: ['key', 'locale', 'context'],\n api: { include: ['list', 'get', 'create', 'update', 'delete'] },\n cli: {\n include: ['list', 'get', 'create', 'update', 'delete', 'approve'],\n // approve is an admin review action invoked in-process via the CLI;\n // it intentionally isn't exposed over HTTP today.\n skipApiCheck: true,\n },\n mcp: { include: [] },\n})\nexport class LanguageOverride extends SmrtObject {\n @field({ required: true })\n key: string = '';\n\n @field({ required: true })\n locale: string = '';\n\n @field({ type: 'text', nullable: true })\n tenantId: string | null = null;\n\n @field({ type: 'text', required: true })\n template: string = '';\n\n /** True when this row was produced by the AI translation job. */\n @field({ type: 'boolean', required: true, default: false })\n auto_generated: boolean = false;\n\n /** sha256 of the source template at translation time, for re-translation gating. */\n @field({ type: 'text', nullable: true })\n source_hash: string | null = null;\n\n /** AI model identifier (null for human-edited rows). */\n @field({ type: 'text', nullable: true })\n ai_model: string | null = null;\n\n /** ISO timestamp marking admin review of an auto-generated row. */\n @field({ type: 'text', nullable: true })\n reviewed_at: string | null = null;\n\n /** User ID of the reviewer. */\n @field({ type: 'text', nullable: true })\n reviewed_by: string | null = null;\n\n constructor(options: LanguageOverrideCtorOptions = {}) {\n super(options);\n\n if (options.key !== undefined) this.key = options.key;\n if (options.locale !== undefined)\n this.locale = normalizeLocale(options.locale);\n if (options.tenantId !== undefined) this.tenantId = options.tenantId;\n if (options.template !== undefined) this.template = options.template;\n if (options.auto_generated !== undefined) {\n this.auto_generated = options.auto_generated;\n }\n if (options.source_hash !== undefined)\n this.source_hash = options.source_hash;\n if (options.ai_model !== undefined) this.ai_model = options.ai_model;\n if (options.reviewed_at !== undefined)\n this.reviewed_at = options.reviewed_at;\n if (options.reviewed_by !== undefined)\n this.reviewed_by = options.reviewed_by;\n }\n\n override async save(): Promise<this> {\n if (!this.key || this.key.trim() === '') {\n throw new Error('LanguageOverride.key is required');\n }\n if (!this.locale || this.locale.trim() === '') {\n throw new Error('LanguageOverride.locale is required');\n }\n if (typeof this.template !== 'string') {\n throw new Error('LanguageOverride.template must be a string');\n }\n\n this.locale = normalizeLocale(this.locale);\n // `context` is the conflictColumn-friendly scope: '__app__' for nullable\n // tenant, tenantId otherwise. Mirrors the prompt-override convention.\n this.context = this.tenantId ?? '__app__';\n\n const previousIdentity = await this.getPersistedIdentity();\n const identityChanged =\n !!previousIdentity &&\n (previousIdentity.key !== this.key ||\n previousIdentity.locale !== this.locale ||\n previousIdentity.tenantId !== this.tenantId);\n\n // Persistence upserts on `(key, locale, context)`, so changing any of\n // those on an existing row makes a plain `super.save()` write the old\n // primary key under a new conflict identity — that hits the PK\n // constraint instead of moving the override. Use the same\n // delete-then-insert dance as PromptOverride: prefer a transaction when\n // the driver supports it; otherwise stage a replacement row first and\n // delete the old one only after the new one is durable.\n const result =\n identityChanged && previousIdentity\n ? await this.saveAfterIdentityChange()\n : await super.save();\n\n if (identityChanged && previousIdentity) {\n invalidateLanguageCache(\n previousIdentity.key,\n previousIdentity.locale,\n previousIdentity.tenantId,\n this.db,\n );\n }\n invalidateLanguageCache(this.key, this.locale, this.tenantId, this.db);\n return result;\n }\n\n private async saveAfterIdentityChange(): Promise<this> {\n if (typeof this.db.beginTransaction === 'function') {\n return this.saveAfterIdentityChangeInTransaction();\n }\n return this.saveAfterIdentityChangeWithDeferredDelete();\n }\n\n private async saveAfterIdentityChangeInTransaction(): Promise<this> {\n const originalDb = this._db;\n const originalOptionsDb = this.options.db;\n const tx = (await this.db.beginTransaction?.()) as\n | LanguageTransactionHandle\n | undefined;\n\n if (!tx) {\n return this.saveAfterIdentityChangeWithDeferredDelete();\n }\n\n try {\n this._db = tx;\n this.options.db = tx;\n await super.delete();\n const result = await super.save();\n await tx.commit();\n return result;\n } catch (error) {\n try {\n await tx.rollback();\n } catch {\n // Preserve the original save error; rollback failures are secondary.\n }\n throw error;\n } finally {\n this._db = originalDb;\n this.options.db = originalOptionsDb;\n }\n }\n\n private async saveAfterIdentityChangeWithDeferredDelete(): Promise<this> {\n const previousId = this.id;\n if (!previousId) {\n return super.save();\n }\n\n const replacementId = crypto.randomUUID();\n let replacementSaved = false;\n this.id = replacementId;\n\n try {\n const result = await super.save();\n replacementSaved = true;\n await this.db.delete(this.tableName, { id: previousId });\n return result;\n } catch (error) {\n if (replacementSaved) {\n try {\n await this.db.delete(this.tableName, { id: replacementId });\n } catch {\n // Best effort cleanup keeps the original row as the source of truth.\n }\n }\n this.id = previousId;\n throw error;\n }\n }\n\n override async delete(): Promise<void> {\n const key = this.key;\n const locale = this.locale;\n const tenantId = this.tenantId;\n await super.delete();\n invalidateLanguageCache(key, locale, tenantId, this.db);\n }\n\n /**\n * Mark this auto-generated row as reviewed by an admin. Useful for the\n * admin review queue surfaced via `smrt languages approve <id>`.\n */\n async approve(reviewerId: string): Promise<this> {\n this.reviewed_at = new Date().toISOString();\n this.reviewed_by = reviewerId;\n return this.save();\n }\n\n private async getPersistedIdentity(): Promise<{\n key: string;\n locale: string;\n tenantId: string | null;\n } | null> {\n if (!this.id) {\n return null;\n }\n\n const existing = await this.db.get(this.tableName, { id: this.id });\n if (!existing) {\n return null;\n }\n\n const row = existing as Record<string, unknown>;\n return {\n key: String(row.key ?? this.key),\n locale: String(row.locale ?? this.locale),\n tenantId:\n row.tenantId !== undefined\n ? (row.tenantId as string | null)\n : (((row.tenant_id as string | null | undefined) ?? null) as\n | string\n | null),\n };\n }\n}\n\nexport type { LanguageOverrideOptions } from '../types.js';\n","import { SmrtCollection } from '@happyvertical/smrt-core';\nimport { LanguageOverride } from '../models/LanguageOverride.js';\nimport { normalizeLocale } from '../utils.js';\n\nexport class LanguageOverrideCollection extends SmrtCollection<LanguageOverride> {\n static readonly _itemClass = LanguageOverride;\n\n /**\n * Look up the app-level (tenantId = null) override for a (key, locale) pair.\n * App-level rows hold AI auto-translations and ops-curated app-wide strings.\n */\n async getAppOverride(\n key: string,\n locale: string,\n ): Promise<LanguageOverride | null> {\n const items = await this.list({\n where: { key, locale: normalizeLocale(locale), tenantId: null },\n });\n return items[0] ?? null;\n }\n\n async getTenantOverride(\n key: string,\n locale: string,\n tenantId: string,\n ): Promise<LanguageOverride | null> {\n const items = await this.list({\n where: { key, locale: normalizeLocale(locale), tenantId },\n });\n return items[0] ?? null;\n }\n\n /**\n * Convenience helper for the resolver: returns the app and (optional) tenant\n * override rows for a (key, locale).\n */\n async getResolutionLayers(\n key: string,\n locale: string,\n tenantId?: string | null,\n ): Promise<{\n app: LanguageOverride | null;\n tenant: LanguageOverride | null;\n }> {\n const [app, tenant] = await Promise.all([\n this.getAppOverride(key, locale),\n tenantId != null\n ? this.getTenantOverride(key, locale, tenantId)\n : Promise.resolve(null),\n ]);\n return { app, tenant };\n }\n\n /** All overrides for a tenant (used to build the AI translation glossary). */\n async listTenantOverrides(tenantId: string): Promise<LanguageOverride[]> {\n return this.list({ where: { tenantId } });\n }\n\n /** Auto-generated overrides that no admin has approved yet. */\n async listUnreviewedAutoTranslations(\n options: { locale?: string; limit?: number } = {},\n ): Promise<LanguageOverride[]> {\n const where: Record<string, unknown> = {\n auto_generated: true,\n reviewed_at: null,\n };\n if (options.locale) {\n where.locale = normalizeLocale(options.locale);\n }\n return this.list({\n where,\n orderBy: 'createdAt ASC',\n limit: options.limit,\n });\n }\n}\n","import type { LanguageOverride } from './models/LanguageOverride.js';\nimport { normalizeLocale } from './utils.js';\n\n/**\n * Build a glossary string suitable for inclusion in an AI translation prompt.\n *\n * Pulls the tenant's existing language overrides for the source/target locales\n * and renders them as a concise list of \"source → target\" pairs, restricted to\n * keys that actually have both sides. The intent is \"make AI auto-translations\n * match tenant voice\", not bulk dump every override.\n *\n * Returns an empty string when there is nothing useful to add.\n */\nexport function buildTenantGlossary(\n overrides: LanguageOverride[],\n options: { sourceLocale: string; targetLocale: string; max?: number },\n): string {\n const sourceLocale = normalizeLocale(options.sourceLocale);\n const targetLocale = normalizeLocale(options.targetLocale);\n const max = options.max ?? 25;\n\n const sourceByKey = new Map<string, string>();\n const targetByKey = new Map<string, string>();\n\n for (const override of overrides) {\n if (!override.template) continue;\n const locale = normalizeLocale(override.locale);\n if (locale === sourceLocale) {\n sourceByKey.set(override.key, override.template);\n } else if (locale === targetLocale) {\n targetByKey.set(override.key, override.template);\n }\n }\n\n const pairs: string[] = [];\n for (const [key, source] of sourceByKey.entries()) {\n const target = targetByKey.get(key);\n if (target) {\n pairs.push(`- \"${source}\" → \"${target}\"`);\n if (pairs.length >= max) break;\n }\n }\n\n return pairs.join('\\n');\n}\n","import type {\n LanguageStringDefinition,\n LanguageStringDefinitionInput,\n} from './types.js';\nimport { computeSourceHash, normalizeLocale } from './utils.js';\n\ndeclare global {\n // eslint-disable-next-line no-var\n var __smrtLanguageRegistry:\n | Map<string, Map<string, LanguageStringDefinition>>\n | undefined;\n}\n\nfunction getRegistry(): Map<string, Map<string, LanguageStringDefinition>> {\n if (!globalThis.__smrtLanguageRegistry) {\n globalThis.__smrtLanguageRegistry = new Map();\n }\n return globalThis.__smrtLanguageRegistry;\n}\n\nfunction buildIndex(\n key: string,\n locale: string,\n): { key: string; locale: string } {\n return { key, locale: normalizeLocale(locale) };\n}\n\nexport const LanguageRegistry = {\n register(input: LanguageStringDefinitionInput): LanguageStringDefinition {\n if (!input.key || input.key.trim() === '') {\n throw new Error('Language string definitions require a non-empty key');\n }\n if (!input.locale || input.locale.trim() === '') {\n throw new Error(\n `Language string \"${input.key}\" requires a non-empty locale`,\n );\n }\n if (typeof input.template !== 'string') {\n throw new Error(\n `Language string \"${input.key}\" template must be a string`,\n );\n }\n\n const { key, locale } = buildIndex(input.key, input.locale);\n const definition: LanguageStringDefinition = {\n key,\n locale,\n template: input.template,\n sourceHash: computeSourceHash(input.template),\n };\n\n const registry = getRegistry();\n let byLocale = registry.get(key);\n if (!byLocale) {\n byLocale = new Map();\n registry.set(key, byLocale);\n }\n\n const existing = byLocale.get(locale);\n if (existing) {\n if (existing.template !== definition.template) {\n throw new Error(\n `Language string \"${key}\" is already registered for locale \"${locale}\" with a different template`,\n );\n }\n return existing;\n }\n\n byLocale.set(locale, definition);\n return definition;\n },\n\n get(key: string, locale: string): LanguageStringDefinition | undefined {\n return getRegistry().get(key)?.get(normalizeLocale(locale));\n },\n\n has(key: string, locale: string): boolean {\n return Boolean(this.get(key, locale));\n },\n\n hasKey(key: string): boolean {\n return getRegistry().has(key);\n },\n\n /** Return the locales that have a registered code default for the given key. */\n getLocalesForKey(key: string): string[] {\n const byLocale = getRegistry().get(key);\n return byLocale ? Array.from(byLocale.keys()) : [];\n },\n\n /** All registered (key, locale, template) triples. Used by the batch translator. */\n getAll(): LanguageStringDefinition[] {\n const all: LanguageStringDefinition[] = [];\n for (const byLocale of getRegistry().values()) {\n for (const definition of byLocale.values()) {\n all.push(definition);\n }\n }\n return all;\n },\n\n /** Distinct keys currently registered. */\n getKeys(): string[] {\n return Array.from(getRegistry().keys());\n },\n\n clear(): void {\n getRegistry().clear();\n },\n};\n\nexport function defineLanguageString(\n input: LanguageStringDefinitionInput,\n): LanguageStringDefinition {\n return LanguageRegistry.register(input);\n}\n"],"names":[],"mappings":"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"}