@happyvertical/smrt-languages 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,247 @@
1
+ import { DatabaseInterface } from '@happyvertical/sql';
2
+ import { SmrtClassOptions } from '@happyvertical/smrt-core';
3
+ import { SmrtCollection } from '@happyvertical/smrt-core';
4
+ import { SmrtObject } from '@happyvertical/smrt-core';
5
+ import { SmrtObjectOptions } from '@happyvertical/smrt-core';
6
+
7
+ /**
8
+ * Build the locale fallback chain for a requested locale.
9
+ *
10
+ * Example: `fr-CA` with default `en` → `['fr-CA', 'fr', 'en']`.
11
+ * Duplicates and empty segments are removed.
12
+ */
13
+ export declare function buildLocaleFallbackChain(requested: string, defaultLocale: string): string[];
14
+
15
+ /**
16
+ * Build a glossary string suitable for inclusion in an AI translation prompt.
17
+ *
18
+ * Pulls the tenant's existing language overrides for the source/target locales
19
+ * and renders them as a concise list of "source → target" pairs, restricted to
20
+ * keys that actually have both sides. The intent is "make AI auto-translations
21
+ * match tenant voice", not bulk dump every override.
22
+ *
23
+ * Returns an empty string when there is nothing useful to add.
24
+ */
25
+ export declare function buildTenantGlossary(overrides: LanguageOverride[], options: {
26
+ sourceLocale: string;
27
+ targetLocale: string;
28
+ max?: number;
29
+ }): string;
30
+
31
+ /** Deterministic translation-job ID — used for stampede protection. */
32
+ export declare function buildTranslationJobId(key: string, targetLocale: string): string;
33
+
34
+ export declare function clearLanguageCache(): void;
35
+
36
+ /** Stable hash used to gate auto-translation re-runs against a specific source. */
37
+ export declare function computeSourceHash(template: string): string;
38
+
39
+ export declare function defineLanguageString(input: LanguageStringDefinitionInput): LanguageStringDefinition;
40
+
41
+ export declare function getLanguageCacheTtlMs(): number;
42
+
43
+ /**
44
+ * Drop cached entries that match the given (key, locale).
45
+ *
46
+ * - When `tenantId` is provided, only that tenant's entries for this DB are dropped.
47
+ * - When `tenantId` is null/undefined, every (key, locale, *) entry across tenants
48
+ * is dropped — used for app-level writes that affect all tenants.
49
+ */
50
+ export declare function invalidateLanguageCache(key: string, locale: string, tenantId: string | null | undefined, db: DatabaseInterface | unknown): void;
51
+
52
+ export declare function invalidateResolvedLanguageCache(key: string, locale: string, tenantId: string | null | undefined, db: unknown): void;
53
+
54
+ export declare interface LanguageCacheValue {
55
+ key: string;
56
+ locale: string;
57
+ template: string;
58
+ source: ResolvedLanguageString['source'];
59
+ resolvedFromLocale: string;
60
+ }
61
+
62
+ export declare class LanguageOverride extends SmrtObject {
63
+ key: string;
64
+ locale: string;
65
+ tenantId: string | null;
66
+ template: string;
67
+ /** True when this row was produced by the AI translation job. */
68
+ auto_generated: boolean;
69
+ /** sha256 of the source template at translation time, for re-translation gating. */
70
+ source_hash: string | null;
71
+ /** AI model identifier (null for human-edited rows). */
72
+ ai_model: string | null;
73
+ /** ISO timestamp marking admin review of an auto-generated row. */
74
+ reviewed_at: string | null;
75
+ /** User ID of the reviewer. */
76
+ reviewed_by: string | null;
77
+ constructor(options?: LanguageOverrideCtorOptions);
78
+ save(): Promise<this>;
79
+ private saveAfterIdentityChange;
80
+ private saveAfterIdentityChangeInTransaction;
81
+ private saveAfterIdentityChangeWithDeferredDelete;
82
+ delete(): Promise<void>;
83
+ /**
84
+ * Mark this auto-generated row as reviewed by an admin. Useful for the
85
+ * admin review queue surfaced via `smrt languages approve <id>`.
86
+ */
87
+ approve(reviewerId: string): Promise<this>;
88
+ private getPersistedIdentity;
89
+ }
90
+
91
+ export declare class LanguageOverrideCollection extends SmrtCollection<LanguageOverride> {
92
+ static readonly _itemClass: typeof LanguageOverride;
93
+ /**
94
+ * Look up the app-level (tenantId = null) override for a (key, locale) pair.
95
+ * App-level rows hold AI auto-translations and ops-curated app-wide strings.
96
+ */
97
+ getAppOverride(key: string, locale: string): Promise<LanguageOverride | null>;
98
+ getTenantOverride(key: string, locale: string, tenantId: string): Promise<LanguageOverride | null>;
99
+ /**
100
+ * Convenience helper for the resolver: returns the app and (optional) tenant
101
+ * override rows for a (key, locale).
102
+ */
103
+ getResolutionLayers(key: string, locale: string, tenantId?: string | null): Promise<{
104
+ app: LanguageOverride | null;
105
+ tenant: LanguageOverride | null;
106
+ }>;
107
+ /** All overrides for a tenant (used to build the AI translation glossary). */
108
+ listTenantOverrides(tenantId: string): Promise<LanguageOverride[]>;
109
+ /** Auto-generated overrides that no admin has approved yet. */
110
+ listUnreviewedAutoTranslations(options?: {
111
+ locale?: string;
112
+ limit?: number;
113
+ }): Promise<LanguageOverride[]>;
114
+ }
115
+
116
+ export declare interface LanguageOverrideCtorOptions extends SmrtObjectOptions, LanguageOverrideOptions {
117
+ }
118
+
119
+ export declare interface LanguageOverrideOptions {
120
+ key?: string;
121
+ locale?: string;
122
+ tenantId?: string | null;
123
+ template?: string;
124
+ auto_generated?: boolean;
125
+ source_hash?: string | null;
126
+ ai_model?: string | null;
127
+ reviewed_at?: string | null;
128
+ reviewed_by?: string | null;
129
+ }
130
+
131
+ export declare const LanguageRegistry: {
132
+ register(input: LanguageStringDefinitionInput): LanguageStringDefinition;
133
+ get(key: string, locale: string): LanguageStringDefinition | undefined;
134
+ has(key: string, locale: string): boolean;
135
+ hasKey(key: string): boolean;
136
+ /** Return the locales that have a registered code default for the given key. */
137
+ getLocalesForKey(key: string): string[];
138
+ /** All registered (key, locale, template) triples. Used by the batch translator. */
139
+ getAll(): LanguageStringDefinition[];
140
+ /** Distinct keys currently registered. */
141
+ getKeys(): string[];
142
+ clear(): void;
143
+ };
144
+
145
+ /**
146
+ * Per-package config bag, read via `getPackageConfig('languages', ...)`.
147
+ *
148
+ * `overrides[key][locale]` is a plain string template that takes precedence over
149
+ * code defaults for that (key, locale) pair. It is the file-config layer in the
150
+ * 5-layer resolution chain.
151
+ */
152
+ export declare interface LanguagesPackageConfig {
153
+ /** Default locale used when none is supplied at resolve-time. Defaults to 'en'. */
154
+ defaultLocale?: string;
155
+ /** When set, AI auto-translation jobs are only enqueued for these locales. */
156
+ supportedLocales?: string[];
157
+ /** Daily cap on AI translation jobs per tenant (null = no cap). */
158
+ translationBudgetPerTenantPerDay?: number | null;
159
+ /** File-config overrides, keyed by (key)(locale). */
160
+ overrides?: Record<string, Record<string, string>>;
161
+ [key: string]: unknown;
162
+ }
163
+
164
+ export declare interface LanguageStringDefinition {
165
+ key: string;
166
+ locale: string;
167
+ template: string;
168
+ /** sha256 of the template at registration time, used for re-translation gating */
169
+ sourceHash: string;
170
+ }
171
+
172
+ /** A registered code default for a single (key, locale) pair. */
173
+ export declare interface LanguageStringDefinitionInput {
174
+ key: string;
175
+ locale: string;
176
+ template: string;
177
+ }
178
+
179
+ export declare type LanguageVariables = Record<string, unknown>;
180
+
181
+ /**
182
+ * Normalize a BCP-47-ish locale tag for consistent matching across the
183
+ * registry, the cache, and DB lookups.
184
+ *
185
+ * Lowercases the language subtag and uppercases everything after it so
186
+ * `fr-ca`, `fr-CA`, and `Fr-Ca` collapse to the same key. This is **not**
187
+ * canonical BCP-47 casing (proper BCP-47 lower-cases variants and uses
188
+ * Title-case for script subtags like `Hans`); it is an internal
189
+ * normalization shared by every layer in this package and intentionally
190
+ * stricter than what BCP-47 mandates.
191
+ */
192
+ export declare function normalizeLocale(locale: string): string;
193
+
194
+ /* Excluded from this release type: PACKAGE_VERSION_INITIALIZED */
195
+
196
+ /**
197
+ * Render `{var}` placeholders against the supplied variables.
198
+ *
199
+ * When at least one variable is provided, every `{...}` placeholder in the
200
+ * template is replaced; missing or `undefined`/`null` variables collapse to
201
+ * empty strings (callers that pass `name` but not `title` get the `{title}`
202
+ * placeholder removed).
203
+ *
204
+ * When `variables` is `undefined` or `{}` the template is returned unchanged
205
+ * — that fast path avoids the regex pass for the common "no interpolation
206
+ * needed" case. If you need every placeholder collapsed regardless, pass at
207
+ * least one variable (e.g. `{ __forceRender: true }`).
208
+ */
209
+ export declare function renderTemplate(template: string, variables?: LanguageVariables): string;
210
+
211
+ export declare interface ResolvedLanguageString {
212
+ key: string;
213
+ locale: string;
214
+ /** The fully-rendered string with variables substituted. */
215
+ text: string;
216
+ /** The raw template that produced `text`. */
217
+ template: string;
218
+ /** Locale that actually produced the template (may differ from `locale` after fallback). */
219
+ resolvedFromLocale: string;
220
+ /**
221
+ * `'code' | 'config' | 'app' | 'tenant' | 'runtime'`. `'fallback'` indicates
222
+ * no exact match was found and a fallback locale was used.
223
+ */
224
+ source: 'code' | 'config' | 'app' | 'tenant' | 'runtime' | 'fallback' | 'missing';
225
+ }
226
+
227
+ export declare function resolveLanguageString(key: string, options?: ResolveLanguageStringOptions): Promise<ResolvedLanguageString>;
228
+
229
+ export declare interface ResolveLanguageStringOptions {
230
+ db?: SmrtClassOptions['db'];
231
+ tenantId?: string | null;
232
+ locale?: string;
233
+ /** Variables substituted into `{var}` placeholders. */
234
+ vars?: LanguageVariables;
235
+ /** Optional per-call override for the rendered template (highest precedence). */
236
+ overrides?: {
237
+ template?: string;
238
+ };
239
+ /**
240
+ * When `true`, throw if the resolution chain finishes without producing a
241
+ * template. When `false` (default), return the key as the rendered string and
242
+ * enqueue an AI translation job for the missing (key, locale).
243
+ */
244
+ strict?: boolean;
245
+ }
246
+
247
+ export { }
package/dist/index.js ADDED
@@ -0,0 +1,242 @@
1
+ import { ObjectRegistry } from "@happyvertical/smrt-core";
2
+ import { i as invalidateLanguageCache, n as normalizeLocale, r as renderTemplate, e as buildLocaleFallbackChain, L as LanguageOverrideCollection, g as getCachedLanguage, a as LanguageRegistry, s as setCachedLanguage } from "./chunks/language-registry-CgsuwQo6.js";
3
+ import { f, b, d, h, c, j, k } from "./chunks/language-registry-CgsuwQo6.js";
4
+ import { getPackageConfig } from "@happyvertical/smrt-config";
5
+ import { getTenantId } from "@happyvertical/smrt-tenancy";
6
+ ObjectRegistry.registerPackageManifest(
7
+ new URL("./manifest.json", import.meta.url)
8
+ );
9
+ const DEFAULT_LOCALE = "en";
10
+ function getLanguagesConfig() {
11
+ return getPackageConfig("languages", {
12
+ defaultLocale: DEFAULT_LOCALE,
13
+ overrides: {}
14
+ });
15
+ }
16
+ async function resolveBaseForLocale(key, locale, options, config) {
17
+ const tenantId = options.tenantId !== void 0 ? options.tenantId : getTenantId() ?? null;
18
+ let collection = null;
19
+ if (options.db) {
20
+ collection = await LanguageOverrideCollection.create({
21
+ db: options.db
22
+ });
23
+ }
24
+ const cacheDb = collection?.db ?? options.db;
25
+ const cached = getCachedLanguage(key, locale, tenantId, cacheDb);
26
+ if (cached) {
27
+ return {
28
+ template: cached.template,
29
+ source: cached.source,
30
+ resolvedFromLocale: cached.resolvedFromLocale
31
+ };
32
+ }
33
+ if (collection && tenantId) {
34
+ const tenantOverride = await collection.getTenantOverride(
35
+ key,
36
+ locale,
37
+ tenantId
38
+ );
39
+ if (tenantOverride) {
40
+ return finalize({
41
+ key,
42
+ locale,
43
+ tenantId,
44
+ db: cacheDb,
45
+ attempt: {
46
+ template: tenantOverride.template,
47
+ source: "tenant",
48
+ resolvedFromLocale: locale
49
+ }
50
+ });
51
+ }
52
+ }
53
+ if (collection) {
54
+ const appOverride = await collection.getAppOverride(key, locale);
55
+ if (appOverride) {
56
+ return finalize({
57
+ key,
58
+ locale,
59
+ tenantId,
60
+ db: cacheDb,
61
+ attempt: {
62
+ template: appOverride.template,
63
+ source: "app",
64
+ resolvedFromLocale: locale
65
+ }
66
+ });
67
+ }
68
+ }
69
+ const configForKey = config.overrides?.[key];
70
+ const configTemplate = lookupNormalizedLocale(configForKey, locale);
71
+ if (typeof configTemplate === "string") {
72
+ return finalize({
73
+ key,
74
+ locale,
75
+ tenantId,
76
+ db: cacheDb,
77
+ attempt: {
78
+ template: configTemplate,
79
+ source: "config",
80
+ resolvedFromLocale: locale
81
+ }
82
+ });
83
+ }
84
+ const definition = LanguageRegistry.get(key, locale);
85
+ if (definition) {
86
+ return finalize({
87
+ key,
88
+ locale,
89
+ tenantId,
90
+ db: cacheDb,
91
+ attempt: {
92
+ template: definition.template,
93
+ source: "code",
94
+ resolvedFromLocale: locale
95
+ }
96
+ });
97
+ }
98
+ return {
99
+ template: null,
100
+ source: "missing",
101
+ resolvedFromLocale: locale
102
+ };
103
+ }
104
+ function lookupNormalizedLocale(bag, locale) {
105
+ if (!bag) return void 0;
106
+ if (typeof bag[locale] === "string") return bag[locale];
107
+ for (const [candidate, template] of Object.entries(bag)) {
108
+ if (normalizeLocale(candidate) === locale) {
109
+ return template;
110
+ }
111
+ }
112
+ return void 0;
113
+ }
114
+ function finalize(args) {
115
+ if (args.attempt.template !== null) {
116
+ const value = {
117
+ key: args.key,
118
+ locale: args.locale,
119
+ template: args.attempt.template,
120
+ source: args.attempt.source,
121
+ resolvedFromLocale: args.attempt.resolvedFromLocale
122
+ };
123
+ setCachedLanguage(args.key, args.locale, args.tenantId, args.db, value);
124
+ }
125
+ return args.attempt;
126
+ }
127
+ async function resolveLanguageString(key, options = {}) {
128
+ if (!key || key.trim() === "") {
129
+ throw new Error("resolveLanguageString requires a non-empty key");
130
+ }
131
+ const config = getLanguagesConfig();
132
+ const defaultLocale = normalizeLocale(config.defaultLocale ?? DEFAULT_LOCALE);
133
+ const requested = normalizeLocale(options.locale ?? defaultLocale);
134
+ if (typeof options.overrides?.template === "string") {
135
+ return {
136
+ key,
137
+ locale: requested,
138
+ template: options.overrides.template,
139
+ text: renderTemplate(options.overrides.template, options.vars),
140
+ resolvedFromLocale: requested,
141
+ source: "runtime"
142
+ };
143
+ }
144
+ const chain = buildLocaleFallbackChain(requested, defaultLocale);
145
+ let firstHit = null;
146
+ let firstLocale = requested;
147
+ for (const locale of chain) {
148
+ const attempt = await resolveBaseForLocale(key, locale, options, config);
149
+ if (attempt.template !== null) {
150
+ firstHit = attempt;
151
+ firstLocale = locale;
152
+ break;
153
+ }
154
+ }
155
+ if (!firstHit) {
156
+ if (options.strict) {
157
+ throw new Error(
158
+ `Language string "${key}" has no resolution for locale "${requested}"`
159
+ );
160
+ }
161
+ await tryEnqueueTranslation({
162
+ key,
163
+ requestedLocale: requested,
164
+ defaultLocale,
165
+ options
166
+ });
167
+ return {
168
+ key,
169
+ locale: requested,
170
+ template: key,
171
+ text: key,
172
+ resolvedFromLocale: requested,
173
+ source: "missing"
174
+ };
175
+ }
176
+ if (firstLocale !== requested) {
177
+ await tryEnqueueTranslation({
178
+ key,
179
+ requestedLocale: requested,
180
+ defaultLocale,
181
+ options
182
+ });
183
+ return {
184
+ key,
185
+ locale: requested,
186
+ template: firstHit.template ?? key,
187
+ text: renderTemplate(firstHit.template ?? key, options.vars),
188
+ resolvedFromLocale: firstLocale,
189
+ source: "fallback"
190
+ };
191
+ }
192
+ return {
193
+ key,
194
+ locale: requested,
195
+ template: firstHit.template ?? key,
196
+ text: renderTemplate(firstHit.template ?? key, options.vars),
197
+ resolvedFromLocale: firstLocale,
198
+ source: firstHit.source
199
+ };
200
+ }
201
+ async function tryEnqueueTranslation(args) {
202
+ if (!args.options.db) {
203
+ return;
204
+ }
205
+ if (normalizeLocale(args.requestedLocale) === args.defaultLocale) {
206
+ return;
207
+ }
208
+ try {
209
+ const { enqueueTranslationJob } = await import("./chunks/translation-job-DHg2E-eH.js");
210
+ await enqueueTranslationJob({
211
+ key: args.key,
212
+ targetLocale: args.requestedLocale,
213
+ sourceLocale: args.defaultLocale,
214
+ tenantId: args.options.tenantId ?? getTenantId() ?? null,
215
+ db: args.options.db
216
+ });
217
+ } catch {
218
+ }
219
+ }
220
+ function invalidateResolvedLanguageCache(key, locale, tenantId, db) {
221
+ invalidateLanguageCache(key, locale, tenantId, db);
222
+ }
223
+ const PACKAGE_VERSION_INITIALIZED = true;
224
+ export {
225
+ f as LanguageOverride,
226
+ LanguageOverrideCollection,
227
+ LanguageRegistry,
228
+ PACKAGE_VERSION_INITIALIZED,
229
+ buildLocaleFallbackChain,
230
+ b as buildTenantGlossary,
231
+ d as buildTranslationJobId,
232
+ h as clearLanguageCache,
233
+ c as computeSourceHash,
234
+ j as defineLanguageString,
235
+ k as getLanguageCacheTtlMs,
236
+ invalidateLanguageCache,
237
+ invalidateResolvedLanguageCache,
238
+ normalizeLocale,
239
+ renderTemplate,
240
+ resolveLanguageString
241
+ };
242
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","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 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 as any).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 // 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 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 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 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 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}): 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(args.key, args.locale, args.tenantId, args.db, value);\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"],"names":[],"mappings":";;;;;AAOA,eAAe;AAAA,EACb,IAAA,IAAA,mBAAA,YAAA,GAAA;AACF;ACYA,MAAM,iBAAiB;AAEvB,SAAS,qBAA6C;AACpD,SAAO,iBAAyC,aAAa;AAAA,IAC3D,eAAe;AAAA,IACf,WAAW,CAAA;AAAA,EAAC,CACb;AACH;AAQA,eAAe,qBACb,KACA,QACA,SACA,QAC4B;AAC5B,QAAM,WACJ,QAAQ,aAAa,SAAY,QAAQ,WAAY,iBAAiB;AAExE,MAAI,aAAgD;AACpD,MAAI,QAAQ,IAAI;AACd,iBAAa,MAAO,2BAAmC,OAAO;AAAA,MAC5D,IAAI,QAAQ;AAAA,IAAA,CACb;AAAA,EACH;AAEA,QAAM,UAAU,YAAY,MAAM,QAAQ;AAC1C,QAAM,SAAS,kBAAkB,KAAK,QAAQ,UAAU,OAAO;AAC/D,MAAI,QAAQ;AACV,WAAO;AAAA,MACL,UAAU,OAAO;AAAA,MACjB,QAAQ,OAAO;AAAA,MACf,oBAAoB,OAAO;AAAA,IAAA;AAAA,EAE/B;AAGA,MAAI,cAAc,UAAU;AAC1B,UAAM,iBAAiB,MAAM,WAAW;AAAA,MACtC;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAEF,QAAI,gBAAgB;AAClB,aAAO,SAAS;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,QACA,IAAI;AAAA,QACJ,SAAS;AAAA,UACP,UAAU,eAAe;AAAA,UACzB,QAAQ;AAAA,UACR,oBAAoB;AAAA,QAAA;AAAA,MACtB,CACD;AAAA,IACH;AAAA,EACF;AAGA,MAAI,YAAY;AACd,UAAM,cAAc,MAAM,WAAW,eAAe,KAAK,MAAM;AAC/D,QAAI,aAAa;AACf,aAAO,SAAS;AAAA,QACd;AAAA,QACA;AAAA,QACA;AAAA,QACA,IAAI;AAAA,QACJ,SAAS;AAAA,UACP,UAAU,YAAY;AAAA,UACtB,QAAQ;AAAA,UACR,oBAAoB;AAAA,QAAA;AAAA,MACtB,CACD;AAAA,IACH;AAAA,EACF;AAKA,QAAM,eAAe,OAAO,YAAY,GAAG;AAC3C,QAAM,iBAAiB,uBAAuB,cAAc,MAAM;AAClE,MAAI,OAAO,mBAAmB,UAAU;AACtC,WAAO,SAAS;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,IAAI;AAAA,MACJ,SAAS;AAAA,QACP,UAAU;AAAA,QACV,QAAQ;AAAA,QACR,oBAAoB;AAAA,MAAA;AAAA,IACtB,CACD;AAAA,EACH;AAGA,QAAM,aAAa,iBAAiB,IAAI,KAAK,MAAM;AACnD,MAAI,YAAY;AACd,WAAO,SAAS;AAAA,MACd;AAAA,MACA;AAAA,MACA;AAAA,MACA,IAAI;AAAA,MACJ,SAAS;AAAA,QACP,UAAU,WAAW;AAAA,QACrB,QAAQ;AAAA,QACR,oBAAoB;AAAA,MAAA;AAAA,IACtB,CACD;AAAA,EACH;AAEA,SAAO;AAAA,IACL,UAAU;AAAA,IACV,QAAQ;AAAA,IACR,oBAAoB;AAAA,EAAA;AAExB;AAEA,SAAS,uBACP,KACA,QACoB;AACpB,MAAI,CAAC,IAAK,QAAO;AAEjB,MAAI,OAAO,IAAI,MAAM,MAAM,SAAU,QAAO,IAAI,MAAM;AACtD,aAAW,CAAC,WAAW,QAAQ,KAAK,OAAO,QAAQ,GAAG,GAAG;AACvD,QAAI,gBAAgB,SAAS,MAAM,QAAQ;AACzC,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,SAAS,MAMI;AACpB,MAAI,KAAK,QAAQ,aAAa,MAAM;AAClC,UAAM,QAA4B;AAAA,MAChC,KAAK,KAAK;AAAA,MACV,QAAQ,KAAK;AAAA,MACb,UAAU,KAAK,QAAQ;AAAA,MACvB,QAAQ,KAAK,QAAQ;AAAA,MACrB,oBAAoB,KAAK,QAAQ;AAAA,IAAA;AAEnC,sBAAkB,KAAK,KAAK,KAAK,QAAQ,KAAK,UAAU,KAAK,IAAI,KAAK;AAAA,EACxE;AACA,SAAO,KAAK;AACd;AAEA,eAAsB,sBACpB,KACA,UAAwC,IACP;AACjC,MAAI,CAAC,OAAO,IAAI,KAAA,MAAW,IAAI;AAC7B,UAAM,IAAI,MAAM,gDAAgD;AAAA,EAClE;AAEA,QAAM,SAAS,mBAAA;AACf,QAAM,gBAAgB,gBAAgB,OAAO,iBAAiB,cAAc;AAC5E,QAAM,YAAY,gBAAgB,QAAQ,UAAU,aAAa;AAGjE,MAAI,OAAO,QAAQ,WAAW,aAAa,UAAU;AACnD,WAAO;AAAA,MACL;AAAA,MACA,QAAQ;AAAA,MACR,UAAU,QAAQ,UAAU;AAAA,MAC5B,MAAM,eAAe,QAAQ,UAAU,UAAU,QAAQ,IAAI;AAAA,MAC7D,oBAAoB;AAAA,MACpB,QAAQ;AAAA,IAAA;AAAA,EAEZ;AAEA,QAAM,QAAQ,yBAAyB,WAAW,aAAa;AAE/D,MAAI,WAAqC;AACzC,MAAI,cAAc;AAClB,aAAW,UAAU,OAAO;AAC1B,UAAM,UAAU,MAAM,qBAAqB,KAAK,QAAQ,SAAS,MAAM;AACvE,QAAI,QAAQ,aAAa,MAAM;AAC7B,iBAAW;AACX,oBAAc;AACd;AAAA,IACF;AAAA,EACF;AAEA,MAAI,CAAC,UAAU;AACb,QAAI,QAAQ,QAAQ;AAClB,YAAM,IAAI;AAAA,QACR,oBAAoB,GAAG,mCAAmC,SAAS;AAAA,MAAA;AAAA,IAEvE;AAKA,UAAM,sBAAsB;AAAA,MAC1B;AAAA,MACA,iBAAiB;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACD;AACD,WAAO;AAAA,MACL;AAAA,MACA,QAAQ;AAAA,MACR,UAAU;AAAA,MACV,MAAM;AAAA,MACN,oBAAoB;AAAA,MACpB,QAAQ;AAAA,IAAA;AAAA,EAEZ;AAIA,MAAI,gBAAgB,WAAW;AAC7B,UAAM,sBAAsB;AAAA,MAC1B;AAAA,MACA,iBAAiB;AAAA,MACjB;AAAA,MACA;AAAA,IAAA,CACD;AACD,WAAO;AAAA,MACL;AAAA,MACA,QAAQ;AAAA,MACR,UAAU,SAAS,YAAY;AAAA,MAC/B,MAAM,eAAe,SAAS,YAAY,KAAK,QAAQ,IAAI;AAAA,MAC3D,oBAAoB;AAAA,MACpB,QAAQ;AAAA,IAAA;AAAA,EAEZ;AAEA,SAAO;AAAA,IACL;AAAA,IACA,QAAQ;AAAA,IACR,UAAU,SAAS,YAAY;AAAA,IAC/B,MAAM,eAAe,SAAS,YAAY,KAAK,QAAQ,IAAI;AAAA,IAC3D,oBAAoB;AAAA,IACpB,QAAQ,SAAS;AAAA,EAAA;AAErB;AAUA,eAAe,sBAAsB,MAKnB;AAChB,MAAI,CAAC,KAAK,QAAQ,IAAI;AAGpB;AAAA,EACF;AACA,MAAI,gBAAgB,KAAK,eAAe,MAAM,KAAK,eAAe;AAChE;AAAA,EACF;AAEA,MAAI;AACF,UAAM,EAAE,sBAAA,IAA0B,MAAM,OAAO,sCAAsB;AACrE,UAAM,sBAAsB;AAAA,MAC1B,KAAK,KAAK;AAAA,MACV,cAAc,KAAK;AAAA,MACnB,cAAc,KAAK;AAAA,MACnB,UAAU,KAAK,QAAQ,YAAY,iBAAiB;AAAA,MACpD,IAAI,KAAK,QAAQ;AAAA,IAAA,CAClB;AAAA,EACH,QAAQ;AAAA,EAER;AACF;AAEO,SAAS,gCACd,KACA,QACA,UACA,IACM;AACN,0BAAwB,KAAK,QAAQ,UAAU,EAAE;AACnD;AC1PO,MAAM,8BAA8B;"}
package/dist/jobs.d.ts ADDED
@@ -0,0 +1,62 @@
1
+ import { SmrtObject } from '@happyvertical/smrt-core';
2
+
3
+ /** Feature flag key honored as the kill-switch for AI auto-translation. */
4
+ export declare const AUTO_TRANSLATE_FEATURE_KEY = "smrt-languages.auto_translate";
5
+
6
+ /**
7
+ * Enqueue a translation job for `(key, targetLocale)`, deduplicated against
8
+ * any already-pending job for the same target. Returns the (possibly existing)
9
+ * job's deterministic ID.
10
+ */
11
+ export declare function enqueueTranslationJob(options: EnqueueTranslationOptions): Promise<{
12
+ id: string;
13
+ status: 'enqueued' | 'duplicate' | 'skipped';
14
+ }>;
15
+
16
+ declare interface EnqueueTranslationOptions {
17
+ key: string;
18
+ targetLocale: string;
19
+ sourceLocale?: string;
20
+ tenantId?: string | null;
21
+ db: unknown;
22
+ /** When true, skip the dedup check and force a fresh job. */
23
+ force?: boolean;
24
+ }
25
+
26
+ /**
27
+ * SmrtObject that owns the translation job's `execute` method. The TaskRunner
28
+ * resolves jobs by `objectType` so the registered class name needs to match
29
+ * what `enqueueTranslationJob` writes.
30
+ */
31
+ export declare class LanguageTranslationTask extends SmrtObject {
32
+ /**
33
+ * Job-runner entrypoint. Reads the translation payload, calls
34
+ * `@happyvertical/ai`, and upserts a `LanguageOverride` row.
35
+ */
36
+ execute(args: TranslationJobPayload): Promise<{
37
+ skipped?: 'feature_disabled' | 'not_supported' | 'budget' | 'stale';
38
+ template?: string;
39
+ }>;
40
+ }
41
+
42
+ /**
43
+ * Prompt key registered with `smrt-prompts` so ops can tune the AI translation
44
+ * style without redeploying. The prompt itself is referenced by the job
45
+ * handler when calling `@happyvertical/ai`.
46
+ */
47
+ export declare const TRANSLATION_PROMPT_KEY = "smrt-languages.translation";
48
+
49
+ /** Payload pushed to the smrt-jobs translation handler. */
50
+ export declare interface TranslationJobPayload {
51
+ key: string;
52
+ sourceLocale: string;
53
+ sourceTemplate: string;
54
+ sourceHash: string;
55
+ targetLocale: string;
56
+ tenantId?: string | null;
57
+ /** Optional model override; defaults to config / @happyvertical/ai default. */
58
+ model?: string;
59
+ [key: string]: unknown;
60
+ }
61
+
62
+ export { }
package/dist/jobs.js ADDED
@@ -0,0 +1,8 @@
1
+ import { AUTO_TRANSLATE_FEATURE_KEY, LanguageTranslationTask, TRANSLATION_PROMPT_KEY, enqueueTranslationJob } from "./chunks/translation-job-DHg2E-eH.js";
2
+ export {
3
+ AUTO_TRANSLATE_FEATURE_KEY,
4
+ LanguageTranslationTask,
5
+ TRANSLATION_PROMPT_KEY,
6
+ enqueueTranslationJob
7
+ };
8
+ //# sourceMappingURL=jobs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"jobs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";"}