@happyvertical/smrt-languages 0.57.37 → 0.57.38

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.
@@ -1,4 +1,4 @@
1
- import { SmrtCollection, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
1
+ import { RuntimeError, SmrtCollection, SmrtObject, field, smrt } from "@happyvertical/smrt-core";
2
2
  import { createHash } from "node:crypto";
3
3
  //#region src/utils.ts
4
4
  function isPlainObject(value) {
@@ -166,7 +166,7 @@ var LanguageOverride = class extends SmrtObject {
166
166
  if (options.reviewed_at !== void 0) this.reviewed_at = options.reviewed_at;
167
167
  if (options.reviewed_by !== void 0) this.reviewed_by = options.reviewed_by;
168
168
  }
169
- async save() {
169
+ async save(options) {
170
170
  if (!this.key || this.key.trim() === "") throw new Error("LanguageOverride.key is required");
171
171
  if (!this.locale || this.locale.trim() === "") throw new Error("LanguageOverride.locale is required");
172
172
  if (typeof this.template !== "string") throw new Error("LanguageOverride.template must be a string");
@@ -174,25 +174,25 @@ var LanguageOverride = class extends SmrtObject {
174
174
  this.context = this.tenantId ?? "__app__";
175
175
  const previousIdentity = await this.getPersistedIdentity();
176
176
  const identityChanged = !!previousIdentity && (previousIdentity.key !== this.key || previousIdentity.locale !== this.locale || previousIdentity.tenantId !== this.tenantId);
177
- const result = identityChanged && previousIdentity ? await this.saveAfterIdentityChange() : await super.save();
177
+ const result = identityChanged && previousIdentity ? await this.saveAfterIdentityChange(options) : await super.save(options);
178
178
  if (identityChanged && previousIdentity) invalidateLanguageCache(previousIdentity.key, previousIdentity.locale, previousIdentity.tenantId, this.db);
179
179
  invalidateLanguageCache(this.key, this.locale, this.tenantId, this.db);
180
180
  return result;
181
181
  }
182
- async saveAfterIdentityChange() {
183
- if (typeof this.db.beginTransaction === "function") return this.saveAfterIdentityChangeInTransaction();
184
- return this.saveAfterIdentityChangeWithDeferredDelete();
182
+ async saveAfterIdentityChange(options) {
183
+ if (typeof this.db.beginTransaction === "function") return this.saveAfterIdentityChangeInTransaction(options);
184
+ return this.saveAfterIdentityChangeWithDeferredDelete(options);
185
185
  }
186
- async saveAfterIdentityChangeInTransaction() {
186
+ async saveAfterIdentityChangeInTransaction(options) {
187
187
  const originalDb = this._db;
188
188
  const originalOptionsDb = this.options.db;
189
189
  const tx = await this.db.beginTransaction?.();
190
- if (!tx) return this.saveAfterIdentityChangeWithDeferredDelete();
190
+ if (!tx) return this.saveAfterIdentityChangeWithDeferredDelete(options);
191
191
  try {
192
192
  this._db = tx;
193
193
  this.options.db = tx;
194
- await super.delete();
195
- const result = await super.save();
194
+ await super.delete(options?.expectedUpdatedAt === void 0 ? void 0 : { expectedUpdatedAt: options.expectedUpdatedAt });
195
+ const result = await super.save(withoutRevisionGuard(options));
196
196
  await tx.commit();
197
197
  return result;
198
198
  } catch (error) {
@@ -205,14 +205,18 @@ var LanguageOverride = class extends SmrtObject {
205
205
  this.options.db = originalOptionsDb;
206
206
  }
207
207
  }
208
- async saveAfterIdentityChangeWithDeferredDelete() {
208
+ async saveAfterIdentityChangeWithDeferredDelete(options) {
209
209
  const previousId = this.id;
210
- if (!previousId) return super.save();
210
+ if (!previousId) return super.save(options);
211
+ if (options?.expectedUpdatedAt !== void 0) throw new RuntimeError(`${this.constructor.name} identity changes honour expectedUpdatedAt only inside a transaction`, "RUNTIME_REVISION_GUARD_REQUIRES_TRANSACTION", {
212
+ className: this.constructor.name,
213
+ id: previousId
214
+ });
211
215
  const replacementId = crypto.randomUUID();
212
216
  let replacementSaved = false;
213
217
  this.id = replacementId;
214
218
  try {
215
- const result = await super.save();
219
+ const result = await super.save(withoutRevisionGuard(options));
216
220
  replacementSaved = true;
217
221
  await this.db.delete(this.tableName, { id: previousId });
218
222
  return result;
@@ -311,6 +315,11 @@ LanguageOverride = __decorateClass([smrt({
311
315
  },
312
316
  mcp: { include: [] }
313
317
  })], LanguageOverride);
318
+ function withoutRevisionGuard(options) {
319
+ if (!options || options.expectedUpdatedAt === void 0) return options;
320
+ const { expectedUpdatedAt: _expected, ...rest } = options;
321
+ return rest;
322
+ }
314
323
  //#endregion
315
324
  //#region src/collections/LanguageOverrideCollection.ts
316
325
  var LanguageOverrideCollection = class extends SmrtCollection {
@@ -458,4 +467,4 @@ function defineLanguageString(input) {
458
467
  //#endregion
459
468
  export { LanguageOverride as a, getLanguageCacheGeneration as c, setCachedLanguage as d, buildLocaleFallbackChain as f, renderTemplate as g, normalizeLocale as h, LanguageOverrideCollection as i, getLanguageCacheTtlMs as l, computeSourceHash as m, defineLanguageString as n, clearLanguageCache as o, buildTranslationJobId as p, buildTenantGlossary as r, getCachedLanguage as s, LanguageRegistry as t, invalidateLanguageCache as u };
460
469
 
461
- //# sourceMappingURL=language-registry-BO6kxyz8.js.map
470
+ //# sourceMappingURL=language-registry-sNjkO2BQ.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"language-registry-sNjkO2BQ.js","names":[],"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>();\n/**\n * Monotonic per-`(db, key, locale)` invalidation counter; see\n * getLanguageCacheGeneration.\n */\nconst cacheGenerations = new Map<string, number>();\n/**\n * Source of every generation value. Drawing from one counter instead of\n * incrementing each entry independently makes a generation unique across every\n * `(key, locale)` and every clear, which is what lets `clearedThrough` work as\n * a floor.\n */\nlet nextGeneration = 1;\n/**\n * Floor that `clearLanguageCache()` raises for *every* `(key, locale)`,\n * including pairs with no map entry. A pair that has never been invalidated in\n * this process reads as generation 0, so a resolution that captured 0 before a\n * clear would still see 0 after it and write its pre-clear value back — the\n * flush would be silently undone for the rest of the TTL.\n */\nlet clearedThrough = 0;\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\nfunction buildGenerationKey(\n key: string,\n locale: string,\n db: DatabaseInterface | unknown,\n): string {\n return `${getDbNamespace(db)}::${key}::${normalizeLocale(locale)}`;\n}\n\nfunction bumpGeneration(\n key: string,\n locale: string,\n db: DatabaseInterface | unknown,\n): void {\n cacheGenerations.set(buildGenerationKey(key, locale, db), nextGeneration++);\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\n/**\n * Reads the current invalidation generation for a `(key, locale)` pair.\n *\n * A resolution captures this *before* its asynchronous layer loads and hands\n * it back to {@link setCachedLanguage}. Any write that lands while those loads\n * are in flight bumps the generation, so the in-flight resolution — which read\n * the pre-write layers — is refused the cache write instead of repopulating\n * the entry the write just invalidated. Without this, a read that raced a\n * write served the pre-write value for the full TTL, and a raced `delete()`\n * resurrected the deleted override.\n *\n * Granularity is `(db, key, locale)`, not `(db, key)` as in smrt-playbooks and\n * smrt-prompts, and not `(db, key, locale, tenantId)`:\n *\n * - `locale` is part of the key because every layer a cached entry is built\n * from is read at exactly the locale it is cached under. `resolveLanguageString`\n * walks a fallback chain (`fr-CA` → `fr` → `en`), but each attempt reads its\n * own locale's rows (`getTenantOverride`/`getAppOverride` are called with\n * that locale) and caches the result under that same locale; an attempt that\n * resolves nothing caches nothing, so a fallback hit is never stored under\n * the requested locale. Every writer — `LanguageOverride.save()`/`delete()`\n * (both identities, when one changes) and the translation job — invalidates\n * the same `(key, locale)` it wrote. A per-locale generation therefore still\n * covers every write that can change what a cached entry was built from.\n * - `tenantId` is deliberately *not* part of the key, because an app-level row\n * (tenantId null) is inherited by every tenant, so a write to any scope of a\n * `(key, locale)` must be able to refuse an in-flight write for any other\n * scope of it. This matches the coarsest fan-out of\n * {@link invalidateLanguageCache}.\n *\n * A pair with no entry of its own reports the {@link clearLanguageCache} floor\n * rather than a bare 0, so a clear refuses in-flight writes for pairs that have\n * never been invalidated too.\n */\nexport function getLanguageCacheGeneration(\n key: string,\n locale: string,\n db: DatabaseInterface | unknown,\n): number {\n const generation =\n cacheGenerations.get(buildGenerationKey(key, locale, db)) ?? 0;\n return generation > clearedThrough ? generation : clearedThrough;\n}\n\nexport function setCachedLanguage(\n key: string,\n locale: string,\n tenantId: string | null | undefined,\n db: DatabaseInterface | unknown,\n value: LanguageCacheValue,\n loadedAtGeneration: number,\n): void {\n if (getLanguageCacheGeneration(key, locale, db) !== loadedAtGeneration) {\n // A write landed while this resolution was loading. Its value is already\n // stale, so drop it rather than poison the entry for the whole TTL.\n return;\n }\n\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 bumpGeneration(key, normalizedLocale, db);\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 // Raise the floor instead of resetting counters: a resolution that started\n // before the clear must be refused its write, and raising a single floor\n // covers pairs with no generation entry, which a per-entry bump cannot reach.\n // With the floor carrying the refusal, the per-entry values can be dropped.\n clearedThrough = nextGeneration++;\n cacheGenerations.clear();\n}\n","import {\n field,\n RuntimeError,\n SmrtObject,\n type SmrtObjectOptions,\n type SmrtSaveOptions,\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 { static __smrtPackage__ = \"@happyvertical/smrt-languages\";\n @field({ required: true })\n key: string = '';\n\n @field({ required: true })\n locale: string = '';\n\n // Not tenant-scoped (any scope is writable), but the value is a tenant id:\n // uuid on Postgres, so it compares with every other tenant_id column.\n @field({ sqlType: 'UUID', 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(options?: SmrtSaveOptions): 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(options)` 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(options)\n : await super.save(options);\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(\n options?: SmrtSaveOptions,\n ): Promise<this> {\n if (typeof this.db.beginTransaction === 'function') {\n return this.saveAfterIdentityChangeInTransaction(options);\n }\n return this.saveAfterIdentityChangeWithDeferredDelete(options);\n }\n\n private async saveAfterIdentityChangeInTransaction(\n options?: SmrtSaveOptions,\n ): 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(options);\n }\n\n try {\n this._db = tx;\n this.options.db = tx;\n // The caller's revision guards the ORIGINAL row (checked by the delete);\n // the replacement is a fresh insert and must not carry the update guard.\n await super.delete(\n options?.expectedUpdatedAt === undefined\n ? undefined\n : { expectedUpdatedAt: options.expectedUpdatedAt },\n );\n const result = await super.save(withoutRevisionGuard(options));\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(\n options?: SmrtSaveOptions,\n ): Promise<this> {\n const previousId = this.id;\n if (!previousId) {\n return super.save(options);\n }\n\n // Without a transaction, a check here and the later unguarded delete are\n // not atomic: a concurrent update could be deleted. Fail closed rather\n // than accept a revision guard that cannot be honoured (#4408).\n if (options?.expectedUpdatedAt !== undefined) {\n throw new RuntimeError(\n `${this.constructor.name} identity changes honour expectedUpdatedAt only inside a transaction`,\n 'RUNTIME_REVISION_GUARD_REQUIRES_TRANSACTION',\n { className: this.constructor.name, id: previousId },\n );\n }\n\n const replacementId = crypto.randomUUID();\n let replacementSaved = false;\n this.id = replacementId;\n\n try {\n const result = await super.save(withoutRevisionGuard(options));\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\n/** The replacement row of an identity change is an insert, not a guarded update. */\nfunction withoutRevisionGuard(\n options: SmrtSaveOptions | undefined,\n): SmrtSaveOptions | undefined {\n if (!options || options.expectedUpdatedAt === undefined) return options;\n const { expectedUpdatedAt: _expected, ...rest } = options;\n return rest;\n}\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"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { h as normalizeLocale, i as LanguageOverrideCollection, m as computeSourceHash, p as buildTranslationJobId, r as buildTenantGlossary, t as LanguageRegistry, u as invalidateLanguageCache } from "./language-registry-BO6kxyz8.js";
1
+ import { h as normalizeLocale, i as LanguageOverrideCollection, m as computeSourceHash, p as buildTranslationJobId, r as buildTenantGlossary, t as LanguageRegistry, u as invalidateLanguageCache } from "./language-registry-sNjkO2BQ.js";
2
2
  import { SmrtObject, smrt } from "@happyvertical/smrt-core";
3
3
  import { getPackageConfig } from "@happyvertical/smrt-config";
4
4
  import { getAI } from "@happyvertical/ai";
@@ -262,4 +262,4 @@ function containsObviousMarkupLeak(value) {
262
262
  //#endregion
263
263
  export { translation_job_exports as a, enqueueTranslationJob as i, LanguageTranslationTask as n, TRANSLATION_PROMPT_KEY as r, AUTO_TRANSLATE_FEATURE_KEY as t };
264
264
 
265
- //# sourceMappingURL=translation-job-B4sdwQ3W.js.map
265
+ //# sourceMappingURL=translation-job-DMWfy7m4.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"translation-job-B4sdwQ3W.js","names":[],"sources":["../../src/jobs/translation-job.ts"],"sourcesContent":["import { type GetAIOptions, getAI } from '@happyvertical/ai';\nimport { createLogger } from '@happyvertical/logger';\nimport { getPackageConfig } from '@happyvertical/smrt-config';\nimport {\n type SmrtClassOptions,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { FeatureResolver } from '@happyvertical/smrt-features';\nimport { SmrtJobCollection } from '@happyvertical/smrt-jobs';\nimport { definePrompt, resolvePrompt } from '@happyvertical/smrt-prompts';\nimport { invalidateLanguageCache } from '../cache.js';\nimport { LanguageOverrideCollection } from '../collections/LanguageOverrideCollection.js';\nimport { buildTenantGlossary } from '../glossary.js';\nimport { LanguageRegistry } from '../language-registry.js';\nimport type {\n LanguagesPackageConfig,\n TranslationJobPayload,\n} from '../types.js';\nimport {\n buildTranslationJobId,\n computeSourceHash,\n normalizeLocale,\n} from '../utils.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Feature flag key honored as the kill-switch for AI auto-translation. */\nexport const AUTO_TRANSLATE_FEATURE_KEY = 'smrt-languages.auto_translate';\n\n/**\n * Prompt key registered with `smrt-prompts` so ops can tune the AI translation\n * style without redeploying. The prompt itself is referenced by the job\n * handler when calling `@happyvertical/ai`.\n */\nexport const TRANSLATION_PROMPT_KEY = 'smrt-languages.translation';\n\n/**\n * Brace-containing example values that must reach the model verbatim. The\n * prompt renderer treats every `{...}` as a variable, so anything we want the\n * model to literally see (the placeholder syntax we're asking it to preserve,\n * the JSON shape we're asking it to return) is injected via a variable rather\n * than being baked into the static template.\n */\nconst PLACEHOLDER_EXAMPLE = '\"{name}\"';\nconst RESPONSE_SHAPE_EXAMPLE = '{\"translation\": \"Hola, {name}\"}';\n\ndefinePrompt({\n key: TRANSLATION_PROMPT_KEY,\n template:\n 'You translate user-facing application strings from {sourceLocale} to {targetLocale}.\\n' +\n 'Preserve any placeholders such as {placeholderExample} exactly as they appear, including the braces. Do not add markup or commentary.\\n' +\n 'Match the organization glossary where applicable:\\n{glossary}\\n' +\n 'String to translate: \"{template}\"\\n' +\n 'Reply with a JSON object shaped like {responseShapeExample} where the translation field is your translated string.',\n editable: {\n template: true,\n profile: true,\n model: true,\n params: true,\n },\n});\n\n/**\n * SmrtObject that owns the translation job's `execute` method. The TaskRunner\n * resolves jobs by `objectType` so the registered class name needs to match\n * what `enqueueTranslationJob` writes.\n */\n@smrt({\n api: { include: [] },\n cli: { include: [] },\n mcp: { include: [] },\n})\nexport class LanguageTranslationTask extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-languages\";\n /**\n * Job-runner entrypoint. Reads the translation payload, calls\n * `@happyvertical/ai`, and upserts a `LanguageOverride` row.\n */\n async execute(args: TranslationJobPayload): Promise<{\n skipped?: 'feature_disabled' | 'not_supported' | 'budget' | 'stale';\n template?: string;\n }> {\n if (!args?.key || !args.targetLocale || !args.sourceTemplate) {\n throw new Error(\n 'LanguageTranslationTask.execute requires a translation payload',\n );\n }\n\n const config = getLanguagesConfig();\n const targetLocale = normalizeLocale(args.targetLocale);\n const sourceLocale = normalizeLocale(args.sourceLocale);\n\n // Locale allowlist (cheap pre-check before any AI / DB work).\n if (\n Array.isArray(config.supportedLocales) &&\n config.supportedLocales.length > 0 &&\n !config.supportedLocales.map(normalizeLocale).includes(targetLocale)\n ) {\n return { skipped: 'not_supported' };\n }\n\n // Feature flag kill switch (best-effort — never blocks if unavailable).\n if (await isAutoTranslateDisabled(args.tenantId ?? null, this.options)) {\n return { skipped: 'feature_disabled' };\n }\n\n const overrides = await LanguageOverrideCollection.create(this.options);\n\n // Source-hash gate: if a translation already exists with the same source,\n // do nothing. Only re-translate when the source changed.\n const existing = await overrides.getAppOverride(args.key, targetLocale);\n if (existing?.auto_generated && existing.source_hash === args.sourceHash) {\n return { skipped: 'stale', template: existing.template };\n }\n if (existing && !existing.auto_generated) {\n // Human-edited rows are never overwritten.\n return { skipped: 'stale', template: existing.template };\n }\n\n // Per-tenant daily budget.\n if (\n args.tenantId &&\n typeof config.translationBudgetPerTenantPerDay === 'number'\n ) {\n const used = await countTodayTenantTranslations(\n this.options,\n args.tenantId,\n );\n if (used >= config.translationBudgetPerTenantPerDay) {\n return { skipped: 'budget' };\n }\n }\n\n // Build glossary from tenant overrides (no-op when no tenant context).\n let glossary = '';\n if (args.tenantId) {\n const tenantOverrides = await overrides.listTenantOverrides(\n args.tenantId,\n );\n glossary = buildTenantGlossary(tenantOverrides, {\n sourceLocale,\n targetLocale,\n max: 25,\n });\n }\n if (!glossary) {\n glossary = '(no organization glossary)';\n }\n\n // Pass the task's DB into resolvePrompt so any app/tenant-level prompt\n // overrides stored in `_smrt_prompt_overrides` are honored — that's the\n // whole reason we register the translation prompt with smrt-prompts in\n // the first place.\n const prompt = await resolvePrompt(TRANSLATION_PROMPT_KEY, {\n db: this.options.db,\n tenantId: args.tenantId ?? null,\n variables: {\n sourceLocale,\n targetLocale,\n template: args.sourceTemplate,\n glossary,\n placeholderExample: PLACEHOLDER_EXAMPLE,\n responseShapeExample: RESPONSE_SHAPE_EXAMPLE,\n },\n });\n\n // Single merged AI config: per-job model override (`args.model`) wins\n // over the prompt's `ai.model`, and the same merge feeds both `getAI()`\n // (client construction) and `ai.message()` (request options) so the\n // override actually takes effect end-to-end.\n const promptAi = (prompt.ai ?? {}) as Record<string, unknown>;\n const mergedAiConfig: Record<string, unknown> = { ...promptAi };\n if (args.model) mergedAiConfig.model = args.model;\n const ai = await getAI(mergedAiConfig as GetAIOptions);\n\n const message = await ai.message(prompt.text, {\n ...mergedAiConfig,\n responseFormat: { type: 'json_object' },\n });\n\n const translated = parseTranslationResponse(message);\n if (!translated) {\n throw new Error(\n `LanguageTranslationTask: AI returned no usable translation for \"${args.key}\" → ${targetLocale}`,\n );\n }\n\n if (containsObviousMarkupLeak(translated)) {\n throw new Error(\n `LanguageTranslationTask: refusing to persist suspicious translation for \"${args.key}\"`,\n );\n }\n\n // Persist the model that actually produced the translation, not just the\n // prompt's default — `args.model` overrides the prompt's `ai.model`.\n const aiModel =\n (args.model as string | undefined) ??\n (promptAi.model as string | undefined) ??\n null;\n const sourceHash =\n args.sourceHash || computeSourceHash(args.sourceTemplate);\n\n if (existing) {\n existing.template = translated;\n existing.auto_generated = true;\n existing.source_hash = sourceHash;\n existing.ai_model = aiModel;\n existing.reviewed_at = null;\n existing.reviewed_by = null;\n await existing.save();\n } else {\n // Use the collection's create() so the new row is initialized against\n // the same DB the task is running on. Constructing `new LanguageOverride`\n // directly leaves it un-initialized and `.save()` then trips on the\n // \"Database accessed before initialization\" guard.\n await overrides.create({\n key: args.key,\n locale: targetLocale,\n tenantId: null,\n template: translated,\n auto_generated: true,\n source_hash: sourceHash,\n ai_model: aiModel,\n reviewed_at: null,\n reviewed_by: null,\n });\n }\n\n invalidateLanguageCache(args.key, targetLocale, null, this.db);\n return { template: translated };\n }\n}\n\ninterface EnqueueTranslationOptions {\n key: string;\n targetLocale: string;\n sourceLocale?: string;\n tenantId?: string | null;\n db: SmrtClassOptions['db'];\n /** When true, skip the dedup check and force a fresh job. */\n force?: boolean;\n}\n\n/**\n * Enqueue a translation job for `(key, targetLocale)`, deduplicated against\n * any already-pending job for the same target. Returns the (possibly existing)\n * job's deterministic ID.\n */\nexport async function enqueueTranslationJob(\n options: EnqueueTranslationOptions,\n): Promise<{ id: string; status: 'enqueued' | 'duplicate' | 'skipped' }> {\n const targetLocale = normalizeLocale(options.targetLocale);\n const sourceLocale = normalizeLocale(options.sourceLocale ?? 'en');\n const dedupId = buildTranslationJobId(options.key, targetLocale);\n\n const definition = LanguageRegistry.get(options.key, sourceLocale);\n if (!definition) {\n return { id: dedupId, status: 'skipped' };\n }\n\n const config = getLanguagesConfig();\n if (\n Array.isArray(config.supportedLocales) &&\n config.supportedLocales.length > 0 &&\n !config.supportedLocales.map(normalizeLocale).includes(targetLocale)\n ) {\n return { id: dedupId, status: 'skipped' };\n }\n\n const jobs = await SmrtJobCollection.create({ db: options.db });\n\n if (!options.force) {\n const existing = await findPendingTranslationJob(\n jobs,\n options.key,\n targetLocale,\n );\n if (existing) {\n return { id: dedupId, status: 'duplicate' };\n }\n }\n\n const payload: TranslationJobPayload = {\n key: options.key,\n sourceLocale,\n sourceTemplate: definition.template,\n sourceHash: definition.sourceHash,\n targetLocale,\n tenantId: options.tenantId ?? null,\n };\n\n // Stamp tenantId on the SmrtJob row so the per-tenant daily budget query\n // (which filters by `tenantId`) actually counts this job. SmrtJob.save()\n // will fall back to `getTenantId()` from AsyncLocalStorage when undefined,\n // but we may be enqueueing from outside a tenant context (e.g. resolver\n // miss with an explicit tenantId option), so set it explicitly here.\n const job = await jobs.create({\n queue: 'languages',\n objectType: 'LanguageTranslationTask',\n objectId: null,\n method: 'execute',\n tenantId: options.tenantId ?? null,\n args: { ...payload, _dedupId: dedupId },\n runAt: new Date(),\n priority: 25,\n });\n await job.save();\n\n return { id: dedupId, status: 'enqueued' };\n}\n\n/**\n * In-memory match cap for the dedup scan. We pull the most recently-queued\n * language jobs and check them in JS because querying inside a JSON column\n * portably across SQLite/Postgres is fiddly. If the working queue is deeper\n * than this, the scan truncates — when that happens we log a warning instead\n * of silently letting duplicate jobs slip through, and the handler's\n * source-hash gate (`execute()`) still prevents redundant AI calls. Tune via\n * `packages.languages.dedupScanLimit` if your steady-state pending queue\n * regularly exceeds the default.\n */\nconst DEFAULT_DEDUP_SCAN_LIMIT = 500;\n\nasync function findPendingTranslationJob(\n jobs: SmrtJobCollection,\n key: string,\n targetLocale: string,\n): Promise<unknown | null> {\n const dedupId = buildTranslationJobId(key, targetLocale);\n const config = getLanguagesConfig();\n const scanLimit =\n typeof (config as { dedupScanLimit?: number }).dedupScanLimit === 'number'\n ? (config as { dedupScanLimit: number }).dedupScanLimit\n : DEFAULT_DEDUP_SCAN_LIMIT;\n\n const rows = await jobs.list({\n where: {\n objectType: 'LanguageTranslationTask',\n method: 'execute',\n status: ['pending', 'running'],\n queue: 'languages',\n },\n orderBy: 'createdAt DESC',\n limit: scanLimit,\n });\n\n for (const row of rows) {\n const args = (row as { args?: Record<string, unknown> }).args ?? {};\n if (args._dedupId === dedupId) return row;\n if (\n args.key === key &&\n typeof args.targetLocale === 'string' &&\n normalizeLocale(args.targetLocale as string) === targetLocale\n ) {\n return row;\n }\n }\n\n if (rows.length === scanLimit) {\n // Don't crash; the handler's source-hash gate is the durable safeguard.\n // But surface a warning so operators notice when the queue depth has\n // outgrown the in-memory match window.\n logger.warn(\n `[smrt-languages] translation-job dedup scan hit its limit (${scanLimit}); raise packages.languages.dedupScanLimit if duplicate jobs appear`,\n );\n }\n return null;\n}\n\nasync function countTodayTenantTranslations(\n options: SmrtClassOptions,\n tenantId: string,\n): Promise<number> {\n // SmrtCollection.list() encodes operators into the where-clause key (e.g.\n // `'createdAt >='`), not as `{ op, value }` objects — using the wrong shape\n // either throws or matches nothing, and a swallowed failure here silently\n // disables the budget. Let the call propagate so configuration mistakes\n // surface immediately rather than as a missed throttle in production.\n const jobs = await SmrtJobCollection.create({ db: options.db });\n const since = new Date();\n since.setUTCHours(0, 0, 0, 0);\n const rows = await jobs.list({\n where: {\n objectType: 'LanguageTranslationTask',\n method: 'execute',\n tenantId,\n 'createdAt >=': since.toISOString(),\n },\n });\n return rows.length;\n}\n\nfunction getLanguagesConfig(): LanguagesPackageConfig {\n return getPackageConfig<LanguagesPackageConfig>('languages', {\n defaultLocale: 'en',\n overrides: {},\n });\n}\n\nasync function isAutoTranslateDisabled(\n tenantId: string | null,\n options: SmrtClassOptions,\n): Promise<boolean> {\n try {\n const resolver = new FeatureResolver(options);\n const enabled = await resolver.isEnabled(AUTO_TRANSLATE_FEATURE_KEY, {\n tenantId: tenantId ?? undefined,\n });\n return enabled === false;\n } catch {\n // If features package can't resolve (no definition synced, etc.), default\n // to enabled — operators opt out by registering the flag.\n return false;\n }\n}\n\nfunction parseTranslationResponse(\n message: string | null | undefined,\n): string | null {\n if (!message) return null;\n\n // We requested `responseFormat: json_object` so the model is supposed to\n // return a JSON object with a `translation` string. If parsing succeeds but\n // the shape is wrong (e.g. `{\"text\":\"Hola\"}`), we MUST NOT fall through to\n // the bare-string path — that would persist the whole JSON blob as the\n // translation. Only the parse-failed branch may try to use the raw message\n // as a literal string (some providers occasionally return prose despite\n // the format hint).\n let parseFailed = false;\n let parsed: unknown;\n try {\n parsed = JSON.parse(message);\n } catch {\n parseFailed = true;\n }\n\n if (!parseFailed) {\n if (\n parsed &&\n typeof parsed === 'object' &&\n !Array.isArray(parsed) &&\n typeof (parsed as { translation?: unknown }).translation === 'string'\n ) {\n const cleaned = (parsed as { translation: string }).translation.trim();\n return cleaned.length > 0 ? cleaned : null;\n }\n if (typeof parsed === 'string') {\n const cleaned = parsed.trim();\n return cleaned.length > 0 ? cleaned : null;\n }\n return null;\n }\n\n const trimmed = String(message).trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\nfunction containsObviousMarkupLeak(value: string): boolean {\n // Cheap defense: refuse responses that look like raw HTML/system tags. We\n // can tighten this in v1.1 once we have telemetry on real failure modes.\n return /<\\/?(script|html|body|iframe|system)/i.test(value);\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"translation-job-DMWfy7m4.js","names":[],"sources":["../../src/jobs/translation-job.ts"],"sourcesContent":["import { type GetAIOptions, getAI } from '@happyvertical/ai';\nimport { createLogger } from '@happyvertical/logger';\nimport { getPackageConfig } from '@happyvertical/smrt-config';\nimport {\n type SmrtClassOptions,\n SmrtObject,\n smrt,\n} from '@happyvertical/smrt-core';\nimport { FeatureResolver } from '@happyvertical/smrt-features';\nimport { SmrtJobCollection } from '@happyvertical/smrt-jobs';\nimport { definePrompt, resolvePrompt } from '@happyvertical/smrt-prompts';\nimport { invalidateLanguageCache } from '../cache.js';\nimport { LanguageOverrideCollection } from '../collections/LanguageOverrideCollection.js';\nimport { buildTenantGlossary } from '../glossary.js';\nimport { LanguageRegistry } from '../language-registry.js';\nimport type {\n LanguagesPackageConfig,\n TranslationJobPayload,\n} from '../types.js';\nimport {\n buildTranslationJobId,\n computeSourceHash,\n normalizeLocale,\n} from '../utils.js';\n\nconst logger = createLogger({ level: 'info' });\n\n/** Feature flag key honored as the kill-switch for AI auto-translation. */\nexport const AUTO_TRANSLATE_FEATURE_KEY = 'smrt-languages.auto_translate';\n\n/**\n * Prompt key registered with `smrt-prompts` so ops can tune the AI translation\n * style without redeploying. The prompt itself is referenced by the job\n * handler when calling `@happyvertical/ai`.\n */\nexport const TRANSLATION_PROMPT_KEY = 'smrt-languages.translation';\n\n/**\n * Brace-containing example values that must reach the model verbatim. The\n * prompt renderer treats every `{...}` as a variable, so anything we want the\n * model to literally see (the placeholder syntax we're asking it to preserve,\n * the JSON shape we're asking it to return) is injected via a variable rather\n * than being baked into the static template.\n */\nconst PLACEHOLDER_EXAMPLE = '\"{name}\"';\nconst RESPONSE_SHAPE_EXAMPLE = '{\"translation\": \"Hola, {name}\"}';\n\ndefinePrompt({\n key: TRANSLATION_PROMPT_KEY,\n template:\n 'You translate user-facing application strings from {sourceLocale} to {targetLocale}.\\n' +\n 'Preserve any placeholders such as {placeholderExample} exactly as they appear, including the braces. Do not add markup or commentary.\\n' +\n 'Match the organization glossary where applicable:\\n{glossary}\\n' +\n 'String to translate: \"{template}\"\\n' +\n 'Reply with a JSON object shaped like {responseShapeExample} where the translation field is your translated string.',\n editable: {\n template: true,\n profile: true,\n model: true,\n params: true,\n },\n});\n\n/**\n * SmrtObject that owns the translation job's `execute` method. The TaskRunner\n * resolves jobs by `objectType` so the registered class name needs to match\n * what `enqueueTranslationJob` writes.\n */\n@smrt({\n api: { include: [] },\n cli: { include: [] },\n mcp: { include: [] },\n})\nexport class LanguageTranslationTask extends SmrtObject { static __smrtPackage__ = \"@happyvertical/smrt-languages\";\n /**\n * Job-runner entrypoint. Reads the translation payload, calls\n * `@happyvertical/ai`, and upserts a `LanguageOverride` row.\n */\n async execute(args: TranslationJobPayload): Promise<{\n skipped?: 'feature_disabled' | 'not_supported' | 'budget' | 'stale';\n template?: string;\n }> {\n if (!args?.key || !args.targetLocale || !args.sourceTemplate) {\n throw new Error(\n 'LanguageTranslationTask.execute requires a translation payload',\n );\n }\n\n const config = getLanguagesConfig();\n const targetLocale = normalizeLocale(args.targetLocale);\n const sourceLocale = normalizeLocale(args.sourceLocale);\n\n // Locale allowlist (cheap pre-check before any AI / DB work).\n if (\n Array.isArray(config.supportedLocales) &&\n config.supportedLocales.length > 0 &&\n !config.supportedLocales.map(normalizeLocale).includes(targetLocale)\n ) {\n return { skipped: 'not_supported' };\n }\n\n // Feature flag kill switch (best-effort — never blocks if unavailable).\n if (await isAutoTranslateDisabled(args.tenantId ?? null, this.options)) {\n return { skipped: 'feature_disabled' };\n }\n\n const overrides = await LanguageOverrideCollection.create(this.options);\n\n // Source-hash gate: if a translation already exists with the same source,\n // do nothing. Only re-translate when the source changed.\n const existing = await overrides.getAppOverride(args.key, targetLocale);\n if (existing?.auto_generated && existing.source_hash === args.sourceHash) {\n return { skipped: 'stale', template: existing.template };\n }\n if (existing && !existing.auto_generated) {\n // Human-edited rows are never overwritten.\n return { skipped: 'stale', template: existing.template };\n }\n\n // Per-tenant daily budget.\n if (\n args.tenantId &&\n typeof config.translationBudgetPerTenantPerDay === 'number'\n ) {\n const used = await countTodayTenantTranslations(\n this.options,\n args.tenantId,\n );\n if (used >= config.translationBudgetPerTenantPerDay) {\n return { skipped: 'budget' };\n }\n }\n\n // Build glossary from tenant overrides (no-op when no tenant context).\n let glossary = '';\n if (args.tenantId) {\n const tenantOverrides = await overrides.listTenantOverrides(\n args.tenantId,\n );\n glossary = buildTenantGlossary(tenantOverrides, {\n sourceLocale,\n targetLocale,\n max: 25,\n });\n }\n if (!glossary) {\n glossary = '(no organization glossary)';\n }\n\n // Pass the task's DB into resolvePrompt so any app/tenant-level prompt\n // overrides stored in `_smrt_prompt_overrides` are honored — that's the\n // whole reason we register the translation prompt with smrt-prompts in\n // the first place.\n const prompt = await resolvePrompt(TRANSLATION_PROMPT_KEY, {\n db: this.options.db,\n tenantId: args.tenantId ?? null,\n variables: {\n sourceLocale,\n targetLocale,\n template: args.sourceTemplate,\n glossary,\n placeholderExample: PLACEHOLDER_EXAMPLE,\n responseShapeExample: RESPONSE_SHAPE_EXAMPLE,\n },\n });\n\n // Single merged AI config: per-job model override (`args.model`) wins\n // over the prompt's `ai.model`, and the same merge feeds both `getAI()`\n // (client construction) and `ai.message()` (request options) so the\n // override actually takes effect end-to-end.\n const promptAi = (prompt.ai ?? {}) as Record<string, unknown>;\n const mergedAiConfig: Record<string, unknown> = { ...promptAi };\n if (args.model) mergedAiConfig.model = args.model;\n const ai = await getAI(mergedAiConfig as GetAIOptions);\n\n const message = await ai.message(prompt.text, {\n ...mergedAiConfig,\n responseFormat: { type: 'json_object' },\n });\n\n const translated = parseTranslationResponse(message);\n if (!translated) {\n throw new Error(\n `LanguageTranslationTask: AI returned no usable translation for \"${args.key}\" → ${targetLocale}`,\n );\n }\n\n if (containsObviousMarkupLeak(translated)) {\n throw new Error(\n `LanguageTranslationTask: refusing to persist suspicious translation for \"${args.key}\"`,\n );\n }\n\n // Persist the model that actually produced the translation, not just the\n // prompt's default — `args.model` overrides the prompt's `ai.model`.\n const aiModel =\n (args.model as string | undefined) ??\n (promptAi.model as string | undefined) ??\n null;\n const sourceHash =\n args.sourceHash || computeSourceHash(args.sourceTemplate);\n\n if (existing) {\n existing.template = translated;\n existing.auto_generated = true;\n existing.source_hash = sourceHash;\n existing.ai_model = aiModel;\n existing.reviewed_at = null;\n existing.reviewed_by = null;\n await existing.save();\n } else {\n // Use the collection's create() so the new row is initialized against\n // the same DB the task is running on. Constructing `new LanguageOverride`\n // directly leaves it un-initialized and `.save()` then trips on the\n // \"Database accessed before initialization\" guard.\n await overrides.create({\n key: args.key,\n locale: targetLocale,\n tenantId: null,\n template: translated,\n auto_generated: true,\n source_hash: sourceHash,\n ai_model: aiModel,\n reviewed_at: null,\n reviewed_by: null,\n });\n }\n\n invalidateLanguageCache(args.key, targetLocale, null, this.db);\n return { template: translated };\n }\n}\n\ninterface EnqueueTranslationOptions {\n key: string;\n targetLocale: string;\n sourceLocale?: string;\n tenantId?: string | null;\n db: SmrtClassOptions['db'];\n /** When true, skip the dedup check and force a fresh job. */\n force?: boolean;\n}\n\n/**\n * Enqueue a translation job for `(key, targetLocale)`, deduplicated against\n * any already-pending job for the same target. Returns the (possibly existing)\n * job's deterministic ID.\n */\nexport async function enqueueTranslationJob(\n options: EnqueueTranslationOptions,\n): Promise<{ id: string; status: 'enqueued' | 'duplicate' | 'skipped' }> {\n const targetLocale = normalizeLocale(options.targetLocale);\n const sourceLocale = normalizeLocale(options.sourceLocale ?? 'en');\n const dedupId = buildTranslationJobId(options.key, targetLocale);\n\n const definition = LanguageRegistry.get(options.key, sourceLocale);\n if (!definition) {\n return { id: dedupId, status: 'skipped' };\n }\n\n const config = getLanguagesConfig();\n if (\n Array.isArray(config.supportedLocales) &&\n config.supportedLocales.length > 0 &&\n !config.supportedLocales.map(normalizeLocale).includes(targetLocale)\n ) {\n return { id: dedupId, status: 'skipped' };\n }\n\n const jobs = await SmrtJobCollection.create({ db: options.db });\n\n if (!options.force) {\n const existing = await findPendingTranslationJob(\n jobs,\n options.key,\n targetLocale,\n );\n if (existing) {\n return { id: dedupId, status: 'duplicate' };\n }\n }\n\n const payload: TranslationJobPayload = {\n key: options.key,\n sourceLocale,\n sourceTemplate: definition.template,\n sourceHash: definition.sourceHash,\n targetLocale,\n tenantId: options.tenantId ?? null,\n };\n\n // Stamp tenantId on the SmrtJob row so the per-tenant daily budget query\n // (which filters by `tenantId`) actually counts this job. SmrtJob.save()\n // will fall back to `getTenantId()` from AsyncLocalStorage when undefined,\n // but we may be enqueueing from outside a tenant context (e.g. resolver\n // miss with an explicit tenantId option), so set it explicitly here.\n const job = await jobs.create({\n queue: 'languages',\n objectType: 'LanguageTranslationTask',\n objectId: null,\n method: 'execute',\n tenantId: options.tenantId ?? null,\n args: { ...payload, _dedupId: dedupId },\n runAt: new Date(),\n priority: 25,\n });\n await job.save();\n\n return { id: dedupId, status: 'enqueued' };\n}\n\n/**\n * In-memory match cap for the dedup scan. We pull the most recently-queued\n * language jobs and check them in JS because querying inside a JSON column\n * portably across SQLite/Postgres is fiddly. If the working queue is deeper\n * than this, the scan truncates — when that happens we log a warning instead\n * of silently letting duplicate jobs slip through, and the handler's\n * source-hash gate (`execute()`) still prevents redundant AI calls. Tune via\n * `packages.languages.dedupScanLimit` if your steady-state pending queue\n * regularly exceeds the default.\n */\nconst DEFAULT_DEDUP_SCAN_LIMIT = 500;\n\nasync function findPendingTranslationJob(\n jobs: SmrtJobCollection,\n key: string,\n targetLocale: string,\n): Promise<unknown | null> {\n const dedupId = buildTranslationJobId(key, targetLocale);\n const config = getLanguagesConfig();\n const scanLimit =\n typeof (config as { dedupScanLimit?: number }).dedupScanLimit === 'number'\n ? (config as { dedupScanLimit: number }).dedupScanLimit\n : DEFAULT_DEDUP_SCAN_LIMIT;\n\n const rows = await jobs.list({\n where: {\n objectType: 'LanguageTranslationTask',\n method: 'execute',\n status: ['pending', 'running'],\n queue: 'languages',\n },\n orderBy: 'createdAt DESC',\n limit: scanLimit,\n });\n\n for (const row of rows) {\n const args = (row as { args?: Record<string, unknown> }).args ?? {};\n if (args._dedupId === dedupId) return row;\n if (\n args.key === key &&\n typeof args.targetLocale === 'string' &&\n normalizeLocale(args.targetLocale as string) === targetLocale\n ) {\n return row;\n }\n }\n\n if (rows.length === scanLimit) {\n // Don't crash; the handler's source-hash gate is the durable safeguard.\n // But surface a warning so operators notice when the queue depth has\n // outgrown the in-memory match window.\n logger.warn(\n `[smrt-languages] translation-job dedup scan hit its limit (${scanLimit}); raise packages.languages.dedupScanLimit if duplicate jobs appear`,\n );\n }\n return null;\n}\n\nasync function countTodayTenantTranslations(\n options: SmrtClassOptions,\n tenantId: string,\n): Promise<number> {\n // SmrtCollection.list() encodes operators into the where-clause key (e.g.\n // `'createdAt >='`), not as `{ op, value }` objects — using the wrong shape\n // either throws or matches nothing, and a swallowed failure here silently\n // disables the budget. Let the call propagate so configuration mistakes\n // surface immediately rather than as a missed throttle in production.\n const jobs = await SmrtJobCollection.create({ db: options.db });\n const since = new Date();\n since.setUTCHours(0, 0, 0, 0);\n const rows = await jobs.list({\n where: {\n objectType: 'LanguageTranslationTask',\n method: 'execute',\n tenantId,\n 'createdAt >=': since.toISOString(),\n },\n });\n return rows.length;\n}\n\nfunction getLanguagesConfig(): LanguagesPackageConfig {\n return getPackageConfig<LanguagesPackageConfig>('languages', {\n defaultLocale: 'en',\n overrides: {},\n });\n}\n\nasync function isAutoTranslateDisabled(\n tenantId: string | null,\n options: SmrtClassOptions,\n): Promise<boolean> {\n try {\n const resolver = new FeatureResolver(options);\n const enabled = await resolver.isEnabled(AUTO_TRANSLATE_FEATURE_KEY, {\n tenantId: tenantId ?? undefined,\n });\n return enabled === false;\n } catch {\n // If features package can't resolve (no definition synced, etc.), default\n // to enabled — operators opt out by registering the flag.\n return false;\n }\n}\n\nfunction parseTranslationResponse(\n message: string | null | undefined,\n): string | null {\n if (!message) return null;\n\n // We requested `responseFormat: json_object` so the model is supposed to\n // return a JSON object with a `translation` string. If parsing succeeds but\n // the shape is wrong (e.g. `{\"text\":\"Hola\"}`), we MUST NOT fall through to\n // the bare-string path — that would persist the whole JSON blob as the\n // translation. Only the parse-failed branch may try to use the raw message\n // as a literal string (some providers occasionally return prose despite\n // the format hint).\n let parseFailed = false;\n let parsed: unknown;\n try {\n parsed = JSON.parse(message);\n } catch {\n parseFailed = true;\n }\n\n if (!parseFailed) {\n if (\n parsed &&\n typeof parsed === 'object' &&\n !Array.isArray(parsed) &&\n typeof (parsed as { translation?: unknown }).translation === 'string'\n ) {\n const cleaned = (parsed as { translation: string }).translation.trim();\n return cleaned.length > 0 ? cleaned : null;\n }\n if (typeof parsed === 'string') {\n const cleaned = parsed.trim();\n return cleaned.length > 0 ? cleaned : null;\n }\n return null;\n }\n\n const trimmed = String(message).trim();\n return trimmed.length > 0 ? trimmed : null;\n}\n\nfunction containsObviousMarkupLeak(value: string): boolean {\n // Cheap defense: refuse responses that look like raw HTML/system tags. We\n // can tighten this in v1.1 once we have telemetry on real failure modes.\n return /<\\/?(script|html|body|iframe|system)/i.test(value);\n}\n"],"mappings":"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"}
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
- import { i as enqueueTranslationJob } from "./chunks/translation-job-B4sdwQ3W.js";
2
- import { h as normalizeLocale, i as LanguageOverrideCollection, t as LanguageRegistry } from "./chunks/language-registry-BO6kxyz8.js";
1
+ import { i as enqueueTranslationJob } from "./chunks/translation-job-DMWfy7m4.js";
2
+ import { h as normalizeLocale, i as LanguageOverrideCollection, t as LanguageRegistry } from "./chunks/language-registry-sNjkO2BQ.js";
3
3
  //#region src/cli.ts
4
4
  async function translateMissing(options) {
5
5
  const sourceLocale = normalizeLocale(options.sourceLocale ?? "en");
package/dist/index.js CHANGED
@@ -1,9 +1,9 @@
1
- import { a as LanguageOverride, c as getLanguageCacheGeneration, d as setCachedLanguage, f as buildLocaleFallbackChain, g as renderTemplate, h as normalizeLocale, i as LanguageOverrideCollection, l as getLanguageCacheTtlMs, m as computeSourceHash, n as defineLanguageString, o as clearLanguageCache, p as buildTranslationJobId, r as buildTenantGlossary, s as getCachedLanguage, t as LanguageRegistry, u as invalidateLanguageCache } from "./chunks/language-registry-BO6kxyz8.js";
1
+ import { a as LanguageOverride, c as getLanguageCacheGeneration, d as setCachedLanguage, f as buildLocaleFallbackChain, g as renderTemplate, h as normalizeLocale, i as LanguageOverrideCollection, l as getLanguageCacheTtlMs, m as computeSourceHash, n as defineLanguageString, o as clearLanguageCache, p as buildTranslationJobId, r as buildTenantGlossary, s as getCachedLanguage, t as LanguageRegistry, u as invalidateLanguageCache } from "./chunks/language-registry-sNjkO2BQ.js";
2
2
  import { ObjectRegistry } from "@happyvertical/smrt-core";
3
3
  import { getPackageConfig } from "@happyvertical/smrt-config";
4
4
  import { getTenantId } from "@happyvertical/smrt-tenancy";
5
5
  //#region src/__smrt-register__.ts
6
- ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-languages","packageVersion":"0.57.37","objects":{"@happyvertical/smrt-languages:LanguageOverrideCollection":{"name":"languageoverridecollection","className":"LanguageOverrideCollection","qualifiedName":"@happyvertical/smrt-languages:LanguageOverrideCollection","collection":"languageoverrides","filePath":"/home/runner/work/smrt/smrt/packages/languages/src/collections/LanguageOverrideCollection.ts","packageName":"@happyvertical/smrt-languages","fields":{},"methods":{"getAppOverride":{"name":"getAppOverride","async":true,"parameters":[{"name":"key","type":"string","optional":false},{"name":"locale","type":"string","optional":false}],"returnType":"Promise<LanguageOverride | null>","isStatic":false,"isPublic":true},"getTenantOverride":{"name":"getTenantOverride","async":true,"parameters":[{"name":"key","type":"string","optional":false},{"name":"locale","type":"string","optional":false},{"name":"tenantId","type":"string","optional":false}],"returnType":"Promise<LanguageOverride | null>","isStatic":false,"isPublic":true},"getResolutionLayers":{"name":"getResolutionLayers","async":true,"parameters":[{"name":"key","type":"string","optional":false},{"name":"locale","type":"string","optional":false},{"name":"tenantId","type":"string | null","optional":true,"unionBranches":[{"type":"string"},{"type":"null"}]}],"returnType":"Promise<object>","isStatic":false,"isPublic":true},"listTenantOverrides":{"name":"listTenantOverrides","async":true,"parameters":[{"name":"tenantId","type":"string","optional":false}],"returnType":"Promise<LanguageOverride[]>","isStatic":false,"isPublic":true},"listUnreviewedAutoTranslations":{"name":"listUnreviewedAutoTranslations","async":true,"parameters":[{"name":"options","type":"object","optional":true,"memberTypes":["string","number"]}],"returnType":"Promise<LanguageOverride[]>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"_smrt_language_overrides","api":{"include":["list","get","create","update","delete"]},"mcp":{"include":[]},"cli":{"include":["list","get","create","update","delete"],"skipApiCheck":true}},"extends":"SmrtCollection","extendsTypeArg":"LanguageOverride","exportName":"LanguageOverrideCollection","collectionExportName":"LanguageOverrideCollectionCollection","schema":{"tableName":"_smrt_language_overrides","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_language_overrides\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"key\\" TEXT NOT NULL DEFAULT '',\\n \\"locale\\" TEXT NOT NULL DEFAULT '',\\n \\"tenant_id\\" UUID,\\n \\"template\\" TEXT NOT NULL,\\n \\"auto_generated\\" BOOLEAN NOT NULL DEFAULT FALSE,\\n \\"source_hash\\" TEXT,\\n \\"ai_model\\" TEXT,\\n \\"reviewed_at\\" TEXT,\\n \\"reviewed_by\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"key":{"type":"TEXT","notNull":true,"unique":false,"default":""},"locale":{"type":"TEXT","notNull":true,"unique":false,"default":""},"tenant_id":{"type":"UUID","notNull":false,"unique":false},"template":{"type":"TEXT","notNull":true,"unique":false},"auto_generated":{"type":"BOOLEAN","notNull":true,"unique":false,"default":false},"source_hash":{"type":"TEXT","notNull":false,"unique":false},"ai_model":{"type":"TEXT","notNull":false,"unique":false},"reviewed_at":{"type":"TEXT","notNull":false,"unique":false},"reviewed_by":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"_smrt_language_overrides_key_locale_idx","columns":["key","locale","context"],"unique":true},{"name":"_smrt_language_overrides_slug_context_idx","columns":["slug","context"]},{"name":"_smrt_language_overrides_created_at_idx","columns":["created_at"]}],"version":"91442913"},"importPath":"@happyvertical/smrt-languages"},"@happyvertical/smrt-languages:LanguageTranslationTask":{"name":"languagetranslationtask","className":"LanguageTranslationTask","qualifiedName":"@happyvertical/smrt-languages:LanguageTranslationTask","collection":"languagetranslationtasks","description":"SmrtObject that owns the translation job's execute method. The TaskRunner resolves jobs by objectType so the registered class name needs to match what enqueueTranslationJob writes.","filePath":"/home/runner/work/smrt/smrt/packages/languages/src/jobs/translation-job.ts","packageName":"@happyvertical/smrt-languages","fields":{},"methods":{"execute":{"name":"execute","async":true,"parameters":[{"name":"args","type":"TranslationJobPayload","optional":false}],"returnType":"Promise<object>","isStatic":false,"isPublic":true}},"decoratorConfig":{"api":{"include":[]},"cli":{"include":[]},"mcp":{"include":[]}},"extends":"SmrtObject","exportName":"LanguageTranslationTask","collectionExportName":"LanguageTranslationTaskCollection","schema":{"tableName":"language_translation_tasks","ddl":"CREATE TABLE IF NOT EXISTS \\"language_translation_tasks\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"}},"indexes":[{"name":"language_translation_tasks_slug_context_idx","columns":["slug","context"],"unique":true},{"name":"language_translation_tasks_created_at_idx","columns":["created_at"]}],"version":"2cfc8812"},"importPath":"@happyvertical/smrt-languages/jobs"},"@happyvertical/smrt-languages:LanguageOverride":{"name":"languageoverride","className":"LanguageOverride","qualifiedName":"@happyvertical/smrt-languages:LanguageOverride","collection":"languageoverrides","filePath":"/home/runner/work/smrt/smrt/packages/languages/src/models/LanguageOverride.ts","packageName":"@happyvertical/smrt-languages","fields":{"key":{"type":"text","required":true,"default":"","_meta":{"required":true}},"locale":{"type":"text","required":true,"default":"","_meta":{"required":true}},"tenantId":{"type":"text","required":false,"_meta":{"sqlType":"UUID","nullable":true}},"template":{"type":"text","required":true,"_meta":{"required":true}},"auto_generated":{"type":"boolean","required":true,"default":false,"_meta":{"required":true,"default":false}},"source_hash":{"type":"text","required":false,"_meta":{"nullable":true}},"ai_model":{"type":"text","required":false,"_meta":{"nullable":true}},"reviewed_at":{"type":"text","required":false,"_meta":{"nullable":true}},"reviewed_by":{"type":"text","required":false,"_meta":{"nullable":true}}},"methods":{"save":{"name":"save","async":true,"parameters":[],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"delete":{"name":"delete","async":true,"parameters":[],"returnType":"Promise<void>","isStatic":false,"isPublic":true},"approve":{"name":"approve","async":true,"parameters":[{"name":"reviewerId","type":"string","optional":false}],"returnType":"Promise<this>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"_smrt_language_overrides","conflictColumns":["key","locale","context"],"api":{"include":["list","get","create","update","delete"]},"cli":{"include":["list","get","create","update","delete","approve"],"skipApiCheck":true},"mcp":{"include":[]}},"extends":"SmrtObject","exportName":"LanguageOverride","collectionExportName":"LanguageOverrideCollection","validationRules":[{"field":"key","rule":"required","fieldType":"text"},{"field":"locale","rule":"required","fieldType":"text"},{"field":"template","rule":"required","fieldType":"text"},{"field":"auto_generated","rule":"required","fieldType":"boolean"}],"schema":{"tableName":"_smrt_language_overrides","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_language_overrides\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"key\\" TEXT NOT NULL DEFAULT '',\\n \\"locale\\" TEXT NOT NULL DEFAULT '',\\n \\"tenant_id\\" UUID,\\n \\"template\\" TEXT NOT NULL,\\n \\"auto_generated\\" BOOLEAN NOT NULL DEFAULT FALSE,\\n \\"source_hash\\" TEXT,\\n \\"ai_model\\" TEXT,\\n \\"reviewed_at\\" TEXT,\\n \\"reviewed_by\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"key":{"type":"TEXT","notNull":true,"unique":false,"default":""},"locale":{"type":"TEXT","notNull":true,"unique":false,"default":""},"tenant_id":{"type":"UUID","notNull":false,"unique":false},"template":{"type":"TEXT","notNull":true,"unique":false},"auto_generated":{"type":"BOOLEAN","notNull":true,"unique":false,"default":false},"source_hash":{"type":"TEXT","notNull":false,"unique":false},"ai_model":{"type":"TEXT","notNull":false,"unique":false},"reviewed_at":{"type":"TEXT","notNull":false,"unique":false},"reviewed_by":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"_smrt_language_overrides_key_locale_idx","columns":["key","locale","context"],"unique":true},{"name":"_smrt_language_overrides_slug_context_idx","columns":["slug","context"]},{"name":"_smrt_language_overrides_created_at_idx","columns":["created_at"]}],"version":"91442913"},"importPath":"@happyvertical/smrt-languages"}},"moduleType":"smrt","smrtDependencies":["@happyvertical/smrt-core","@happyvertical/smrt-features","@happyvertical/smrt-jobs","@happyvertical/smrt-prompts","@happyvertical/smrt-tenancy"],"browser":{"status":"server-only","issues":["#3615","#3618","#3624","#3627"],"reason":"node:crypto","via":["@happyvertical/smrt-features","@happyvertical/smrt-jobs","@happyvertical/smrt-tenancy"]},"stampsConstructors":true}`));
6
+ ObjectRegistry.registerPackageManifest(JSON.parse(`{"version":"1.0.0","timestamp":0,"packageName":"@happyvertical/smrt-languages","packageVersion":"0.57.38","objects":{"@happyvertical/smrt-languages:LanguageOverrideCollection":{"name":"languageoverridecollection","className":"LanguageOverrideCollection","qualifiedName":"@happyvertical/smrt-languages:LanguageOverrideCollection","collection":"languageoverrides","filePath":"/home/runner/work/smrt/smrt/packages/languages/src/collections/LanguageOverrideCollection.ts","packageName":"@happyvertical/smrt-languages","fields":{},"methods":{"getAppOverride":{"name":"getAppOverride","async":true,"parameters":[{"name":"key","type":"string","optional":false},{"name":"locale","type":"string","optional":false}],"returnType":"Promise<LanguageOverride | null>","isStatic":false,"isPublic":true},"getTenantOverride":{"name":"getTenantOverride","async":true,"parameters":[{"name":"key","type":"string","optional":false},{"name":"locale","type":"string","optional":false},{"name":"tenantId","type":"string","optional":false}],"returnType":"Promise<LanguageOverride | null>","isStatic":false,"isPublic":true},"getResolutionLayers":{"name":"getResolutionLayers","async":true,"parameters":[{"name":"key","type":"string","optional":false},{"name":"locale","type":"string","optional":false},{"name":"tenantId","type":"string | null","optional":true,"unionBranches":[{"type":"string"},{"type":"null"}]}],"returnType":"Promise<object>","isStatic":false,"isPublic":true},"listTenantOverrides":{"name":"listTenantOverrides","async":true,"parameters":[{"name":"tenantId","type":"string","optional":false}],"returnType":"Promise<LanguageOverride[]>","isStatic":false,"isPublic":true},"listUnreviewedAutoTranslations":{"name":"listUnreviewedAutoTranslations","async":true,"parameters":[{"name":"options","type":"object","optional":true,"memberTypes":["string","number"]}],"returnType":"Promise<LanguageOverride[]>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"_smrt_language_overrides","api":{"include":["list","get","create","update","delete"]},"mcp":{"include":[]},"cli":{"include":["list","get","create","update","delete"],"skipApiCheck":true}},"extends":"SmrtCollection","extendsTypeArg":"LanguageOverride","exportName":"LanguageOverrideCollection","collectionExportName":"LanguageOverrideCollectionCollection","schema":{"tableName":"_smrt_language_overrides","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_language_overrides\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"key\\" TEXT NOT NULL DEFAULT '',\\n \\"locale\\" TEXT NOT NULL DEFAULT '',\\n \\"tenant_id\\" UUID,\\n \\"template\\" TEXT NOT NULL,\\n \\"auto_generated\\" BOOLEAN NOT NULL DEFAULT FALSE,\\n \\"source_hash\\" TEXT,\\n \\"ai_model\\" TEXT,\\n \\"reviewed_at\\" TEXT,\\n \\"reviewed_by\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"key":{"type":"TEXT","notNull":true,"unique":false,"default":""},"locale":{"type":"TEXT","notNull":true,"unique":false,"default":""},"tenant_id":{"type":"UUID","notNull":false,"unique":false},"template":{"type":"TEXT","notNull":true,"unique":false},"auto_generated":{"type":"BOOLEAN","notNull":true,"unique":false,"default":false},"source_hash":{"type":"TEXT","notNull":false,"unique":false},"ai_model":{"type":"TEXT","notNull":false,"unique":false},"reviewed_at":{"type":"TEXT","notNull":false,"unique":false},"reviewed_by":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"_smrt_language_overrides_key_locale_idx","columns":["key","locale","context"],"unique":true},{"name":"_smrt_language_overrides_slug_context_idx","columns":["slug","context"]},{"name":"_smrt_language_overrides_created_at_idx","columns":["created_at"]}],"version":"91442913"},"importPath":"@happyvertical/smrt-languages"},"@happyvertical/smrt-languages:LanguageTranslationTask":{"name":"languagetranslationtask","className":"LanguageTranslationTask","qualifiedName":"@happyvertical/smrt-languages:LanguageTranslationTask","collection":"languagetranslationtasks","description":"SmrtObject that owns the translation job's execute method. The TaskRunner resolves jobs by objectType so the registered class name needs to match what enqueueTranslationJob writes.","filePath":"/home/runner/work/smrt/smrt/packages/languages/src/jobs/translation-job.ts","packageName":"@happyvertical/smrt-languages","fields":{},"methods":{"execute":{"name":"execute","async":true,"parameters":[{"name":"args","type":"TranslationJobPayload","optional":false}],"returnType":"Promise<object>","isStatic":false,"isPublic":true}},"decoratorConfig":{"api":{"include":[]},"cli":{"include":[]},"mcp":{"include":[]}},"extends":"SmrtObject","exportName":"LanguageTranslationTask","collectionExportName":"LanguageTranslationTaskCollection","schema":{"tableName":"language_translation_tasks","ddl":"CREATE TABLE IF NOT EXISTS \\"language_translation_tasks\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"}},"indexes":[{"name":"language_translation_tasks_slug_context_idx","columns":["slug","context"],"unique":true},{"name":"language_translation_tasks_created_at_idx","columns":["created_at"]}],"version":"2cfc8812"},"importPath":"@happyvertical/smrt-languages/jobs"},"@happyvertical/smrt-languages:LanguageOverride":{"name":"languageoverride","className":"LanguageOverride","qualifiedName":"@happyvertical/smrt-languages:LanguageOverride","collection":"languageoverrides","filePath":"/home/runner/work/smrt/smrt/packages/languages/src/models/LanguageOverride.ts","packageName":"@happyvertical/smrt-languages","fields":{"key":{"type":"text","required":true,"default":"","_meta":{"required":true}},"locale":{"type":"text","required":true,"default":"","_meta":{"required":true}},"tenantId":{"type":"text","required":false,"_meta":{"sqlType":"UUID","nullable":true}},"template":{"type":"text","required":true,"_meta":{"required":true}},"auto_generated":{"type":"boolean","required":true,"default":false,"_meta":{"required":true,"default":false}},"source_hash":{"type":"text","required":false,"_meta":{"nullable":true}},"ai_model":{"type":"text","required":false,"_meta":{"nullable":true}},"reviewed_at":{"type":"text","required":false,"_meta":{"nullable":true}},"reviewed_by":{"type":"text","required":false,"_meta":{"nullable":true}}},"methods":{"save":{"name":"save","async":true,"parameters":[{"name":"options","type":"SmrtSaveOptions","optional":true}],"returnType":"Promise<this>","isStatic":false,"isPublic":true},"delete":{"name":"delete","async":true,"parameters":[],"returnType":"Promise<void>","isStatic":false,"isPublic":true},"approve":{"name":"approve","async":true,"parameters":[{"name":"reviewerId","type":"string","optional":false}],"returnType":"Promise<this>","isStatic":false,"isPublic":true}},"decoratorConfig":{"tableName":"_smrt_language_overrides","conflictColumns":["key","locale","context"],"api":{"include":["list","get","create","update","delete"]},"cli":{"include":["list","get","create","update","delete","approve"],"skipApiCheck":true},"mcp":{"include":[]}},"extends":"SmrtObject","exportName":"LanguageOverride","collectionExportName":"LanguageOverrideCollection","validationRules":[{"field":"key","rule":"required","fieldType":"text"},{"field":"locale","rule":"required","fieldType":"text"},{"field":"template","rule":"required","fieldType":"text"},{"field":"auto_generated","rule":"required","fieldType":"boolean"}],"schema":{"tableName":"_smrt_language_overrides","ddl":"CREATE TABLE IF NOT EXISTS \\"_smrt_language_overrides\\" (\\n \\"id\\" UUID PRIMARY KEY NOT NULL,\\n \\"slug\\" TEXT NOT NULL,\\n \\"context\\" TEXT NOT NULL DEFAULT '',\\n \\"created_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"updated_at\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\n \\"key\\" TEXT NOT NULL DEFAULT '',\\n \\"locale\\" TEXT NOT NULL DEFAULT '',\\n \\"tenant_id\\" UUID,\\n \\"template\\" TEXT NOT NULL,\\n \\"auto_generated\\" BOOLEAN NOT NULL DEFAULT FALSE,\\n \\"source_hash\\" TEXT,\\n \\"ai_model\\" TEXT,\\n \\"reviewed_at\\" TEXT,\\n \\"reviewed_by\\" TEXT\\n);","columns":{"id":{"type":"UUID","primaryKey":true,"referenceKind":"id","notNull":true},"slug":{"type":"TEXT","notNull":true},"context":{"type":"TEXT","notNull":true,"default":""},"created_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"updated_at":{"type":"TIMESTAMP","notNull":true,"default":"current_timestamp"},"key":{"type":"TEXT","notNull":true,"unique":false,"default":""},"locale":{"type":"TEXT","notNull":true,"unique":false,"default":""},"tenant_id":{"type":"UUID","notNull":false,"unique":false},"template":{"type":"TEXT","notNull":true,"unique":false},"auto_generated":{"type":"BOOLEAN","notNull":true,"unique":false,"default":false},"source_hash":{"type":"TEXT","notNull":false,"unique":false},"ai_model":{"type":"TEXT","notNull":false,"unique":false},"reviewed_at":{"type":"TEXT","notNull":false,"unique":false},"reviewed_by":{"type":"TEXT","notNull":false,"unique":false}},"indexes":[{"name":"_smrt_language_overrides_key_locale_idx","columns":["key","locale","context"],"unique":true},{"name":"_smrt_language_overrides_slug_context_idx","columns":["slug","context"]},{"name":"_smrt_language_overrides_created_at_idx","columns":["created_at"]}],"version":"91442913"},"importPath":"@happyvertical/smrt-languages"}},"moduleType":"smrt","smrtDependencies":["@happyvertical/smrt-core","@happyvertical/smrt-features","@happyvertical/smrt-jobs","@happyvertical/smrt-prompts","@happyvertical/smrt-tenancy"],"browser":{"status":"server-only","issues":["#3615","#3618","#3624","#3627"],"reason":"node:crypto","via":["@happyvertical/smrt-features","@happyvertical/smrt-jobs","@happyvertical/smrt-tenancy"]},"stampsConstructors":true}`));
7
7
  //#endregion
8
8
  //#region src/language-resolver.ts
9
9
  var DEFAULT_LOCALE = "en";
@@ -176,7 +176,7 @@ async function tryEnqueueTranslation(args) {
176
176
  if (!args.options.db) return;
177
177
  if (normalizeLocale(args.requestedLocale) === args.defaultLocale) return;
178
178
  try {
179
- const { enqueueTranslationJob } = await import("./chunks/translation-job-B4sdwQ3W.js").then((n) => n.a);
179
+ const { enqueueTranslationJob } = await import("./chunks/translation-job-DMWfy7m4.js").then((n) => n.a);
180
180
  await enqueueTranslationJob({
181
181
  key: args.key,
182
182
  targetLocale: args.requestedLocale,
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/language-resolver.ts","../src/index.ts"],"sourcesContent":["// Generated by @happyvertical/smrt-core smrtPlugin at library build time.\n// The package manifest is inlined so self-registration cannot miss when a\n// downstream bundler relocates this module away from dist/manifest.json\n// (https://github.com/happyvertical/smrt/issues/1506,\n// https://github.com/happyvertical/smrt/issues/1507).\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(JSON.parse(\"{\\\"version\\\":\\\"1.0.0\\\",\\\"timestamp\\\":0,\\\"packageName\\\":\\\"@happyvertical/smrt-languages\\\",\\\"packageVersion\\\":\\\"0.57.37\\\",\\\"objects\\\":{\\\"@happyvertical/smrt-languages:LanguageOverrideCollection\\\":{\\\"name\\\":\\\"languageoverridecollection\\\",\\\"className\\\":\\\"LanguageOverrideCollection\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-languages:LanguageOverrideCollection\\\",\\\"collection\\\":\\\"languageoverrides\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/languages/src/collections/LanguageOverrideCollection.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-languages\\\",\\\"fields\\\":{},\\\"methods\\\":{\\\"getAppOverride\\\":{\\\"name\\\":\\\"getAppOverride\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"locale\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<LanguageOverride | null>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"getTenantOverride\\\":{\\\"name\\\":\\\"getTenantOverride\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"locale\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"tenantId\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<LanguageOverride | null>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"getResolutionLayers\\\":{\\\"name\\\":\\\"getResolutionLayers\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"locale\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"tenantId\\\",\\\"type\\\":\\\"string | null\\\",\\\"optional\\\":true,\\\"unionBranches\\\":[{\\\"type\\\":\\\"string\\\"},{\\\"type\\\":\\\"null\\\"}]}],\\\"returnType\\\":\\\"Promise<object>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"listTenantOverrides\\\":{\\\"name\\\":\\\"listTenantOverrides\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"tenantId\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<LanguageOverride[]>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"listUnreviewedAutoTranslations\\\":{\\\"name\\\":\\\"listUnreviewedAutoTranslations\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"options\\\",\\\"type\\\":\\\"object\\\",\\\"optional\\\":true,\\\"memberTypes\\\":[\\\"string\\\",\\\"number\\\"]}],\\\"returnType\\\":\\\"Promise<LanguageOverride[]>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true}},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"_smrt_language_overrides\\\",\\\"api\\\":{\\\"include\\\":[\\\"list\\\",\\\"get\\\",\\\"create\\\",\\\"update\\\",\\\"delete\\\"]},\\\"mcp\\\":{\\\"include\\\":[]},\\\"cli\\\":{\\\"include\\\":[\\\"list\\\",\\\"get\\\",\\\"create\\\",\\\"update\\\",\\\"delete\\\"],\\\"skipApiCheck\\\":true}},\\\"extends\\\":\\\"SmrtCollection\\\",\\\"extendsTypeArg\\\":\\\"LanguageOverride\\\",\\\"exportName\\\":\\\"LanguageOverrideCollection\\\",\\\"collectionExportName\\\":\\\"LanguageOverrideCollectionCollection\\\",\\\"schema\\\":{\\\"tableName\\\":\\\"_smrt_language_overrides\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"_smrt_language_overrides\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"key\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"locale\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"tenant_id\\\\\\\" UUID,\\\\n \\\\\\\"template\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"auto_generated\\\\\\\" BOOLEAN NOT NULL DEFAULT FALSE,\\\\n \\\\\\\"source_hash\\\\\\\" TEXT,\\\\n \\\\\\\"ai_model\\\\\\\" TEXT,\\\\n \\\\\\\"reviewed_at\\\\\\\" TEXT,\\\\n \\\\\\\"reviewed_by\\\\\\\" TEXT\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"key\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":\\\"\\\"},\\\"locale\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":\\\"\\\"},\\\"tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"template\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"auto_generated\\\":{\\\"type\\\":\\\"BOOLEAN\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":false},\\\"source_hash\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"ai_model\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"reviewed_at\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"reviewed_by\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false}},\\\"indexes\\\":[{\\\"name\\\":\\\"_smrt_language_overrides_key_locale_idx\\\",\\\"columns\\\":[\\\"key\\\",\\\"locale\\\",\\\"context\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"_smrt_language_overrides_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"]},{\\\"name\\\":\\\"_smrt_language_overrides_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]}],\\\"version\\\":\\\"91442913\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-languages\\\"},\\\"@happyvertical/smrt-languages:LanguageTranslationTask\\\":{\\\"name\\\":\\\"languagetranslationtask\\\",\\\"className\\\":\\\"LanguageTranslationTask\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-languages:LanguageTranslationTask\\\",\\\"collection\\\":\\\"languagetranslationtasks\\\",\\\"description\\\":\\\"SmrtObject that owns the translation job's execute method. The TaskRunner resolves jobs by objectType so the registered class name needs to match what enqueueTranslationJob writes.\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/languages/src/jobs/translation-job.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-languages\\\",\\\"fields\\\":{},\\\"methods\\\":{\\\"execute\\\":{\\\"name\\\":\\\"execute\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"args\\\",\\\"type\\\":\\\"TranslationJobPayload\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<object>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true}},\\\"decoratorConfig\\\":{\\\"api\\\":{\\\"include\\\":[]},\\\"cli\\\":{\\\"include\\\":[]},\\\"mcp\\\":{\\\"include\\\":[]}},\\\"extends\\\":\\\"SmrtObject\\\",\\\"exportName\\\":\\\"LanguageTranslationTask\\\",\\\"collectionExportName\\\":\\\"LanguageTranslationTaskCollection\\\",\\\"schema\\\":{\\\"tableName\\\":\\\"language_translation_tasks\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"language_translation_tasks\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"}},\\\"indexes\\\":[{\\\"name\\\":\\\"language_translation_tasks_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"language_translation_tasks_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]}],\\\"version\\\":\\\"2cfc8812\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-languages/jobs\\\"},\\\"@happyvertical/smrt-languages:LanguageOverride\\\":{\\\"name\\\":\\\"languageoverride\\\",\\\"className\\\":\\\"LanguageOverride\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-languages:LanguageOverride\\\",\\\"collection\\\":\\\"languageoverrides\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/languages/src/models/LanguageOverride.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-languages\\\",\\\"fields\\\":{\\\"key\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"default\\\":\\\"\\\",\\\"_meta\\\":{\\\"required\\\":true}},\\\"locale\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"default\\\":\\\"\\\",\\\"_meta\\\":{\\\"required\\\":true}},\\\"tenantId\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"sqlType\\\":\\\"UUID\\\",\\\"nullable\\\":true}},\\\"template\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"_meta\\\":{\\\"required\\\":true}},\\\"auto_generated\\\":{\\\"type\\\":\\\"boolean\\\",\\\"required\\\":true,\\\"default\\\":false,\\\"_meta\\\":{\\\"required\\\":true,\\\"default\\\":false}},\\\"source_hash\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true}},\\\"ai_model\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true}},\\\"reviewed_at\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true}},\\\"reviewed_by\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true}}},\\\"methods\\\":{\\\"save\\\":{\\\"name\\\":\\\"save\\\",\\\"async\\\":true,\\\"parameters\\\":[],\\\"returnType\\\":\\\"Promise<this>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"delete\\\":{\\\"name\\\":\\\"delete\\\",\\\"async\\\":true,\\\"parameters\\\":[],\\\"returnType\\\":\\\"Promise<void>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"approve\\\":{\\\"name\\\":\\\"approve\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"reviewerId\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<this>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true}},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"_smrt_language_overrides\\\",\\\"conflictColumns\\\":[\\\"key\\\",\\\"locale\\\",\\\"context\\\"],\\\"api\\\":{\\\"include\\\":[\\\"list\\\",\\\"get\\\",\\\"create\\\",\\\"update\\\",\\\"delete\\\"]},\\\"cli\\\":{\\\"include\\\":[\\\"list\\\",\\\"get\\\",\\\"create\\\",\\\"update\\\",\\\"delete\\\",\\\"approve\\\"],\\\"skipApiCheck\\\":true},\\\"mcp\\\":{\\\"include\\\":[]}},\\\"extends\\\":\\\"SmrtObject\\\",\\\"exportName\\\":\\\"LanguageOverride\\\",\\\"collectionExportName\\\":\\\"LanguageOverrideCollection\\\",\\\"validationRules\\\":[{\\\"field\\\":\\\"key\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"},{\\\"field\\\":\\\"locale\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"},{\\\"field\\\":\\\"template\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"},{\\\"field\\\":\\\"auto_generated\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"boolean\\\"}],\\\"schema\\\":{\\\"tableName\\\":\\\"_smrt_language_overrides\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"_smrt_language_overrides\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"key\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"locale\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"tenant_id\\\\\\\" UUID,\\\\n \\\\\\\"template\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"auto_generated\\\\\\\" BOOLEAN NOT NULL DEFAULT FALSE,\\\\n \\\\\\\"source_hash\\\\\\\" TEXT,\\\\n \\\\\\\"ai_model\\\\\\\" TEXT,\\\\n \\\\\\\"reviewed_at\\\\\\\" TEXT,\\\\n \\\\\\\"reviewed_by\\\\\\\" TEXT\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"key\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":\\\"\\\"},\\\"locale\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":\\\"\\\"},\\\"tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"template\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"auto_generated\\\":{\\\"type\\\":\\\"BOOLEAN\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":false},\\\"source_hash\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"ai_model\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"reviewed_at\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"reviewed_by\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false}},\\\"indexes\\\":[{\\\"name\\\":\\\"_smrt_language_overrides_key_locale_idx\\\",\\\"columns\\\":[\\\"key\\\",\\\"locale\\\",\\\"context\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"_smrt_language_overrides_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"]},{\\\"name\\\":\\\"_smrt_language_overrides_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]}],\\\"version\\\":\\\"91442913\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-languages\\\"}},\\\"moduleType\\\":\\\"smrt\\\",\\\"smrtDependencies\\\":[\\\"@happyvertical/smrt-core\\\",\\\"@happyvertical/smrt-features\\\",\\\"@happyvertical/smrt-jobs\\\",\\\"@happyvertical/smrt-prompts\\\",\\\"@happyvertical/smrt-tenancy\\\"],\\\"browser\\\":{\\\"status\\\":\\\"server-only\\\",\\\"issues\\\":[\\\"#3615\\\",\\\"#3618\\\",\\\"#3624\\\",\\\"#3627\\\"],\\\"reason\\\":\\\"node:crypto\\\",\\\"via\\\":[\\\"@happyvertical/smrt-features\\\",\\\"@happyvertical/smrt-jobs\\\",\\\"@happyvertical/smrt-tenancy\\\"]},\\\"stampsConstructors\\\":true}\"));\n","import { getPackageConfig } from '@happyvertical/smrt-config';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport {\n getCachedLanguage,\n getLanguageCacheGeneration,\n invalidateLanguageCache,\n setCachedLanguage,\n} from './cache.js';\nimport { LanguageOverrideCollection } from './collections/LanguageOverrideCollection.js';\nimport { LanguageRegistry } from './language-registry.js';\nimport type {\n LanguageCacheValue,\n LanguagesPackageConfig,\n ResolvedLanguageString,\n ResolveLanguageStringOptions,\n} from './types.js';\nimport {\n buildLocaleFallbackChain,\n normalizeLocale,\n renderTemplate,\n} from './utils.js';\n\nconst DEFAULT_LOCALE = 'en';\n\nfunction getLanguagesConfig(): LanguagesPackageConfig {\n return getPackageConfig<LanguagesPackageConfig>('languages', {\n defaultLocale: DEFAULT_LOCALE,\n overrides: {},\n });\n}\n\ninterface ResolutionAttempt {\n template: string | null;\n source: ResolvedLanguageString['source'];\n resolvedFromLocale: string;\n}\n\nasync function resolveBaseForLocale(\n key: string,\n locale: string,\n options: ResolveLanguageStringOptions,\n config: LanguagesPackageConfig,\n): Promise<ResolutionAttempt> {\n const tenantId =\n options.tenantId !== undefined ? options.tenantId : (getTenantId() ?? null);\n\n let collection: LanguageOverrideCollection | null = null;\n if (options.db) {\n collection = await LanguageOverrideCollection.create({\n db: options.db,\n });\n }\n\n const cacheDb = collection?.db ?? options.db;\n const cached = getCachedLanguage(key, locale, tenantId, cacheDb);\n if (cached) {\n return {\n template: cached.template,\n source: cached.source,\n resolvedFromLocale: cached.resolvedFromLocale,\n };\n }\n\n // Capture the invalidation generation before the asynchronous layer loads. A\n // write that lands while they are in flight bumps it, and the cache write in\n // `finalize` is then refused rather than repopulating the entry the write\n // just invalidated with the pre-write value.\n const loadedAtGeneration = getLanguageCacheGeneration(key, locale, cacheDb);\n\n // 1. Tenant override (highest precedence among stored layers).\n if (collection && tenantId) {\n const tenantOverride = await collection.getTenantOverride(\n key,\n locale,\n tenantId,\n );\n if (tenantOverride) {\n return finalize({\n key,\n locale,\n tenantId,\n db: cacheDb,\n loadedAtGeneration,\n attempt: {\n template: tenantOverride.template,\n source: 'tenant',\n resolvedFromLocale: locale,\n },\n });\n }\n }\n\n // 2. App-level override (incl. AI auto-translations).\n if (collection) {\n const appOverride = await collection.getAppOverride(key, locale);\n if (appOverride) {\n return finalize({\n key,\n locale,\n tenantId,\n db: cacheDb,\n loadedAtGeneration,\n attempt: {\n template: appOverride.template,\n source: 'app',\n resolvedFromLocale: locale,\n },\n });\n }\n }\n\n // 3. File-config override. Locale keys in user-authored config files\n // commonly come in mixed case (`fr-ca`, `Fr-CA`, etc.); normalize lookup so\n // config matches the same way the DB and code-default layers do.\n const configForKey = config.overrides?.[key];\n const configTemplate = lookupNormalizedLocale(configForKey, locale);\n if (typeof configTemplate === 'string') {\n return finalize({\n key,\n locale,\n tenantId,\n db: cacheDb,\n loadedAtGeneration,\n attempt: {\n template: configTemplate,\n source: 'config',\n resolvedFromLocale: locale,\n },\n });\n }\n\n // 4. Code default.\n const definition = LanguageRegistry.get(key, locale);\n if (definition) {\n return finalize({\n key,\n locale,\n tenantId,\n db: cacheDb,\n loadedAtGeneration,\n attempt: {\n template: definition.template,\n source: 'code',\n resolvedFromLocale: locale,\n },\n });\n }\n\n return {\n template: null,\n source: 'missing',\n resolvedFromLocale: locale,\n };\n}\n\nfunction lookupNormalizedLocale(\n bag: Record<string, string> | undefined,\n locale: string,\n): string | undefined {\n if (!bag) return undefined;\n // Fast path: exact match (most file configs use the canonical form).\n if (typeof bag[locale] === 'string') return bag[locale];\n for (const [candidate, template] of Object.entries(bag)) {\n if (normalizeLocale(candidate) === locale) {\n return template;\n }\n }\n return undefined;\n}\n\nfunction finalize(args: {\n key: string;\n locale: string;\n tenantId: string | null;\n db: unknown;\n attempt: ResolutionAttempt;\n loadedAtGeneration: number;\n}): ResolutionAttempt {\n if (args.attempt.template !== null) {\n const value: LanguageCacheValue = {\n key: args.key,\n locale: args.locale,\n template: args.attempt.template,\n source: args.attempt.source,\n resolvedFromLocale: args.attempt.resolvedFromLocale,\n };\n setCachedLanguage(\n args.key,\n args.locale,\n args.tenantId,\n args.db,\n value,\n args.loadedAtGeneration,\n );\n }\n return args.attempt;\n}\n\nexport async function resolveLanguageString(\n key: string,\n options: ResolveLanguageStringOptions = {},\n): Promise<ResolvedLanguageString> {\n if (!key || key.trim() === '') {\n throw new Error('resolveLanguageString requires a non-empty key');\n }\n\n const config = getLanguagesConfig();\n const defaultLocale = normalizeLocale(config.defaultLocale ?? DEFAULT_LOCALE);\n const requested = normalizeLocale(options.locale ?? defaultLocale);\n\n // Runtime override always wins, regardless of locale chain.\n if (typeof options.overrides?.template === 'string') {\n return {\n key,\n locale: requested,\n template: options.overrides.template,\n text: renderTemplate(options.overrides.template, options.vars),\n resolvedFromLocale: requested,\n source: 'runtime',\n };\n }\n\n const chain = buildLocaleFallbackChain(requested, defaultLocale);\n\n let firstHit: ResolutionAttempt | null = null;\n let firstLocale = requested;\n for (const locale of chain) {\n const attempt = await resolveBaseForLocale(key, locale, options, config);\n if (attempt.template !== null) {\n firstHit = attempt;\n firstLocale = locale;\n break;\n }\n }\n\n if (!firstHit) {\n if (options.strict) {\n throw new Error(\n `Language string \"${key}\" has no resolution for locale \"${requested}\"`,\n );\n }\n // Soft-miss: enqueue translation if the requested locale differs from the\n // default and a default-locale source exists. Failure of the enqueue path\n // must never break resolution — we still return the key as the rendered\n // text so the UI degrades gracefully.\n await tryEnqueueTranslation({\n key,\n requestedLocale: requested,\n defaultLocale,\n options,\n });\n return {\n key,\n locale: requested,\n template: key,\n text: key,\n resolvedFromLocale: requested,\n source: 'missing',\n };\n }\n\n // Hit at a fallback locale that isn't the requested one — fire-and-forget a\n // translation job for the requested target.\n if (firstLocale !== requested) {\n await tryEnqueueTranslation({\n key,\n requestedLocale: requested,\n defaultLocale,\n options,\n });\n return {\n key,\n locale: requested,\n template: firstHit.template ?? key,\n text: renderTemplate(firstHit.template ?? key, options.vars),\n resolvedFromLocale: firstLocale,\n source: 'fallback',\n };\n }\n\n return {\n key,\n locale: requested,\n template: firstHit.template ?? key,\n text: renderTemplate(firstHit.template ?? key, options.vars),\n resolvedFromLocale: firstLocale,\n source: firstHit.source,\n };\n}\n\n/**\n * Best-effort enqueue of an AI translation job. Wrapped so resolution never\n * fails because of an enqueue error, missing optional dependency, etc.\n *\n * The actual job machinery lives in `./jobs/translation-job.ts` and is loaded\n * lazily so the resolver does not pull `@happyvertical/ai` into the import\n * graph for synchronous resolves that never miss.\n */\nasync function tryEnqueueTranslation(args: {\n key: string;\n requestedLocale: string;\n defaultLocale: string;\n options: ResolveLanguageStringOptions;\n}): Promise<void> {\n if (!args.options.db) {\n // Without a DB we cannot persist a job; skip silently — resolver still\n // returns a fallback to the caller.\n return;\n }\n if (normalizeLocale(args.requestedLocale) === args.defaultLocale) {\n return;\n }\n\n try {\n const { enqueueTranslationJob } = await import('./jobs/translation-job.js');\n await enqueueTranslationJob({\n key: args.key,\n targetLocale: args.requestedLocale,\n sourceLocale: args.defaultLocale,\n tenantId: args.options.tenantId ?? getTenantId() ?? null,\n db: args.options.db,\n });\n } catch {\n // Enqueueing must never break resolution.\n }\n}\n\nexport function invalidateResolvedLanguageCache(\n key: string,\n locale: string,\n tenantId: string | null | undefined,\n db: unknown,\n): void {\n invalidateLanguageCache(key, locale, tenantId, db);\n}\n","/**\n * @happyvertical/smrt-languages\n *\n * Code-first language strings with config + tenant overrides and AI-driven\n * auto-translation for SMRT applications. Mirrors the architecture of\n * `@happyvertical/smrt-prompts` plus a smrt-jobs handler that fills gaps in\n * non-default locales the first time a string is requested.\n *\n * The package root intentionally exposes only the read path\n * (`defineLanguageString`, `resolveLanguageString`, `LanguageOverride`, the\n * cache helpers, glossary helpers, and shared types/utilities). The\n * translation-worker stack — which depends on `@happyvertical/ai`,\n * smrt-jobs, smrt-features, and smrt-prompts — lives on the\n * `@happyvertical/smrt-languages/jobs` subpath, and the admin CLI helpers\n * live on `@happyvertical/smrt-languages/cli`. Resolver consumers (the vast\n * majority of call sites) never load the worker dependency tree.\n *\n * @packageDocumentation\n */\n\nimport './__smrt-register__.js';\n\nexport {\n clearLanguageCache,\n getLanguageCacheTtlMs,\n invalidateLanguageCache,\n} from './cache.js';\nexport { LanguageOverrideCollection } from './collections/LanguageOverrideCollection.js';\nexport { buildTenantGlossary } from './glossary.js';\n\nexport {\n defineLanguageString,\n LanguageRegistry,\n} from './language-registry.js';\n\nexport {\n invalidateResolvedLanguageCache,\n resolveLanguageString,\n} from './language-resolver.js';\n\nexport {\n LanguageOverride,\n type LanguageOverrideCtorOptions,\n} from './models/LanguageOverride.js';\n\nexport type {\n LanguageCacheValue,\n LanguageOverrideOptions,\n LanguageStringDefinition,\n LanguageStringDefinitionInput,\n LanguagesPackageConfig,\n LanguageVariables,\n ResolvedLanguageString,\n ResolveLanguageStringOptions,\n} from './types.js';\n\nexport {\n buildLocaleFallbackChain,\n buildTranslationJobId,\n computeSourceHash,\n normalizeLocale,\n renderTemplate,\n} from './utils.js';\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/__smrt-register__.ts","../src/language-resolver.ts","../src/index.ts"],"sourcesContent":["// Generated by @happyvertical/smrt-core smrtPlugin at library build time.\n// The package manifest is inlined so self-registration cannot miss when a\n// downstream bundler relocates this module away from dist/manifest.json\n// (https://github.com/happyvertical/smrt/issues/1506,\n// https://github.com/happyvertical/smrt/issues/1507).\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(JSON.parse(\"{\\\"version\\\":\\\"1.0.0\\\",\\\"timestamp\\\":0,\\\"packageName\\\":\\\"@happyvertical/smrt-languages\\\",\\\"packageVersion\\\":\\\"0.57.38\\\",\\\"objects\\\":{\\\"@happyvertical/smrt-languages:LanguageOverrideCollection\\\":{\\\"name\\\":\\\"languageoverridecollection\\\",\\\"className\\\":\\\"LanguageOverrideCollection\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-languages:LanguageOverrideCollection\\\",\\\"collection\\\":\\\"languageoverrides\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/languages/src/collections/LanguageOverrideCollection.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-languages\\\",\\\"fields\\\":{},\\\"methods\\\":{\\\"getAppOverride\\\":{\\\"name\\\":\\\"getAppOverride\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"locale\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<LanguageOverride | null>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"getTenantOverride\\\":{\\\"name\\\":\\\"getTenantOverride\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"locale\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"tenantId\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<LanguageOverride | null>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"getResolutionLayers\\\":{\\\"name\\\":\\\"getResolutionLayers\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"key\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"locale\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false},{\\\"name\\\":\\\"tenantId\\\",\\\"type\\\":\\\"string | null\\\",\\\"optional\\\":true,\\\"unionBranches\\\":[{\\\"type\\\":\\\"string\\\"},{\\\"type\\\":\\\"null\\\"}]}],\\\"returnType\\\":\\\"Promise<object>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"listTenantOverrides\\\":{\\\"name\\\":\\\"listTenantOverrides\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"tenantId\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<LanguageOverride[]>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"listUnreviewedAutoTranslations\\\":{\\\"name\\\":\\\"listUnreviewedAutoTranslations\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"options\\\",\\\"type\\\":\\\"object\\\",\\\"optional\\\":true,\\\"memberTypes\\\":[\\\"string\\\",\\\"number\\\"]}],\\\"returnType\\\":\\\"Promise<LanguageOverride[]>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true}},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"_smrt_language_overrides\\\",\\\"api\\\":{\\\"include\\\":[\\\"list\\\",\\\"get\\\",\\\"create\\\",\\\"update\\\",\\\"delete\\\"]},\\\"mcp\\\":{\\\"include\\\":[]},\\\"cli\\\":{\\\"include\\\":[\\\"list\\\",\\\"get\\\",\\\"create\\\",\\\"update\\\",\\\"delete\\\"],\\\"skipApiCheck\\\":true}},\\\"extends\\\":\\\"SmrtCollection\\\",\\\"extendsTypeArg\\\":\\\"LanguageOverride\\\",\\\"exportName\\\":\\\"LanguageOverrideCollection\\\",\\\"collectionExportName\\\":\\\"LanguageOverrideCollectionCollection\\\",\\\"schema\\\":{\\\"tableName\\\":\\\"_smrt_language_overrides\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"_smrt_language_overrides\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"key\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"locale\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"tenant_id\\\\\\\" UUID,\\\\n \\\\\\\"template\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"auto_generated\\\\\\\" BOOLEAN NOT NULL DEFAULT FALSE,\\\\n \\\\\\\"source_hash\\\\\\\" TEXT,\\\\n \\\\\\\"ai_model\\\\\\\" TEXT,\\\\n \\\\\\\"reviewed_at\\\\\\\" TEXT,\\\\n \\\\\\\"reviewed_by\\\\\\\" TEXT\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"key\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":\\\"\\\"},\\\"locale\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":\\\"\\\"},\\\"tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"template\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"auto_generated\\\":{\\\"type\\\":\\\"BOOLEAN\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":false},\\\"source_hash\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"ai_model\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"reviewed_at\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"reviewed_by\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false}},\\\"indexes\\\":[{\\\"name\\\":\\\"_smrt_language_overrides_key_locale_idx\\\",\\\"columns\\\":[\\\"key\\\",\\\"locale\\\",\\\"context\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"_smrt_language_overrides_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"]},{\\\"name\\\":\\\"_smrt_language_overrides_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]}],\\\"version\\\":\\\"91442913\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-languages\\\"},\\\"@happyvertical/smrt-languages:LanguageTranslationTask\\\":{\\\"name\\\":\\\"languagetranslationtask\\\",\\\"className\\\":\\\"LanguageTranslationTask\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-languages:LanguageTranslationTask\\\",\\\"collection\\\":\\\"languagetranslationtasks\\\",\\\"description\\\":\\\"SmrtObject that owns the translation job's execute method. The TaskRunner resolves jobs by objectType so the registered class name needs to match what enqueueTranslationJob writes.\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/languages/src/jobs/translation-job.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-languages\\\",\\\"fields\\\":{},\\\"methods\\\":{\\\"execute\\\":{\\\"name\\\":\\\"execute\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"args\\\",\\\"type\\\":\\\"TranslationJobPayload\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<object>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true}},\\\"decoratorConfig\\\":{\\\"api\\\":{\\\"include\\\":[]},\\\"cli\\\":{\\\"include\\\":[]},\\\"mcp\\\":{\\\"include\\\":[]}},\\\"extends\\\":\\\"SmrtObject\\\",\\\"exportName\\\":\\\"LanguageTranslationTask\\\",\\\"collectionExportName\\\":\\\"LanguageTranslationTaskCollection\\\",\\\"schema\\\":{\\\"tableName\\\":\\\"language_translation_tasks\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"language_translation_tasks\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"}},\\\"indexes\\\":[{\\\"name\\\":\\\"language_translation_tasks_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"language_translation_tasks_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]}],\\\"version\\\":\\\"2cfc8812\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-languages/jobs\\\"},\\\"@happyvertical/smrt-languages:LanguageOverride\\\":{\\\"name\\\":\\\"languageoverride\\\",\\\"className\\\":\\\"LanguageOverride\\\",\\\"qualifiedName\\\":\\\"@happyvertical/smrt-languages:LanguageOverride\\\",\\\"collection\\\":\\\"languageoverrides\\\",\\\"filePath\\\":\\\"/home/runner/work/smrt/smrt/packages/languages/src/models/LanguageOverride.ts\\\",\\\"packageName\\\":\\\"@happyvertical/smrt-languages\\\",\\\"fields\\\":{\\\"key\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"default\\\":\\\"\\\",\\\"_meta\\\":{\\\"required\\\":true}},\\\"locale\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"default\\\":\\\"\\\",\\\"_meta\\\":{\\\"required\\\":true}},\\\"tenantId\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"sqlType\\\":\\\"UUID\\\",\\\"nullable\\\":true}},\\\"template\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":true,\\\"_meta\\\":{\\\"required\\\":true}},\\\"auto_generated\\\":{\\\"type\\\":\\\"boolean\\\",\\\"required\\\":true,\\\"default\\\":false,\\\"_meta\\\":{\\\"required\\\":true,\\\"default\\\":false}},\\\"source_hash\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true}},\\\"ai_model\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true}},\\\"reviewed_at\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true}},\\\"reviewed_by\\\":{\\\"type\\\":\\\"text\\\",\\\"required\\\":false,\\\"_meta\\\":{\\\"nullable\\\":true}}},\\\"methods\\\":{\\\"save\\\":{\\\"name\\\":\\\"save\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"options\\\",\\\"type\\\":\\\"SmrtSaveOptions\\\",\\\"optional\\\":true}],\\\"returnType\\\":\\\"Promise<this>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"delete\\\":{\\\"name\\\":\\\"delete\\\",\\\"async\\\":true,\\\"parameters\\\":[],\\\"returnType\\\":\\\"Promise<void>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true},\\\"approve\\\":{\\\"name\\\":\\\"approve\\\",\\\"async\\\":true,\\\"parameters\\\":[{\\\"name\\\":\\\"reviewerId\\\",\\\"type\\\":\\\"string\\\",\\\"optional\\\":false}],\\\"returnType\\\":\\\"Promise<this>\\\",\\\"isStatic\\\":false,\\\"isPublic\\\":true}},\\\"decoratorConfig\\\":{\\\"tableName\\\":\\\"_smrt_language_overrides\\\",\\\"conflictColumns\\\":[\\\"key\\\",\\\"locale\\\",\\\"context\\\"],\\\"api\\\":{\\\"include\\\":[\\\"list\\\",\\\"get\\\",\\\"create\\\",\\\"update\\\",\\\"delete\\\"]},\\\"cli\\\":{\\\"include\\\":[\\\"list\\\",\\\"get\\\",\\\"create\\\",\\\"update\\\",\\\"delete\\\",\\\"approve\\\"],\\\"skipApiCheck\\\":true},\\\"mcp\\\":{\\\"include\\\":[]}},\\\"extends\\\":\\\"SmrtObject\\\",\\\"exportName\\\":\\\"LanguageOverride\\\",\\\"collectionExportName\\\":\\\"LanguageOverrideCollection\\\",\\\"validationRules\\\":[{\\\"field\\\":\\\"key\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"},{\\\"field\\\":\\\"locale\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"},{\\\"field\\\":\\\"template\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"text\\\"},{\\\"field\\\":\\\"auto_generated\\\",\\\"rule\\\":\\\"required\\\",\\\"fieldType\\\":\\\"boolean\\\"}],\\\"schema\\\":{\\\"tableName\\\":\\\"_smrt_language_overrides\\\",\\\"ddl\\\":\\\"CREATE TABLE IF NOT EXISTS \\\\\\\"_smrt_language_overrides\\\\\\\" (\\\\n \\\\\\\"id\\\\\\\" UUID PRIMARY KEY NOT NULL,\\\\n \\\\\\\"slug\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"context\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"created_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"updated_at\\\\\\\" TIMESTAMP NOT NULL DEFAULT current_timestamp,\\\\n \\\\\\\"key\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"locale\\\\\\\" TEXT NOT NULL DEFAULT '',\\\\n \\\\\\\"tenant_id\\\\\\\" UUID,\\\\n \\\\\\\"template\\\\\\\" TEXT NOT NULL,\\\\n \\\\\\\"auto_generated\\\\\\\" BOOLEAN NOT NULL DEFAULT FALSE,\\\\n \\\\\\\"source_hash\\\\\\\" TEXT,\\\\n \\\\\\\"ai_model\\\\\\\" TEXT,\\\\n \\\\\\\"reviewed_at\\\\\\\" TEXT,\\\\n \\\\\\\"reviewed_by\\\\\\\" TEXT\\\\n);\\\",\\\"columns\\\":{\\\"id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"primaryKey\\\":true,\\\"referenceKind\\\":\\\"id\\\",\\\"notNull\\\":true},\\\"slug\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true},\\\"context\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"\\\"},\\\"created_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"updated_at\\\":{\\\"type\\\":\\\"TIMESTAMP\\\",\\\"notNull\\\":true,\\\"default\\\":\\\"current_timestamp\\\"},\\\"key\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":\\\"\\\"},\\\"locale\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":\\\"\\\"},\\\"tenant_id\\\":{\\\"type\\\":\\\"UUID\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"template\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":true,\\\"unique\\\":false},\\\"auto_generated\\\":{\\\"type\\\":\\\"BOOLEAN\\\",\\\"notNull\\\":true,\\\"unique\\\":false,\\\"default\\\":false},\\\"source_hash\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"ai_model\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"reviewed_at\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false},\\\"reviewed_by\\\":{\\\"type\\\":\\\"TEXT\\\",\\\"notNull\\\":false,\\\"unique\\\":false}},\\\"indexes\\\":[{\\\"name\\\":\\\"_smrt_language_overrides_key_locale_idx\\\",\\\"columns\\\":[\\\"key\\\",\\\"locale\\\",\\\"context\\\"],\\\"unique\\\":true},{\\\"name\\\":\\\"_smrt_language_overrides_slug_context_idx\\\",\\\"columns\\\":[\\\"slug\\\",\\\"context\\\"]},{\\\"name\\\":\\\"_smrt_language_overrides_created_at_idx\\\",\\\"columns\\\":[\\\"created_at\\\"]}],\\\"version\\\":\\\"91442913\\\"},\\\"importPath\\\":\\\"@happyvertical/smrt-languages\\\"}},\\\"moduleType\\\":\\\"smrt\\\",\\\"smrtDependencies\\\":[\\\"@happyvertical/smrt-core\\\",\\\"@happyvertical/smrt-features\\\",\\\"@happyvertical/smrt-jobs\\\",\\\"@happyvertical/smrt-prompts\\\",\\\"@happyvertical/smrt-tenancy\\\"],\\\"browser\\\":{\\\"status\\\":\\\"server-only\\\",\\\"issues\\\":[\\\"#3615\\\",\\\"#3618\\\",\\\"#3624\\\",\\\"#3627\\\"],\\\"reason\\\":\\\"node:crypto\\\",\\\"via\\\":[\\\"@happyvertical/smrt-features\\\",\\\"@happyvertical/smrt-jobs\\\",\\\"@happyvertical/smrt-tenancy\\\"]},\\\"stampsConstructors\\\":true}\"));\n","import { getPackageConfig } from '@happyvertical/smrt-config';\nimport { getTenantId } from '@happyvertical/smrt-tenancy';\nimport {\n getCachedLanguage,\n getLanguageCacheGeneration,\n invalidateLanguageCache,\n setCachedLanguage,\n} from './cache.js';\nimport { LanguageOverrideCollection } from './collections/LanguageOverrideCollection.js';\nimport { LanguageRegistry } from './language-registry.js';\nimport type {\n LanguageCacheValue,\n LanguagesPackageConfig,\n ResolvedLanguageString,\n ResolveLanguageStringOptions,\n} from './types.js';\nimport {\n buildLocaleFallbackChain,\n normalizeLocale,\n renderTemplate,\n} from './utils.js';\n\nconst DEFAULT_LOCALE = 'en';\n\nfunction getLanguagesConfig(): LanguagesPackageConfig {\n return getPackageConfig<LanguagesPackageConfig>('languages', {\n defaultLocale: DEFAULT_LOCALE,\n overrides: {},\n });\n}\n\ninterface ResolutionAttempt {\n template: string | null;\n source: ResolvedLanguageString['source'];\n resolvedFromLocale: string;\n}\n\nasync function resolveBaseForLocale(\n key: string,\n locale: string,\n options: ResolveLanguageStringOptions,\n config: LanguagesPackageConfig,\n): Promise<ResolutionAttempt> {\n const tenantId =\n options.tenantId !== undefined ? options.tenantId : (getTenantId() ?? null);\n\n let collection: LanguageOverrideCollection | null = null;\n if (options.db) {\n collection = await LanguageOverrideCollection.create({\n db: options.db,\n });\n }\n\n const cacheDb = collection?.db ?? options.db;\n const cached = getCachedLanguage(key, locale, tenantId, cacheDb);\n if (cached) {\n return {\n template: cached.template,\n source: cached.source,\n resolvedFromLocale: cached.resolvedFromLocale,\n };\n }\n\n // Capture the invalidation generation before the asynchronous layer loads. A\n // write that lands while they are in flight bumps it, and the cache write in\n // `finalize` is then refused rather than repopulating the entry the write\n // just invalidated with the pre-write value.\n const loadedAtGeneration = getLanguageCacheGeneration(key, locale, cacheDb);\n\n // 1. Tenant override (highest precedence among stored layers).\n if (collection && tenantId) {\n const tenantOverride = await collection.getTenantOverride(\n key,\n locale,\n tenantId,\n );\n if (tenantOverride) {\n return finalize({\n key,\n locale,\n tenantId,\n db: cacheDb,\n loadedAtGeneration,\n attempt: {\n template: tenantOverride.template,\n source: 'tenant',\n resolvedFromLocale: locale,\n },\n });\n }\n }\n\n // 2. App-level override (incl. AI auto-translations).\n if (collection) {\n const appOverride = await collection.getAppOverride(key, locale);\n if (appOverride) {\n return finalize({\n key,\n locale,\n tenantId,\n db: cacheDb,\n loadedAtGeneration,\n attempt: {\n template: appOverride.template,\n source: 'app',\n resolvedFromLocale: locale,\n },\n });\n }\n }\n\n // 3. File-config override. Locale keys in user-authored config files\n // commonly come in mixed case (`fr-ca`, `Fr-CA`, etc.); normalize lookup so\n // config matches the same way the DB and code-default layers do.\n const configForKey = config.overrides?.[key];\n const configTemplate = lookupNormalizedLocale(configForKey, locale);\n if (typeof configTemplate === 'string') {\n return finalize({\n key,\n locale,\n tenantId,\n db: cacheDb,\n loadedAtGeneration,\n attempt: {\n template: configTemplate,\n source: 'config',\n resolvedFromLocale: locale,\n },\n });\n }\n\n // 4. Code default.\n const definition = LanguageRegistry.get(key, locale);\n if (definition) {\n return finalize({\n key,\n locale,\n tenantId,\n db: cacheDb,\n loadedAtGeneration,\n attempt: {\n template: definition.template,\n source: 'code',\n resolvedFromLocale: locale,\n },\n });\n }\n\n return {\n template: null,\n source: 'missing',\n resolvedFromLocale: locale,\n };\n}\n\nfunction lookupNormalizedLocale(\n bag: Record<string, string> | undefined,\n locale: string,\n): string | undefined {\n if (!bag) return undefined;\n // Fast path: exact match (most file configs use the canonical form).\n if (typeof bag[locale] === 'string') return bag[locale];\n for (const [candidate, template] of Object.entries(bag)) {\n if (normalizeLocale(candidate) === locale) {\n return template;\n }\n }\n return undefined;\n}\n\nfunction finalize(args: {\n key: string;\n locale: string;\n tenantId: string | null;\n db: unknown;\n attempt: ResolutionAttempt;\n loadedAtGeneration: number;\n}): ResolutionAttempt {\n if (args.attempt.template !== null) {\n const value: LanguageCacheValue = {\n key: args.key,\n locale: args.locale,\n template: args.attempt.template,\n source: args.attempt.source,\n resolvedFromLocale: args.attempt.resolvedFromLocale,\n };\n setCachedLanguage(\n args.key,\n args.locale,\n args.tenantId,\n args.db,\n value,\n args.loadedAtGeneration,\n );\n }\n return args.attempt;\n}\n\nexport async function resolveLanguageString(\n key: string,\n options: ResolveLanguageStringOptions = {},\n): Promise<ResolvedLanguageString> {\n if (!key || key.trim() === '') {\n throw new Error('resolveLanguageString requires a non-empty key');\n }\n\n const config = getLanguagesConfig();\n const defaultLocale = normalizeLocale(config.defaultLocale ?? DEFAULT_LOCALE);\n const requested = normalizeLocale(options.locale ?? defaultLocale);\n\n // Runtime override always wins, regardless of locale chain.\n if (typeof options.overrides?.template === 'string') {\n return {\n key,\n locale: requested,\n template: options.overrides.template,\n text: renderTemplate(options.overrides.template, options.vars),\n resolvedFromLocale: requested,\n source: 'runtime',\n };\n }\n\n const chain = buildLocaleFallbackChain(requested, defaultLocale);\n\n let firstHit: ResolutionAttempt | null = null;\n let firstLocale = requested;\n for (const locale of chain) {\n const attempt = await resolveBaseForLocale(key, locale, options, config);\n if (attempt.template !== null) {\n firstHit = attempt;\n firstLocale = locale;\n break;\n }\n }\n\n if (!firstHit) {\n if (options.strict) {\n throw new Error(\n `Language string \"${key}\" has no resolution for locale \"${requested}\"`,\n );\n }\n // Soft-miss: enqueue translation if the requested locale differs from the\n // default and a default-locale source exists. Failure of the enqueue path\n // must never break resolution — we still return the key as the rendered\n // text so the UI degrades gracefully.\n await tryEnqueueTranslation({\n key,\n requestedLocale: requested,\n defaultLocale,\n options,\n });\n return {\n key,\n locale: requested,\n template: key,\n text: key,\n resolvedFromLocale: requested,\n source: 'missing',\n };\n }\n\n // Hit at a fallback locale that isn't the requested one — fire-and-forget a\n // translation job for the requested target.\n if (firstLocale !== requested) {\n await tryEnqueueTranslation({\n key,\n requestedLocale: requested,\n defaultLocale,\n options,\n });\n return {\n key,\n locale: requested,\n template: firstHit.template ?? key,\n text: renderTemplate(firstHit.template ?? key, options.vars),\n resolvedFromLocale: firstLocale,\n source: 'fallback',\n };\n }\n\n return {\n key,\n locale: requested,\n template: firstHit.template ?? key,\n text: renderTemplate(firstHit.template ?? key, options.vars),\n resolvedFromLocale: firstLocale,\n source: firstHit.source,\n };\n}\n\n/**\n * Best-effort enqueue of an AI translation job. Wrapped so resolution never\n * fails because of an enqueue error, missing optional dependency, etc.\n *\n * The actual job machinery lives in `./jobs/translation-job.ts` and is loaded\n * lazily so the resolver does not pull `@happyvertical/ai` into the import\n * graph for synchronous resolves that never miss.\n */\nasync function tryEnqueueTranslation(args: {\n key: string;\n requestedLocale: string;\n defaultLocale: string;\n options: ResolveLanguageStringOptions;\n}): Promise<void> {\n if (!args.options.db) {\n // Without a DB we cannot persist a job; skip silently — resolver still\n // returns a fallback to the caller.\n return;\n }\n if (normalizeLocale(args.requestedLocale) === args.defaultLocale) {\n return;\n }\n\n try {\n const { enqueueTranslationJob } = await import('./jobs/translation-job.js');\n await enqueueTranslationJob({\n key: args.key,\n targetLocale: args.requestedLocale,\n sourceLocale: args.defaultLocale,\n tenantId: args.options.tenantId ?? getTenantId() ?? null,\n db: args.options.db,\n });\n } catch {\n // Enqueueing must never break resolution.\n }\n}\n\nexport function invalidateResolvedLanguageCache(\n key: string,\n locale: string,\n tenantId: string | null | undefined,\n db: unknown,\n): void {\n invalidateLanguageCache(key, locale, tenantId, db);\n}\n","/**\n * @happyvertical/smrt-languages\n *\n * Code-first language strings with config + tenant overrides and AI-driven\n * auto-translation for SMRT applications. Mirrors the architecture of\n * `@happyvertical/smrt-prompts` plus a smrt-jobs handler that fills gaps in\n * non-default locales the first time a string is requested.\n *\n * The package root intentionally exposes only the read path\n * (`defineLanguageString`, `resolveLanguageString`, `LanguageOverride`, the\n * cache helpers, glossary helpers, and shared types/utilities). The\n * translation-worker stack — which depends on `@happyvertical/ai`,\n * smrt-jobs, smrt-features, and smrt-prompts — lives on the\n * `@happyvertical/smrt-languages/jobs` subpath, and the admin CLI helpers\n * live on `@happyvertical/smrt-languages/cli`. Resolver consumers (the vast\n * majority of call sites) never load the worker dependency tree.\n *\n * @packageDocumentation\n */\n\nimport './__smrt-register__.js';\n\nexport {\n clearLanguageCache,\n getLanguageCacheTtlMs,\n invalidateLanguageCache,\n} from './cache.js';\nexport { LanguageOverrideCollection } from './collections/LanguageOverrideCollection.js';\nexport { buildTenantGlossary } from './glossary.js';\n\nexport {\n defineLanguageString,\n LanguageRegistry,\n} from './language-registry.js';\n\nexport {\n invalidateResolvedLanguageCache,\n resolveLanguageString,\n} from './language-resolver.js';\n\nexport {\n LanguageOverride,\n type LanguageOverrideCtorOptions,\n} from './models/LanguageOverride.js';\n\nexport type {\n LanguageCacheValue,\n LanguageOverrideOptions,\n LanguageStringDefinition,\n LanguageStringDefinitionInput,\n LanguagesPackageConfig,\n LanguageVariables,\n ResolvedLanguageString,\n ResolveLanguageStringOptions,\n} from './types.js';\n\nexport {\n buildLocaleFallbackChain,\n buildTranslationJobId,\n computeSourceHash,\n normalizeLocale,\n renderTemplate,\n} from './utils.js';\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":";;;;;AAOA,eAAe,wBAAwB,KAAK,MAAM,09UAA4zX,CAAC;;;ACe/2X,IAAM,iBAAiB;AAEvB,SAAS,qBAA6C;CACpD,OAAO,iBAAyC,aAAa;EAC3D,eAAe;EACf,WAAW,CAAC;CACd,CAAC;AACH;AAQA,eAAe,qBACb,KACA,QACA,SACA,QAC4B;CAC5B,MAAM,WACJ,QAAQ,aAAa,KAAA,IAAY,QAAQ,WAAY,YAAY,KAAK;CAExE,IAAI,aAAgD;CACpD,IAAI,QAAQ,IACV,aAAa,MAAM,2BAA2B,OAAO,EACnD,IAAI,QAAQ,GACd,CAAC;CAGH,MAAM,UAAU,YAAY,MAAM,QAAQ;CAC1C,MAAM,SAAS,kBAAkB,KAAK,QAAQ,UAAU,OAAO;CAC/D,IAAI,QACF,OAAO;EACL,UAAU,OAAO;EACjB,QAAQ,OAAO;EACf,oBAAoB,OAAO;CAC7B;CAOF,MAAM,qBAAqB,2BAA2B,KAAK,QAAQ,OAAO;CAG1E,IAAI,cAAc,UAAU;EAC1B,MAAM,iBAAiB,MAAM,WAAW,kBACtC,KACA,QACA,QACF;EACA,IAAI,gBACF,OAAO,SAAS;GACd;GACA;GACA;GACA,IAAI;GACJ;GACA,SAAS;IACP,UAAU,eAAe;IACzB,QAAQ;IACR,oBAAoB;GACtB;EACF,CAAC;CAEL;CAGA,IAAI,YAAY;EACd,MAAM,cAAc,MAAM,WAAW,eAAe,KAAK,MAAM;EAC/D,IAAI,aACF,OAAO,SAAS;GACd;GACA;GACA;GACA,IAAI;GACJ;GACA,SAAS;IACP,UAAU,YAAY;IACtB,QAAQ;IACR,oBAAoB;GACtB;EACF,CAAC;CAEL;CAKA,MAAM,eAAe,OAAO,YAAY;CACxC,MAAM,iBAAiB,uBAAuB,cAAc,MAAM;CAClE,IAAI,OAAO,mBAAmB,UAC5B,OAAO,SAAS;EACd;EACA;EACA;EACA,IAAI;EACJ;EACA,SAAS;GACP,UAAU;GACV,QAAQ;GACR,oBAAoB;EACtB;CACF,CAAC;CAIH,MAAM,aAAa,iBAAiB,IAAI,KAAK,MAAM;CACnD,IAAI,YACF,OAAO,SAAS;EACd;EACA;EACA;EACA,IAAI;EACJ;EACA,SAAS;GACP,UAAU,WAAW;GACrB,QAAQ;GACR,oBAAoB;EACtB;CACF,CAAC;CAGH,OAAO;EACL,UAAU;EACV,QAAQ;EACR,oBAAoB;CACtB;AACF;AAEA,SAAS,uBACP,KACA,QACoB;CACpB,IAAI,CAAC,KAAK,OAAO,KAAA;CAEjB,IAAI,OAAO,IAAI,YAAY,UAAU,OAAO,IAAI;CAChD,KAAA,MAAW,CAAC,WAAW,aAAa,OAAO,QAAQ,GAAG,GACpD,IAAI,gBAAgB,SAAS,MAAM,QACjC,OAAO;AAIb;AAEA,SAAS,SAAS,MAOI;CACpB,IAAI,KAAK,QAAQ,aAAa,MAAM;EAClC,MAAM,QAA4B;GAChC,KAAK,KAAK;GACV,QAAQ,KAAK;GACb,UAAU,KAAK,QAAQ;GACvB,QAAQ,KAAK,QAAQ;GACrB,oBAAoB,KAAK,QAAQ;EACnC;EACA,kBACE,KAAK,KACL,KAAK,QACL,KAAK,UACL,KAAK,IACL,OACA,KAAK,kBACP;CACF;CACA,OAAO,KAAK;AACd;AAEA,eAAsB,sBACpB,KACA,UAAwC,CAAC,GACR;CACjC,IAAI,CAAC,OAAO,IAAI,KAAK,MAAM,IACzB,MAAM,IAAI,MAAM,gDAAgD;CAGlE,MAAM,SAAS,mBAAmB;CAClC,MAAM,gBAAgB,gBAAgB,OAAO,iBAAiB,cAAc;CAC5E,MAAM,YAAY,gBAAgB,QAAQ,UAAU,aAAa;CAGjE,IAAI,OAAO,QAAQ,WAAW,aAAa,UACzC,OAAO;EACL;EACA,QAAQ;EACR,UAAU,QAAQ,UAAU;EAC5B,MAAM,eAAe,QAAQ,UAAU,UAAU,QAAQ,IAAI;EAC7D,oBAAoB;EACpB,QAAQ;CACV;CAGF,MAAM,QAAQ,yBAAyB,WAAW,aAAa;CAE/D,IAAI,WAAqC;CACzC,IAAI,cAAc;CAClB,KAAA,MAAW,UAAU,OAAO;EAC1B,MAAM,UAAU,MAAM,qBAAqB,KAAK,QAAQ,SAAS,MAAM;EACvE,IAAI,QAAQ,aAAa,MAAM;GAC7B,WAAW;GACX,cAAc;GACd;EACF;CACF;CAEA,IAAI,CAAC,UAAU;EACb,IAAI,QAAQ,QACV,MAAM,IAAI,MACR,oBAAoB,IAAG,kCAAmC,UAAS,EACrE;EAMF,MAAM,sBAAsB;GAC1B;GACA,iBAAiB;GACjB;GACA;EACF,CAAC;EACD,OAAO;GACL;GACA,QAAQ;GACR,UAAU;GACV,MAAM;GACN,oBAAoB;GACpB,QAAQ;EACV;CACF;CAIA,IAAI,gBAAgB,WAAW;EAC7B,MAAM,sBAAsB;GAC1B;GACA,iBAAiB;GACjB;GACA;EACF,CAAC;EACD,OAAO;GACL;GACA,QAAQ;GACR,UAAU,SAAS,YAAY;GAC/B,MAAM,eAAe,SAAS,YAAY,KAAK,QAAQ,IAAI;GAC3D,oBAAoB;GACpB,QAAQ;EACV;CACF;CAEA,OAAO;EACL;EACA,QAAQ;EACR,UAAU,SAAS,YAAY;EAC/B,MAAM,eAAe,SAAS,YAAY,KAAK,QAAQ,IAAI;EAC3D,oBAAoB;EACpB,QAAQ,SAAS;CACnB;AACF;AAUA,eAAe,sBAAsB,MAKnB;CAChB,IAAI,CAAC,KAAK,QAAQ,IAGhB;CAEF,IAAI,gBAAgB,KAAK,eAAe,MAAM,KAAK,eACjD;CAGF,IAAI;EACF,MAAM,EAAE,0BAA0B,MAAM,OAAO,uCAAA,CAAA,MAAA,MAAA,EAAA,CAAA;EAC/C,MAAM,sBAAsB;GAC1B,KAAK,KAAK;GACV,cAAc,KAAK;GACnB,cAAc,KAAK;GACnB,UAAU,KAAK,QAAQ,YAAY,YAAY,KAAK;GACpD,IAAI,KAAK,QAAQ;EACnB,CAAC;CACH,QAAQ,CAER;AACF;AAEO,SAAS,gCACd,KACA,QACA,UACA,IACM;CACN,wBAAwB,KAAK,QAAQ,UAAU,EAAE;AACnD;;;AC7QO,IAAM,8BAA8B"}
package/dist/jobs.js CHANGED
@@ -1,2 +1,2 @@
1
- import { i as enqueueTranslationJob, n as LanguageTranslationTask, r as TRANSLATION_PROMPT_KEY, t as AUTO_TRANSLATE_FEATURE_KEY } from "./chunks/translation-job-B4sdwQ3W.js";
1
+ import { i as enqueueTranslationJob, n as LanguageTranslationTask, r as TRANSLATION_PROMPT_KEY, t as AUTO_TRANSLATE_FEATURE_KEY } from "./chunks/translation-job-DMWfy7m4.js";
2
2
  export { AUTO_TRANSLATE_FEATURE_KEY, LanguageTranslationTask, TRANSLATION_PROMPT_KEY, enqueueTranslationJob };
@@ -2,7 +2,7 @@
2
2
  "version": "1.0.0",
3
3
  "timestamp": 0,
4
4
  "packageName": "@happyvertical/smrt-languages",
5
- "packageVersion": "0.57.37",
5
+ "packageVersion": "0.57.38",
6
6
  "objects": {
7
7
  "@happyvertical/smrt-languages:LanguageOverrideCollection": {
8
8
  "name": "languageoverridecollection",
@@ -426,7 +426,13 @@
426
426
  "save": {
427
427
  "name": "save",
428
428
  "async": true,
429
- "parameters": [],
429
+ "parameters": [
430
+ {
431
+ "name": "options",
432
+ "type": "SmrtSaveOptions",
433
+ "optional": true
434
+ }
435
+ ],
430
436
  "returnType": "Promise<this>",
431
437
  "isStatic": false,
432
438
  "isPublic": true
@@ -1,4 +1,4 @@
1
- import { SmrtObject, type SmrtObjectOptions } from '@happyvertical/smrt-core';
1
+ import { SmrtObject, type SmrtObjectOptions, type SmrtSaveOptions } from '@happyvertical/smrt-core';
2
2
  import type { LanguageOverrideOptions } from '../types.js';
3
3
  export interface LanguageOverrideCtorOptions extends SmrtObjectOptions, LanguageOverrideOptions {
4
4
  }
@@ -18,7 +18,7 @@ export declare class LanguageOverride extends SmrtObject {
18
18
  /** User ID of the reviewer. */
19
19
  reviewed_by: string | null;
20
20
  constructor(options?: LanguageOverrideCtorOptions);
21
- save(): Promise<this>;
21
+ save(options?: SmrtSaveOptions): Promise<this>;
22
22
  private saveAfterIdentityChange;
23
23
  private saveAfterIdentityChangeInTransaction;
24
24
  private saveAfterIdentityChangeWithDeferredDelete;
@@ -1 +1 @@
1
- {"version":3,"file":"LanguageOverride.d.ts","sourceRoot":"","sources":["../../src/models/LanguageOverride.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,UAAU,EACV,KAAK,iBAAiB,EAEvB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAQ3D,MAAM,WAAW,2BACf,SAAQ,iBAAiB,EACvB,uBAAuB;CAAG;AAE9B,qBAYa,gBAAiB,SAAQ,UAAU;IAE9C,GAAG,EAAE,MAAM,CAAM;IAGjB,MAAM,EAAE,MAAM,CAAM;IAKpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAG/B,QAAQ,EAAE,MAAM,CAAM;IAEtB,iEAAiE;IAEjE,cAAc,EAAE,OAAO,CAAS;IAEhC,oFAAoF;IAEpF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAElC,wDAAwD;IAExD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B,mEAAmE;IAEnE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAElC,+BAA+B;IAE/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAEtB,OAAO,GAAE,2BAAgC;IAoBtC,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;YA+CtB,uBAAuB;YAOvB,oCAAoC;YA+BpC,yCAAyC;IA4BxC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAQtC;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAMlC,oBAAoB;CA0BnC;AAED,YAAY,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC"}
1
+ {"version":3,"file":"LanguageOverride.d.ts","sourceRoot":"","sources":["../../src/models/LanguageOverride.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,UAAU,EACV,KAAK,iBAAiB,EACtB,KAAK,eAAe,EAErB,MAAM,0BAA0B,CAAC;AAGlC,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC;AAQ3D,MAAM,WAAW,2BACf,SAAQ,iBAAiB,EACvB,uBAAuB;CAAG;AAE9B,qBAYa,gBAAiB,SAAQ,UAAU;IAE9C,GAAG,EAAE,MAAM,CAAM;IAGjB,MAAM,EAAE,MAAM,CAAM;IAKpB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAG/B,QAAQ,EAAE,MAAM,CAAM;IAEtB,iEAAiE;IAEjE,cAAc,EAAE,OAAO,CAAS;IAEhC,oFAAoF;IAEpF,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAElC,wDAAwD;IAExD,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAQ;IAE/B,mEAAmE;IAEnE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;IAElC,+BAA+B;IAE/B,WAAW,EAAE,MAAM,GAAG,IAAI,CAAQ;gBAEtB,OAAO,GAAE,2BAAgC;IAoBtC,IAAI,CAAC,OAAO,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC;YA+C/C,uBAAuB;YASvB,oCAAoC;YAuCpC,yCAAyC;IAyCxC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAQtC;;;OAGG;IACG,OAAO,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAMlC,oBAAoB;CA0BnC;AAED,YAAY,EAAE,uBAAuB,EAAE,MAAM,aAAa,CAAC"}
@@ -3,12 +3,12 @@
3
3
  "sensitiveFieldsExcluded": true,
4
4
  "generatedAt": "1970-01-01T00:00:00.000Z",
5
5
  "packageName": "@happyvertical/smrt-languages",
6
- "packageVersion": "0.57.37",
6
+ "packageVersion": "0.57.38",
7
7
  "sourceManifestPath": "dist/manifest.json",
8
8
  "agentDocPath": "AGENTS.md",
9
9
  "sourceHashes": {
10
- "manifest": "6d17b02d49b699c1ac43d1d214e84b27ae6bb7f83a6287aae664d53212005fdc",
11
- "packageJson": "bd49a50f6fa02979dfd7bbe95cbcd09a9b9650b5a3f4a2f7a51ecce55dfe5f4c",
10
+ "manifest": "b0e2af0552e39977212b2fcb71c7f86746a379f42be7a3938dc68584ab9c5c49",
11
+ "packageJson": "47aad3f856168ce2cb313e8c57c0a288b8e36db1af08bb08478806378f5efc80",
12
12
  "agents": "45d290839ba542a82a3cdf421d919a9dbc31f5280823ac432662bd678a087d7a"
13
13
  },
14
14
  "exports": [
@@ -298,6 +298,9 @@
298
298
  {
299
299
  "name": "save",
300
300
  "async": true,
301
+ "params": [
302
+ "options?: SmrtSaveOptions"
303
+ ],
301
304
  "returns": "Promise<this>"
302
305
  }
303
306
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/smrt-languages",
3
- "version": "0.57.37",
3
+ "version": "0.57.38",
4
4
  "description": "Code-first language strings with config + tenant overrides and AI auto-translation for SMRT",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -29,17 +29,17 @@
29
29
  "dependencies": {
30
30
  "@happyvertical/ai": "^0.110.0",
31
31
  "@happyvertical/logger": "^0.110.0",
32
- "@happyvertical/smrt-config": "0.57.37",
33
- "@happyvertical/smrt-core": "0.57.37",
34
- "@happyvertical/smrt-features": "0.57.37",
35
- "@happyvertical/smrt-jobs": "0.57.37",
36
- "@happyvertical/smrt-prompts": "0.57.37",
37
- "@happyvertical/smrt-tenancy": "0.57.37",
32
+ "@happyvertical/smrt-config": "0.57.38",
33
+ "@happyvertical/smrt-core": "0.57.38",
34
+ "@happyvertical/smrt-features": "0.57.38",
35
+ "@happyvertical/smrt-jobs": "0.57.38",
36
+ "@happyvertical/smrt-prompts": "0.57.38",
37
+ "@happyvertical/smrt-tenancy": "0.57.38",
38
38
  "@happyvertical/sql": "^0.110.0"
39
39
  },
40
40
  "devDependencies": {
41
- "@happyvertical/smrt-cli": "0.57.37",
42
- "@happyvertical/smrt-vitest": "0.57.37",
41
+ "@happyvertical/smrt-cli": "0.57.38",
42
+ "@happyvertical/smrt-vitest": "0.57.38",
43
43
  "@types/node": "24.13.6",
44
44
  "typescript": "6.0.3",
45
45
  "vite": "8.1.6",
@@ -1 +0,0 @@
1
- {"version":3,"file":"language-registry-BO6kxyz8.js","names":[],"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>();\n/**\n * Monotonic per-`(db, key, locale)` invalidation counter; see\n * getLanguageCacheGeneration.\n */\nconst cacheGenerations = new Map<string, number>();\n/**\n * Source of every generation value. Drawing from one counter instead of\n * incrementing each entry independently makes a generation unique across every\n * `(key, locale)` and every clear, which is what lets `clearedThrough` work as\n * a floor.\n */\nlet nextGeneration = 1;\n/**\n * Floor that `clearLanguageCache()` raises for *every* `(key, locale)`,\n * including pairs with no map entry. A pair that has never been invalidated in\n * this process reads as generation 0, so a resolution that captured 0 before a\n * clear would still see 0 after it and write its pre-clear value back — the\n * flush would be silently undone for the rest of the TTL.\n */\nlet clearedThrough = 0;\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\nfunction buildGenerationKey(\n key: string,\n locale: string,\n db: DatabaseInterface | unknown,\n): string {\n return `${getDbNamespace(db)}::${key}::${normalizeLocale(locale)}`;\n}\n\nfunction bumpGeneration(\n key: string,\n locale: string,\n db: DatabaseInterface | unknown,\n): void {\n cacheGenerations.set(buildGenerationKey(key, locale, db), nextGeneration++);\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\n/**\n * Reads the current invalidation generation for a `(key, locale)` pair.\n *\n * A resolution captures this *before* its asynchronous layer loads and hands\n * it back to {@link setCachedLanguage}. Any write that lands while those loads\n * are in flight bumps the generation, so the in-flight resolution — which read\n * the pre-write layers — is refused the cache write instead of repopulating\n * the entry the write just invalidated. Without this, a read that raced a\n * write served the pre-write value for the full TTL, and a raced `delete()`\n * resurrected the deleted override.\n *\n * Granularity is `(db, key, locale)`, not `(db, key)` as in smrt-playbooks and\n * smrt-prompts, and not `(db, key, locale, tenantId)`:\n *\n * - `locale` is part of the key because every layer a cached entry is built\n * from is read at exactly the locale it is cached under. `resolveLanguageString`\n * walks a fallback chain (`fr-CA` → `fr` → `en`), but each attempt reads its\n * own locale's rows (`getTenantOverride`/`getAppOverride` are called with\n * that locale) and caches the result under that same locale; an attempt that\n * resolves nothing caches nothing, so a fallback hit is never stored under\n * the requested locale. Every writer — `LanguageOverride.save()`/`delete()`\n * (both identities, when one changes) and the translation job — invalidates\n * the same `(key, locale)` it wrote. A per-locale generation therefore still\n * covers every write that can change what a cached entry was built from.\n * - `tenantId` is deliberately *not* part of the key, because an app-level row\n * (tenantId null) is inherited by every tenant, so a write to any scope of a\n * `(key, locale)` must be able to refuse an in-flight write for any other\n * scope of it. This matches the coarsest fan-out of\n * {@link invalidateLanguageCache}.\n *\n * A pair with no entry of its own reports the {@link clearLanguageCache} floor\n * rather than a bare 0, so a clear refuses in-flight writes for pairs that have\n * never been invalidated too.\n */\nexport function getLanguageCacheGeneration(\n key: string,\n locale: string,\n db: DatabaseInterface | unknown,\n): number {\n const generation =\n cacheGenerations.get(buildGenerationKey(key, locale, db)) ?? 0;\n return generation > clearedThrough ? generation : clearedThrough;\n}\n\nexport function setCachedLanguage(\n key: string,\n locale: string,\n tenantId: string | null | undefined,\n db: DatabaseInterface | unknown,\n value: LanguageCacheValue,\n loadedAtGeneration: number,\n): void {\n if (getLanguageCacheGeneration(key, locale, db) !== loadedAtGeneration) {\n // A write landed while this resolution was loading. Its value is already\n // stale, so drop it rather than poison the entry for the whole TTL.\n return;\n }\n\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 bumpGeneration(key, normalizedLocale, db);\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 // Raise the floor instead of resetting counters: a resolution that started\n // before the clear must be refused its write, and raising a single floor\n // covers pairs with no generation entry, which a per-entry bump cannot reach.\n // With the floor carrying the refusal, the per-entry values can be dropped.\n clearedThrough = nextGeneration++;\n cacheGenerations.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 { static __smrtPackage__ = \"@happyvertical/smrt-languages\";\n @field({ required: true })\n key: string = '';\n\n @field({ required: true })\n locale: string = '';\n\n // Not tenant-scoped (any scope is writable), but the value is a tenant id:\n // uuid on Postgres, so it compares with every other tenant_id column.\n @field({ sqlType: 'UUID', 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"],"mappings":"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"}