@happyvertical/smrt-languages 0.51.21 → 0.51.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +3 -0
- package/dist/chunks/{translation-job-C4YLXz8M.js → translation-job-CYJya3SL.js} +2 -2
- package/dist/chunks/translation-job-CYJya3SL.js.map +1 -0
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/jobs.js +1 -1
- package/dist/manifest.json +89 -89
- package/dist/smrt-knowledge.json +44 -44
- package/package.json +12 -12
- package/dist/chunks/translation-job-C4YLXz8M.js.map +0 -1
package/AGENTS.md
CHANGED
|
@@ -18,6 +18,9 @@ auto-translation for missing locales.
|
|
|
18
18
|
- `enqueueTranslationJob({ key, targetLocale })` writes a `LanguageTranslationTask`
|
|
19
19
|
job into the `languages` queue with a deterministic dedup ID so concurrent
|
|
20
20
|
resolver misses collapse into one job.
|
|
21
|
+
- Keep `LanguageTranslationTask` under `src/jobs/`: the manifest generator uses
|
|
22
|
+
that path to stamp its `@happyvertical/smrt-languages/jobs` import path. The
|
|
23
|
+
package root intentionally excludes the translation-worker dependency tree.
|
|
21
24
|
|
|
22
25
|
## Locale-miss flow
|
|
23
26
|
|
|
@@ -18,7 +18,7 @@ var __exportAll = (all, no_symbols) => {
|
|
|
18
18
|
return target;
|
|
19
19
|
};
|
|
20
20
|
//#endregion
|
|
21
|
-
//#region src/translation-job.ts
|
|
21
|
+
//#region src/jobs/translation-job.ts
|
|
22
22
|
var translation_job_exports = /* @__PURE__ */ __exportAll({
|
|
23
23
|
AUTO_TRANSLATE_FEATURE_KEY: () => AUTO_TRANSLATE_FEATURE_KEY,
|
|
24
24
|
LanguageTranslationTask: () => LanguageTranslationTask,
|
|
@@ -254,4 +254,4 @@ function containsObviousMarkupLeak(value) {
|
|
|
254
254
|
//#endregion
|
|
255
255
|
export { translation_job_exports as a, enqueueTranslationJob as i, LanguageTranslationTask as n, TRANSLATION_PROMPT_KEY as r, AUTO_TRANSLATE_FEATURE_KEY as t };
|
|
256
256
|
|
|
257
|
-
//# sourceMappingURL=translation-job-
|
|
257
|
+
//# sourceMappingURL=translation-job-CYJya3SL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"translation-job-CYJya3SL.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 {\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,4 +1,4 @@
|
|
|
1
|
-
import { i as enqueueTranslationJob } from "./chunks/translation-job-
|
|
1
|
+
import { i as enqueueTranslationJob } from "./chunks/translation-job-CYJya3SL.js";
|
|
2
2
|
import { h as normalizeLocale, i as LanguageOverrideCollection, t as LanguageRegistry } from "./chunks/language-registry-BUnsVanY.js";
|
|
3
3
|
//#region src/cli.ts
|
|
4
4
|
async function translateMissing(options) {
|
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { LanguageOverrideCollection } from './collections/LanguageOverrideCollection.js';\nimport {
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["import type { SmrtClassOptions } from '@happyvertical/smrt-core';\nimport { LanguageOverrideCollection } from './collections/LanguageOverrideCollection.js';\nimport { enqueueTranslationJob } from './jobs/translation-job.js';\nimport { LanguageRegistry } from './language-registry.js';\nimport { normalizeLocale } from './utils.js';\n\n/**\n * Implementation of `smrt languages translate --locales=es,fr`.\n *\n * Walks the registered code defaults and enqueues a translation job for every\n * `(key, locale)` pair that does not yet have an app-level override. The job\n * itself respects the same dedup / budget / allowlist rules as the resolver's\n * miss-path enqueue.\n */\nexport async function translateMissing(options: {\n locales: string[];\n sourceLocale?: string;\n db: SmrtClassOptions['db'];\n tenantId?: string | null;\n}): Promise<{\n enqueued: string[];\n skipped: string[];\n}> {\n const sourceLocale = normalizeLocale(options.sourceLocale ?? 'en');\n const targets = options.locales.map(normalizeLocale).filter(Boolean);\n const overrides = await LanguageOverrideCollection.create({\n db: options.db,\n });\n\n const enqueued: string[] = [];\n const skipped: string[] = [];\n\n for (const definition of LanguageRegistry.getAll()) {\n if (definition.locale !== sourceLocale) continue;\n\n for (const target of targets) {\n if (target === sourceLocale) continue;\n const existing = await overrides.getAppOverride(definition.key, target);\n // Human edits win permanently — never enqueue an AI translation job for\n // a row that an admin has curated, even if the source has changed. The\n // CLAUDE.md spec calls this out and the job handler enforces the same\n // rule, but we prefer to skip at the enqueue layer so we don't burn\n // queue budget on work the handler is going to discard.\n if (existing && !existing.auto_generated) {\n skipped.push(`${definition.key}:${target}`);\n continue;\n }\n if (existing && existing.source_hash === definition.sourceHash) {\n skipped.push(`${definition.key}:${target}`);\n continue;\n }\n\n const result = await enqueueTranslationJob({\n key: definition.key,\n targetLocale: target,\n sourceLocale,\n tenantId: options.tenantId ?? null,\n db: options.db,\n });\n\n if (result.status === 'enqueued') {\n enqueued.push(`${definition.key}:${target}`);\n } else {\n skipped.push(`${definition.key}:${target}`);\n }\n }\n }\n\n return { enqueued, skipped };\n}\n\n/** `smrt languages review --locale=es` — list unreviewed auto-translations. */\nexport async function listUnreviewedAutoTranslations(options: {\n db: SmrtClassOptions['db'];\n locale?: string;\n limit?: number;\n}) {\n const overrides = await LanguageOverrideCollection.create({\n db: options.db,\n });\n return overrides.listUnreviewedAutoTranslations({\n locale: options.locale,\n limit: options.limit,\n });\n}\n\n/** `smrt languages approve <id> --reviewer=<user>` — mark reviewed. */\nexport async function approveAutoTranslation(options: {\n db: SmrtClassOptions['db'];\n id: string;\n reviewer: string;\n}) {\n const overrides = await LanguageOverrideCollection.create({\n db: options.db,\n });\n const row = await overrides.get({ id: options.id });\n if (!row) {\n throw new Error(`LanguageOverride \"${options.id}\" not found`);\n }\n return row.approve(options.reviewer);\n}\n\n/** `smrt languages edit <id> --template=...` — human-edit suppresses auto-overwrites. */\nexport async function editLanguageOverride(options: {\n db: SmrtClassOptions['db'];\n id: string;\n template: string;\n reviewer?: string;\n}) {\n const overrides = await LanguageOverrideCollection.create({\n db: options.db,\n });\n const row = await overrides.get({ id: options.id });\n if (!row) {\n throw new Error(`LanguageOverride \"${options.id}\" not found`);\n }\n row.template = options.template;\n row.auto_generated = false;\n row.ai_model = null;\n if (options.reviewer) {\n row.reviewed_at = new Date().toISOString();\n row.reviewed_by = options.reviewer;\n }\n await row.save();\n return row;\n}\n"],"mappings":";;;AAcA,eAAsB,iBAAiB,SAQpC;CACD,MAAM,eAAe,gBAAgB,QAAQ,gBAAgB,IAAI;CACjE,MAAM,UAAU,QAAQ,QAAQ,IAAI,eAAe,CAAA,CAAE,OAAO,OAAO;CACnE,MAAM,YAAY,MAAM,2BAA2B,OAAO,EACxD,IAAI,QAAQ,GACd,CAAC;CAED,MAAM,WAAqB,CAAC;CAC5B,MAAM,UAAoB,CAAC;CAE3B,KAAA,MAAW,cAAc,iBAAiB,OAAO,GAAG;EAClD,IAAI,WAAW,WAAW,cAAc;EAExC,KAAA,MAAW,UAAU,SAAS;GAC5B,IAAI,WAAW,cAAc;GAC7B,MAAM,WAAW,MAAM,UAAU,eAAe,WAAW,KAAK,MAAM;GAMtE,IAAI,YAAY,CAAC,SAAS,gBAAgB;IACxC,QAAQ,KAAK,GAAG,WAAW,IAAG,GAAI,QAAQ;IAC1C;GACF;GACA,IAAI,YAAY,SAAS,gBAAgB,WAAW,YAAY;IAC9D,QAAQ,KAAK,GAAG,WAAW,IAAG,GAAI,QAAQ;IAC1C;GACF;GAUA,KAAI,MARiB,sBAAsB;IACzC,KAAK,WAAW;IAChB,cAAc;IACd;IACA,UAAU,QAAQ,YAAY;IAC9B,IAAI,QAAQ;GACd,CAAC,EAAA,CAEU,WAAW,YACpB,SAAS,KAAK,GAAG,WAAW,IAAG,GAAI,QAAQ;QAE3C,QAAQ,KAAK,GAAG,WAAW,IAAG,GAAI,QAAQ;EAE9C;CACF;CAEA,OAAO;EAAE;EAAU;CAAQ;AAC7B;AAGA,eAAsB,+BAA+B,SAIlD;CAID,QAAO,MAHiB,2BAA2B,OAAO,EACxD,IAAI,QAAQ,GACd,CAAC,EAAA,CACgB,+BAA+B;EAC9C,QAAQ,QAAQ;EAChB,OAAO,QAAQ;CACjB,CAAC;AACH;AAGA,eAAsB,uBAAuB,SAI1C;CAID,MAAM,MAAM,OAAM,MAHM,2BAA2B,OAAO,EACxD,IAAI,QAAQ,GACd,CAAC,EAAA,CAC2B,IAAI,EAAE,IAAI,QAAQ,GAAG,CAAC;CAClD,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,qBAAqB,QAAQ,GAAE,YAAa;CAE9D,OAAO,IAAI,QAAQ,QAAQ,QAAQ;AACrC;AAGA,eAAsB,qBAAqB,SAKxC;CAID,MAAM,MAAM,OAAM,MAHM,2BAA2B,OAAO,EACxD,IAAI,QAAQ,GACd,CAAC,EAAA,CAC2B,IAAI,EAAE,IAAI,QAAQ,GAAG,CAAC;CAClD,IAAI,CAAC,KACH,MAAM,IAAI,MAAM,qBAAqB,QAAQ,GAAE,YAAa;CAE9D,IAAI,WAAW,QAAQ;CACvB,IAAI,iBAAiB;CACrB,IAAI,WAAW;CACf,IAAI,QAAQ,UAAU;EACpB,IAAI,+BAAc,IAAI,KAAK,EAAA,CAAE,YAAY;EACzC,IAAI,cAAc,QAAQ;CAC5B;CACA,MAAM,IAAI,KAAK;CACf,OAAO;AACT"}
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@ 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.51.21\",\"objects\":{\"@happyvertical/smrt-languages:LanguageTranslationTask\":{\"name\":\"languagetranslationtask\",\"className\":\"LanguageTranslationTask\",\"qualifiedName\":\"@happyvertical/smrt-languages:LanguageTranslationTask\",\"collection\":\"languagetranslationtasks\",\"filePath\":\"/home/runner/work/smrt/smrt/packages/languages/src/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\"},\"@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\"},\"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\\\" TEXT,\\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\":\"TEXT\",\"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\":\"4bef8abc\"},\"importPath\":\"@happyvertical/smrt-languages\"},\"@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\":{\"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\\\" TEXT,\\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\":\"TEXT\",\"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\":\"4bef8abc\"},\"importPath\":\"@happyvertical/smrt-languages\"}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-features\",\"@happyvertical/smrt-jobs\",\"@happyvertical/smrt-prompts\",\"@happyvertical/smrt-tenancy\"]}"));
|
|
6
|
+
ObjectRegistry.registerPackageManifest(JSON.parse("{\"version\":\"1.0.0\",\"timestamp\":0,\"packageName\":\"@happyvertical/smrt-languages\",\"packageVersion\":\"0.51.23\",\"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\"},\"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\\\" TEXT,\\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\":\"TEXT\",\"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\":\"4bef8abc\"},\"importPath\":\"@happyvertical/smrt-languages\"},\"@happyvertical/smrt-languages:LanguageTranslationTask\":{\"name\":\"languagetranslationtask\",\"className\":\"LanguageTranslationTask\",\"qualifiedName\":\"@happyvertical/smrt-languages:LanguageTranslationTask\",\"collection\":\"languagetranslationtasks\",\"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\":{\"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\\\" TEXT,\\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\":\"TEXT\",\"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\":\"4bef8abc\"},\"importPath\":\"@happyvertical/smrt-languages\"}},\"moduleType\":\"smrt\",\"smrtDependencies\":[\"@happyvertical/smrt-core\",\"@happyvertical/smrt-features\",\"@happyvertical/smrt-jobs\",\"@happyvertical/smrt-prompts\",\"@happyvertical/smrt-tenancy\"]}"));
|
|
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-
|
|
179
|
+
const { enqueueTranslationJob } = await import("./chunks/translation-job-CYJya3SL.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":["/**\n * Self-registers this package's build-time manifest before any @smrt()\n * decorator in the package fires. Same pattern as smrt-prompts — see\n * packages/prompts/src/__smrt-register__.ts and issue #1132 for context.\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\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 `./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('./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":["/**\n * Self-registers this package's build-time manifest before any @smrt()\n * decorator in the package fires. Same pattern as smrt-prompts — see\n * packages/prompts/src/__smrt-register__.ts and issue #1132 for context.\n */\nimport { ObjectRegistry } from '@happyvertical/smrt-core';\n\nObjectRegistry.registerPackageManifest(\n new URL('./manifest.json', import.meta.url),\n);\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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"}
|
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-
|
|
1
|
+
import { i as enqueueTranslationJob, n as LanguageTranslationTask, r as TRANSLATION_PROMPT_KEY, t as AUTO_TRANSLATE_FEATURE_KEY } from "./chunks/translation-job-CYJya3SL.js";
|
|
2
2
|
export { AUTO_TRANSLATE_FEATURE_KEY, LanguageTranslationTask, TRANSLATION_PROMPT_KEY, enqueueTranslationJob };
|
package/dist/manifest.json
CHANGED
|
@@ -2,96 +2,8 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-languages",
|
|
5
|
-
"packageVersion": "0.51.
|
|
5
|
+
"packageVersion": "0.51.23",
|
|
6
6
|
"objects": {
|
|
7
|
-
"@happyvertical/smrt-languages:LanguageTranslationTask": {
|
|
8
|
-
"name": "languagetranslationtask",
|
|
9
|
-
"className": "LanguageTranslationTask",
|
|
10
|
-
"qualifiedName": "@happyvertical/smrt-languages:LanguageTranslationTask",
|
|
11
|
-
"collection": "languagetranslationtasks",
|
|
12
|
-
"filePath": "/home/runner/work/smrt/smrt/packages/languages/src/translation-job.ts",
|
|
13
|
-
"packageName": "@happyvertical/smrt-languages",
|
|
14
|
-
"fields": {},
|
|
15
|
-
"methods": {
|
|
16
|
-
"execute": {
|
|
17
|
-
"name": "execute",
|
|
18
|
-
"async": true,
|
|
19
|
-
"parameters": [
|
|
20
|
-
{
|
|
21
|
-
"name": "args",
|
|
22
|
-
"type": "TranslationJobPayload",
|
|
23
|
-
"optional": false
|
|
24
|
-
}
|
|
25
|
-
],
|
|
26
|
-
"returnType": "Promise<object>",
|
|
27
|
-
"isStatic": false,
|
|
28
|
-
"isPublic": true
|
|
29
|
-
}
|
|
30
|
-
},
|
|
31
|
-
"decoratorConfig": {
|
|
32
|
-
"api": {
|
|
33
|
-
"include": []
|
|
34
|
-
},
|
|
35
|
-
"cli": {
|
|
36
|
-
"include": []
|
|
37
|
-
},
|
|
38
|
-
"mcp": {
|
|
39
|
-
"include": []
|
|
40
|
-
}
|
|
41
|
-
},
|
|
42
|
-
"extends": "SmrtObject",
|
|
43
|
-
"exportName": "LanguageTranslationTask",
|
|
44
|
-
"collectionExportName": "LanguageTranslationTaskCollection",
|
|
45
|
-
"schema": {
|
|
46
|
-
"tableName": "language_translation_tasks",
|
|
47
|
-
"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);",
|
|
48
|
-
"columns": {
|
|
49
|
-
"id": {
|
|
50
|
-
"type": "UUID",
|
|
51
|
-
"primaryKey": true,
|
|
52
|
-
"referenceKind": "id",
|
|
53
|
-
"notNull": true
|
|
54
|
-
},
|
|
55
|
-
"slug": {
|
|
56
|
-
"type": "TEXT",
|
|
57
|
-
"notNull": true
|
|
58
|
-
},
|
|
59
|
-
"context": {
|
|
60
|
-
"type": "TEXT",
|
|
61
|
-
"notNull": true,
|
|
62
|
-
"default": ""
|
|
63
|
-
},
|
|
64
|
-
"created_at": {
|
|
65
|
-
"type": "TIMESTAMP",
|
|
66
|
-
"notNull": true,
|
|
67
|
-
"default": "current_timestamp"
|
|
68
|
-
},
|
|
69
|
-
"updated_at": {
|
|
70
|
-
"type": "TIMESTAMP",
|
|
71
|
-
"notNull": true,
|
|
72
|
-
"default": "current_timestamp"
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
"indexes": [
|
|
76
|
-
{
|
|
77
|
-
"name": "language_translation_tasks_slug_context_idx",
|
|
78
|
-
"columns": [
|
|
79
|
-
"slug",
|
|
80
|
-
"context"
|
|
81
|
-
],
|
|
82
|
-
"unique": true
|
|
83
|
-
},
|
|
84
|
-
{
|
|
85
|
-
"name": "language_translation_tasks_created_at_idx",
|
|
86
|
-
"columns": [
|
|
87
|
-
"created_at"
|
|
88
|
-
]
|
|
89
|
-
}
|
|
90
|
-
],
|
|
91
|
-
"version": "2cfc8812"
|
|
92
|
-
},
|
|
93
|
-
"importPath": "@happyvertical/smrt-languages"
|
|
94
|
-
},
|
|
95
7
|
"@happyvertical/smrt-languages:LanguageOverrideCollection": {
|
|
96
8
|
"name": "languageoverridecollection",
|
|
97
9
|
"className": "LanguageOverrideCollection",
|
|
@@ -322,6 +234,94 @@
|
|
|
322
234
|
},
|
|
323
235
|
"importPath": "@happyvertical/smrt-languages"
|
|
324
236
|
},
|
|
237
|
+
"@happyvertical/smrt-languages:LanguageTranslationTask": {
|
|
238
|
+
"name": "languagetranslationtask",
|
|
239
|
+
"className": "LanguageTranslationTask",
|
|
240
|
+
"qualifiedName": "@happyvertical/smrt-languages:LanguageTranslationTask",
|
|
241
|
+
"collection": "languagetranslationtasks",
|
|
242
|
+
"filePath": "/home/runner/work/smrt/smrt/packages/languages/src/jobs/translation-job.ts",
|
|
243
|
+
"packageName": "@happyvertical/smrt-languages",
|
|
244
|
+
"fields": {},
|
|
245
|
+
"methods": {
|
|
246
|
+
"execute": {
|
|
247
|
+
"name": "execute",
|
|
248
|
+
"async": true,
|
|
249
|
+
"parameters": [
|
|
250
|
+
{
|
|
251
|
+
"name": "args",
|
|
252
|
+
"type": "TranslationJobPayload",
|
|
253
|
+
"optional": false
|
|
254
|
+
}
|
|
255
|
+
],
|
|
256
|
+
"returnType": "Promise<object>",
|
|
257
|
+
"isStatic": false,
|
|
258
|
+
"isPublic": true
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"decoratorConfig": {
|
|
262
|
+
"api": {
|
|
263
|
+
"include": []
|
|
264
|
+
},
|
|
265
|
+
"cli": {
|
|
266
|
+
"include": []
|
|
267
|
+
},
|
|
268
|
+
"mcp": {
|
|
269
|
+
"include": []
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
"extends": "SmrtObject",
|
|
273
|
+
"exportName": "LanguageTranslationTask",
|
|
274
|
+
"collectionExportName": "LanguageTranslationTaskCollection",
|
|
275
|
+
"schema": {
|
|
276
|
+
"tableName": "language_translation_tasks",
|
|
277
|
+
"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);",
|
|
278
|
+
"columns": {
|
|
279
|
+
"id": {
|
|
280
|
+
"type": "UUID",
|
|
281
|
+
"primaryKey": true,
|
|
282
|
+
"referenceKind": "id",
|
|
283
|
+
"notNull": true
|
|
284
|
+
},
|
|
285
|
+
"slug": {
|
|
286
|
+
"type": "TEXT",
|
|
287
|
+
"notNull": true
|
|
288
|
+
},
|
|
289
|
+
"context": {
|
|
290
|
+
"type": "TEXT",
|
|
291
|
+
"notNull": true,
|
|
292
|
+
"default": ""
|
|
293
|
+
},
|
|
294
|
+
"created_at": {
|
|
295
|
+
"type": "TIMESTAMP",
|
|
296
|
+
"notNull": true,
|
|
297
|
+
"default": "current_timestamp"
|
|
298
|
+
},
|
|
299
|
+
"updated_at": {
|
|
300
|
+
"type": "TIMESTAMP",
|
|
301
|
+
"notNull": true,
|
|
302
|
+
"default": "current_timestamp"
|
|
303
|
+
}
|
|
304
|
+
},
|
|
305
|
+
"indexes": [
|
|
306
|
+
{
|
|
307
|
+
"name": "language_translation_tasks_slug_context_idx",
|
|
308
|
+
"columns": [
|
|
309
|
+
"slug",
|
|
310
|
+
"context"
|
|
311
|
+
],
|
|
312
|
+
"unique": true
|
|
313
|
+
},
|
|
314
|
+
{
|
|
315
|
+
"name": "language_translation_tasks_created_at_idx",
|
|
316
|
+
"columns": [
|
|
317
|
+
"created_at"
|
|
318
|
+
]
|
|
319
|
+
}
|
|
320
|
+
],
|
|
321
|
+
"version": "2cfc8812"
|
|
322
|
+
},
|
|
323
|
+
"importPath": "@happyvertical/smrt-languages/jobs"
|
|
324
|
+
},
|
|
325
325
|
"@happyvertical/smrt-languages:LanguageOverride": {
|
|
326
326
|
"name": "languageoverride",
|
|
327
327
|
"className": "LanguageOverride",
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-languages",
|
|
6
|
-
"packageVersion": "0.51.
|
|
6
|
+
"packageVersion": "0.51.23",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "1e2467a73bede017058366875e254c1e8a1046e53cdf12b0e98ef6525aaa610a",
|
|
11
|
+
"packageJson": "ca75af54dabf903e6e70b4d295e0ba8b11887485ddd22ef16ef2c95577dbfc35",
|
|
12
|
+
"agents": "45d290839ba542a82a3cdf421d919a9dbc31f5280823ac432662bd678a087d7a"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
15
15
|
".",
|
|
@@ -63,44 +63,6 @@
|
|
|
63
63
|
"sensitive-fields-excluded"
|
|
64
64
|
],
|
|
65
65
|
"objects": [
|
|
66
|
-
{
|
|
67
|
-
"name": "LanguageTranslationTask",
|
|
68
|
-
"qualifiedName": "@happyvertical/smrt-languages:LanguageTranslationTask",
|
|
69
|
-
"collection": "languagetranslationtasks",
|
|
70
|
-
"tableName": "language_translation_tasks",
|
|
71
|
-
"packageName": "@happyvertical/smrt-languages",
|
|
72
|
-
"extends": "SmrtObject",
|
|
73
|
-
"fields": [],
|
|
74
|
-
"relationships": [],
|
|
75
|
-
"methods": [
|
|
76
|
-
"execute"
|
|
77
|
-
],
|
|
78
|
-
"methodSignatures": [
|
|
79
|
-
{
|
|
80
|
-
"name": "execute",
|
|
81
|
-
"async": true,
|
|
82
|
-
"params": [
|
|
83
|
-
"args: TranslationJobPayload"
|
|
84
|
-
],
|
|
85
|
-
"returns": "Promise<object>"
|
|
86
|
-
}
|
|
87
|
-
],
|
|
88
|
-
"surfaces": [],
|
|
89
|
-
"withheldSurfaces": [
|
|
90
|
-
{
|
|
91
|
-
"kind": "api",
|
|
92
|
-
"operation": "execute",
|
|
93
|
-
"code": "not-included",
|
|
94
|
-
"reason": "not listed in api.include",
|
|
95
|
-
"objectName": "@happyvertical/smrt-languages:LanguageTranslationTask"
|
|
96
|
-
}
|
|
97
|
-
],
|
|
98
|
-
"relationshipFeatures": [
|
|
99
|
-
"uuidColumns"
|
|
100
|
-
],
|
|
101
|
-
"tags": [],
|
|
102
|
-
"risks": []
|
|
103
|
-
},
|
|
104
66
|
{
|
|
105
67
|
"name": "LanguageOverrideCollection",
|
|
106
68
|
"qualifiedName": "@happyvertical/smrt-languages:LanguageOverrideCollection",
|
|
@@ -272,6 +234,44 @@
|
|
|
272
234
|
"tags": [],
|
|
273
235
|
"risks": []
|
|
274
236
|
},
|
|
237
|
+
{
|
|
238
|
+
"name": "LanguageTranslationTask",
|
|
239
|
+
"qualifiedName": "@happyvertical/smrt-languages:LanguageTranslationTask",
|
|
240
|
+
"collection": "languagetranslationtasks",
|
|
241
|
+
"tableName": "language_translation_tasks",
|
|
242
|
+
"packageName": "@happyvertical/smrt-languages",
|
|
243
|
+
"extends": "SmrtObject",
|
|
244
|
+
"fields": [],
|
|
245
|
+
"relationships": [],
|
|
246
|
+
"methods": [
|
|
247
|
+
"execute"
|
|
248
|
+
],
|
|
249
|
+
"methodSignatures": [
|
|
250
|
+
{
|
|
251
|
+
"name": "execute",
|
|
252
|
+
"async": true,
|
|
253
|
+
"params": [
|
|
254
|
+
"args: TranslationJobPayload"
|
|
255
|
+
],
|
|
256
|
+
"returns": "Promise<object>"
|
|
257
|
+
}
|
|
258
|
+
],
|
|
259
|
+
"surfaces": [],
|
|
260
|
+
"withheldSurfaces": [
|
|
261
|
+
{
|
|
262
|
+
"kind": "api",
|
|
263
|
+
"operation": "execute",
|
|
264
|
+
"code": "not-included",
|
|
265
|
+
"reason": "not listed in api.include",
|
|
266
|
+
"objectName": "@happyvertical/smrt-languages:LanguageTranslationTask"
|
|
267
|
+
}
|
|
268
|
+
],
|
|
269
|
+
"relationshipFeatures": [
|
|
270
|
+
"uuidColumns"
|
|
271
|
+
],
|
|
272
|
+
"tags": [],
|
|
273
|
+
"risks": []
|
|
274
|
+
},
|
|
275
275
|
{
|
|
276
276
|
"name": "LanguageOverride",
|
|
277
277
|
"qualifiedName": "@happyvertical/smrt-languages:LanguageOverride",
|
|
@@ -650,7 +650,7 @@
|
|
|
650
650
|
],
|
|
651
651
|
"prompts": [
|
|
652
652
|
{
|
|
653
|
-
"filePath": "src/translation-job.ts"
|
|
653
|
+
"filePath": "src/jobs/translation-job.ts"
|
|
654
654
|
}
|
|
655
655
|
],
|
|
656
656
|
"relationshipsV2": {
|
|
@@ -661,5 +661,5 @@
|
|
|
661
661
|
"polymorphicAssociations": 0,
|
|
662
662
|
"uuidColumns": 3
|
|
663
663
|
},
|
|
664
|
-
"agentDoc": "# languages\n\nSMRT language string registry with file/config + tenant overrides and AI-driven\nauto-translation for missing locales.\n\n## Core pieces\n\n- `defineLanguageString({ key, locale, template })` registers a code default in\n a global process registry. Same shape as `definePrompt` but keyed by\n `(key, locale)` rather than `key` alone.\n- `resolveLanguageString(key, options)` walks the 5-layer chain (code → file\n config → app override → tenant override → runtime override) and falls back\n through the locale chain (`fr-CA` → `fr` → default) before giving up.\n- `LanguageOverride` stores app-level and tenant-level overrides in\n `_smrt_language_overrides`. `auto_generated`, `source_hash`, `ai_model`,\n `reviewed_at`, `reviewed_by` fields support the AI auto-translation pipeline\n and admin review queue.\n- `enqueueTranslationJob({ key, targetLocale })` writes a `LanguageTranslationTask`\n job into the `languages` queue with a deterministic dedup ID so concurrent\n resolver misses collapse into one job.\n\n## Locale-miss flow\n\nWhen `resolveLanguageString` cannot find an exact `(key, locale, tenantId)`:\n\n1. Walk the locale fallback chain (`buildLocaleFallbackChain`).\n2. Return the first hit — same call returns immediately with\n `source: 'fallback'`.\n3. Fire-and-forget `enqueueTranslationJob` for the missing target, scoped to\n the current tenant for glossary purposes. App-level translations are\n reusable across tenants, so the resulting `LanguageOverride` row is written\n with `tenantId: null`.\n4. Subsequent requests hit the new app-level row and resolve at the requested\n locale.\n\nThe translation job:\n\n- Honors the `smrt-languages.auto_translate` feature flag (kill switch).\n- Skips locales outside `supportedLocales` when configured.\n- Skips when `LanguageOverride` already exists with a matching `source_hash`\n (re-translation is hash-gated, never time-based).\n- Never overwrites a row with `auto_generated: false` — human edits win\n permanently.\n- Pulls the tenant's existing overrides and renders them as a glossary so\n auto-translations match tenant voice.\n\n## Conventions\n\n- Keys are namespaced by package: `users.role.member`, `commerce.invoice.dueText`.\n- Locales follow BCP-47 (`en`, `fr-CA`, `pt-BR`) and are normalized to\n lowercase-language / uppercase-region on persistence.\n- The translation prompt itself is registered with `smrt-prompts` under\n `smrt-languages.translation` so ops can tune wording without redeploying.\n- `context` column on `_smrt_language_overrides` is set to `tenantId` or\n `'__app__'` so the `(key, locale, context)` upsert key remains unique even\n with nullable `tenantId`.\n\n## Caching\n\n`resolveLanguageString()` results are cached per `(db, key, locale, tenantId)`\nwith a TTL, invalidated on `LanguageOverride.save()` / `.delete()` (both\nidentities when one changes) and by the translation job. An app-level write\n(`tenantId = null`) clears every tenant's entry for that `(key, locale)`. Use\n`clearLanguageCache()` in tests.\n\n**A monotonic per-`(db, key, locale)` invalidation generation guards the cache\nwrite.** A resolution captures `getLanguageCacheGeneration(key, locale, db)`\nbefore its asynchronous layer loads and hands it back to `setCachedLanguage()`;\na concurrent write bumps the generation and the in-flight resolution is then\nrefused the cache write instead of repopulating the entry it just invalidated\nwith the pre-write value. Without it, a read that raced a write served the\npre-write value for the full 30s TTL, and a raced `delete()` resurrected the\ndeleted override. The generation includes `locale` — unlike `smrt-playbooks`\nand `smrt-prompts`, which have no locale dimension — because every layer a\ncached entry is built from is read at exactly the locale it is cached under:\nthe fallback chain (`fr-CA` → `fr` → `en`) resolves each locale independently\nand an attempt that resolves nothing caches nothing, so a fallback hit is never\nstored under the requested locale. It deliberately excludes `tenantId`, because\nan app-level row is inherited by every tenant. `clearLanguageCache()` raises a\nsingle floor (`clearedThrough`) rather than resetting or per-entry bumping: a\n`(key, locale)` that has never been invalidated has no map entry and reads as\ngeneration 0, so a per-entry bump cannot reach it and a resolution that started\nbefore the clear would write its pre-clear value back.\n\n## Public API surface\n\n```typescript\nimport {\n defineLanguageString,\n resolveLanguageString,\n LanguageOverride,\n LanguageOverrideCollection,\n clearLanguageCache,\n} from '@happyvertical/smrt-languages';\n\ndefineLanguageString({\n key: 'users.role.member',\n locale: 'en',\n template: 'Member',\n});\n\nconst text = await resolveLanguageString('users.role.member', {\n db,\n tenantId: 'tenant-a',\n locale: 'es',\n vars: { name: 'Will' },\n});\n```\n"
|
|
664
|
+
"agentDoc": "# languages\n\nSMRT language string registry with file/config + tenant overrides and AI-driven\nauto-translation for missing locales.\n\n## Core pieces\n\n- `defineLanguageString({ key, locale, template })` registers a code default in\n a global process registry. Same shape as `definePrompt` but keyed by\n `(key, locale)` rather than `key` alone.\n- `resolveLanguageString(key, options)` walks the 5-layer chain (code → file\n config → app override → tenant override → runtime override) and falls back\n through the locale chain (`fr-CA` → `fr` → default) before giving up.\n- `LanguageOverride` stores app-level and tenant-level overrides in\n `_smrt_language_overrides`. `auto_generated`, `source_hash`, `ai_model`,\n `reviewed_at`, `reviewed_by` fields support the AI auto-translation pipeline\n and admin review queue.\n- `enqueueTranslationJob({ key, targetLocale })` writes a `LanguageTranslationTask`\n job into the `languages` queue with a deterministic dedup ID so concurrent\n resolver misses collapse into one job.\n- Keep `LanguageTranslationTask` under `src/jobs/`: the manifest generator uses\n that path to stamp its `@happyvertical/smrt-languages/jobs` import path. The\n package root intentionally excludes the translation-worker dependency tree.\n\n## Locale-miss flow\n\nWhen `resolveLanguageString` cannot find an exact `(key, locale, tenantId)`:\n\n1. Walk the locale fallback chain (`buildLocaleFallbackChain`).\n2. Return the first hit — same call returns immediately with\n `source: 'fallback'`.\n3. Fire-and-forget `enqueueTranslationJob` for the missing target, scoped to\n the current tenant for glossary purposes. App-level translations are\n reusable across tenants, so the resulting `LanguageOverride` row is written\n with `tenantId: null`.\n4. Subsequent requests hit the new app-level row and resolve at the requested\n locale.\n\nThe translation job:\n\n- Honors the `smrt-languages.auto_translate` feature flag (kill switch).\n- Skips locales outside `supportedLocales` when configured.\n- Skips when `LanguageOverride` already exists with a matching `source_hash`\n (re-translation is hash-gated, never time-based).\n- Never overwrites a row with `auto_generated: false` — human edits win\n permanently.\n- Pulls the tenant's existing overrides and renders them as a glossary so\n auto-translations match tenant voice.\n\n## Conventions\n\n- Keys are namespaced by package: `users.role.member`, `commerce.invoice.dueText`.\n- Locales follow BCP-47 (`en`, `fr-CA`, `pt-BR`) and are normalized to\n lowercase-language / uppercase-region on persistence.\n- The translation prompt itself is registered with `smrt-prompts` under\n `smrt-languages.translation` so ops can tune wording without redeploying.\n- `context` column on `_smrt_language_overrides` is set to `tenantId` or\n `'__app__'` so the `(key, locale, context)` upsert key remains unique even\n with nullable `tenantId`.\n\n## Caching\n\n`resolveLanguageString()` results are cached per `(db, key, locale, tenantId)`\nwith a TTL, invalidated on `LanguageOverride.save()` / `.delete()` (both\nidentities when one changes) and by the translation job. An app-level write\n(`tenantId = null`) clears every tenant's entry for that `(key, locale)`. Use\n`clearLanguageCache()` in tests.\n\n**A monotonic per-`(db, key, locale)` invalidation generation guards the cache\nwrite.** A resolution captures `getLanguageCacheGeneration(key, locale, db)`\nbefore its asynchronous layer loads and hands it back to `setCachedLanguage()`;\na concurrent write bumps the generation and the in-flight resolution is then\nrefused the cache write instead of repopulating the entry it just invalidated\nwith the pre-write value. Without it, a read that raced a write served the\npre-write value for the full 30s TTL, and a raced `delete()` resurrected the\ndeleted override. The generation includes `locale` — unlike `smrt-playbooks`\nand `smrt-prompts`, which have no locale dimension — because every layer a\ncached entry is built from is read at exactly the locale it is cached under:\nthe fallback chain (`fr-CA` → `fr` → `en`) resolves each locale independently\nand an attempt that resolves nothing caches nothing, so a fallback hit is never\nstored under the requested locale. It deliberately excludes `tenantId`, because\nan app-level row is inherited by every tenant. `clearLanguageCache()` raises a\nsingle floor (`clearedThrough`) rather than resetting or per-entry bumping: a\n`(key, locale)` that has never been invalidated has no map entry and reads as\ngeneration 0, so a per-entry bump cannot reach it and a resolution that started\nbefore the clear would write its pre-clear value back.\n\n## Public API surface\n\n```typescript\nimport {\n defineLanguageString,\n resolveLanguageString,\n LanguageOverride,\n LanguageOverrideCollection,\n clearLanguageCache,\n} from '@happyvertical/smrt-languages';\n\ndefineLanguageString({\n key: 'users.role.member',\n locale: 'en',\n template: 'Member',\n});\n\nconst text = await resolveLanguageString('users.role.member', {\n db,\n tenantId: 'tenant-a',\n locale: 'es',\n vars: { name: 'Will' },\n});\n```\n"
|
|
665
665
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-languages",
|
|
3
|
-
"version": "0.51.
|
|
3
|
+
"version": "0.51.23",
|
|
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",
|
|
@@ -27,19 +27,19 @@
|
|
|
27
27
|
"./manifest.json": "./dist/manifest.json"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@happyvertical/ai": "^0.
|
|
31
|
-
"@happyvertical/logger": "^0.
|
|
32
|
-
"@happyvertical/smrt-config": "0.51.
|
|
33
|
-
"@happyvertical/smrt-core": "0.51.
|
|
34
|
-
"@happyvertical/smrt-features": "0.51.
|
|
35
|
-
"@happyvertical/smrt-jobs": "0.51.
|
|
36
|
-
"@happyvertical/smrt-prompts": "0.51.
|
|
37
|
-
"@happyvertical/smrt-tenancy": "0.51.
|
|
38
|
-
"@happyvertical/sql": "^0.
|
|
30
|
+
"@happyvertical/ai": "^0.90.0",
|
|
31
|
+
"@happyvertical/logger": "^0.90.0",
|
|
32
|
+
"@happyvertical/smrt-config": "0.51.23",
|
|
33
|
+
"@happyvertical/smrt-core": "0.51.23",
|
|
34
|
+
"@happyvertical/smrt-features": "0.51.23",
|
|
35
|
+
"@happyvertical/smrt-jobs": "0.51.23",
|
|
36
|
+
"@happyvertical/smrt-prompts": "0.51.23",
|
|
37
|
+
"@happyvertical/smrt-tenancy": "0.51.23",
|
|
38
|
+
"@happyvertical/sql": "^0.90.0"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
|
-
"@happyvertical/smrt-cli": "0.51.
|
|
42
|
-
"@happyvertical/smrt-vitest": "0.51.
|
|
41
|
+
"@happyvertical/smrt-cli": "0.51.23",
|
|
42
|
+
"@happyvertical/smrt-vitest": "0.51.23",
|
|
43
43
|
"@types/node": "24.13.6",
|
|
44
44
|
"typescript": "5.9.3",
|
|
45
45
|
"vite": "8.1.4",
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"translation-job-C4YLXz8M.js","names":[],"sources":["../../src/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 { LanguagesPackageConfig, TranslationJobPayload } 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 {\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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"}
|