@happyvertical/smrt-languages
Language string registry with a 5-layer cascade, locale fallback chains, and AI-driven auto-translation for missing locales with admin review.
v0.29.34i18n RegistryAI Auto-TranslationLocale Fallback
Overview
smrt-languages is the i18n companion to smrt-prompts. It uses
the same 5-layer override cascade but is keyed by (key, locale) instead of just key. When a locale is missing, it walks a fallback chain (fr-CA to fr to default) and asynchronously enqueues an AI translation job so the next request
returns the requested locale natively.
Installation
bash
npm install @happyvertical/smrt-languagesQuick Start
typescript
import {
defineLanguageString,
resolveLanguageString,
LanguageOverride,
LanguageOverrideCollection,
clearLanguageCache,
} from '@happyvertical/smrt-languages';
// 1. Register code defaults (typically at module load time)
defineLanguageString({
key: 'users.role.member',
locale: 'en',
template: 'Member',
});
defineLanguageString({
key: 'commerce.invoice.dueText',
locale: 'en',
template: 'Due by {{ date }}',
});
// 2. Resolve a string for the current tenant + locale
const text = await resolveLanguageString('users.role.member', {
db,
tenantId: 'tenant-a',
locale: 'es', // not registered — walks fallback chain
vars: { name: 'Will' },
});
// First call: returns 'Member' with source: 'fallback'
// Async: enqueues translation job; result becomes app-level override
// Next call: returns the Spanish translationThe 5-Layer Cascade
Identical to smrt-prompts, but keyed by (key, locale):
text
Priority (low to high)
1. Code default defineLanguageString({ key, locale, template })
2. File / config package config 'languages' overrides
3. App-level stored LanguageOverride row with tenantId = null
4. Tenant-level LanguageOverride row with current tenant
5. Runtime override resolveLanguageString({ overrides })
If no row exists for the exact (key, locale, tenantId), fall through to:
- Same key with the requested locale's fallback chain (fr-CA -> fr -> default)
- Return the first hit with source: 'fallback'
- Fire-and-forget AI translation job for the missing targetLocale-Miss Flow
text
// When resolveLanguageString cannot find (key, locale, tenantId):
// 1. Walk locale fallback chain (buildLocaleFallbackChain)
// fr-CA -> fr -> en (configured default)
// 2. Return the first hit immediately:
// { text, source: 'fallback', resolvedFromLocale }
// 3. Fire-and-forget enqueueTranslationJob:
// - Scoped to current tenant for glossary purposes
// - Result is written app-level (tenantId: null) — reusable across tenants
// - Job ID is deterministic, so concurrent misses collapse into one job
// 4. Subsequent requests hit the new app-level row at the requested locale.AI Translation Pipeline
The translation worker honors several invariants to avoid runaway costs and bad overwrites:
- Honors the
smrt-languages.auto_translatefeature flag (kill switch) - Skips locales outside
supportedLocaleswhen configured - Skips when a
LanguageOverridealready exists with a matchingsource_hash(hash-gated, never time-based) - Never overwrites rows with
auto_generated: false— human edits win permanently - Pulls the tenant's existing overrides as a glossary so translations match tenant voice
typescript
// LanguageOverride model fields for the pipeline:
class LanguageOverride extends SmrtObject {
key: string
locale: string // BCP-47, normalized (lowercase lang / uppercase region)
tenantId: string | null
template: string
auto_generated: boolean // true if produced by translation worker
source_hash: string | null // sha256 of source template at translation time
ai_model: string | null // which model produced the translation
reviewed_at: string | null // ISO timestamp of admin review
reviewed_by: string | null // userId of reviewer (admin review queue)
// Note: 'context' is inherited from SmrtObject and set in save() to
// (tenantId ?? '__app__'); it backs the (key, locale, context) upsert key.
}Conventions
- Keys are namespaced by package:
users.role.member,commerce.invoice.dueText - Locales follow BCP-47 (
en,fr-CA,pt-BR) and are normalized on persistence - The translation prompt itself is registered with
smrt-promptsundersmrt-languages.translation— ops can tune wording without redeploying - The
contextcolumn carriestenantIdor'__app__'so the(key, locale, context)upsert key remains unique even with a nullabletenantId
Gotchas
DOs
- Register every user-visible string with
defineLanguageStringat the default locale - Use the admin review queue to promote auto-generated translations to reviewed status
- Manage the kill switch via the
smrt-languages.auto_translatefeature flag - Provide a tenant glossary by curating reviewed overrides — the translator uses them
DON'Ts
- Don't translate user-supplied content with this package — it's for app strings only
- Don't set
auto_generated: falseon auto-produced rows until a human reviews - Don't drop the
contextcolumn — uniqueness depends on it - Don't enqueue translations synchronously — fire-and-forget is the contract