@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.
- package/AGENTS.md +81 -0
- package/CLAUDE.md +1 -0
- package/LICENSE +7 -0
- package/README.md +193 -0
- package/dist/chunks/language-registry-CgsuwQo6.js +509 -0
- package/dist/chunks/language-registry-CgsuwQo6.js.map +1 -0
- package/dist/chunks/translation-job-DHg2E-eH.js +286 -0
- package/dist/chunks/translation-job-DHg2E-eH.js.map +1 -0
- package/dist/cli.d.ts +43 -0
- package/dist/cli.js +83 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +247 -0
- package/dist/index.js +242 -0
- package/dist/index.js.map +1 -0
- package/dist/jobs.d.ts +62 -0
- package/dist/jobs.js +8 -0
- package/dist/jobs.js.map +1 -0
- package/dist/manifest.json +526 -0
- package/dist/smrt-knowledge.json +343 -0
- package/dist/types.d.ts +106 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +77 -0
package/AGENTS.md
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# languages
|
|
2
|
+
|
|
3
|
+
SMRT language string registry with file/config + tenant overrides and AI-driven
|
|
4
|
+
auto-translation for missing locales.
|
|
5
|
+
|
|
6
|
+
## Core pieces
|
|
7
|
+
|
|
8
|
+
- `defineLanguageString({ key, locale, template })` registers a code default in
|
|
9
|
+
a global process registry. Same shape as `definePrompt` but keyed by
|
|
10
|
+
`(key, locale)` rather than `key` alone.
|
|
11
|
+
- `resolveLanguageString(key, options)` walks the 5-layer chain (code → file
|
|
12
|
+
config → app override → tenant override → runtime override) and falls back
|
|
13
|
+
through the locale chain (`fr-CA` → `fr` → default) before giving up.
|
|
14
|
+
- `LanguageOverride` stores app-level and tenant-level overrides in
|
|
15
|
+
`_smrt_language_overrides`. `auto_generated`, `source_hash`, `ai_model`,
|
|
16
|
+
`reviewed_at`, `reviewed_by` fields support the AI auto-translation pipeline
|
|
17
|
+
and admin review queue.
|
|
18
|
+
- `enqueueTranslationJob({ key, targetLocale })` writes a `LanguageTranslationTask`
|
|
19
|
+
job into the `languages` queue with a deterministic dedup ID so concurrent
|
|
20
|
+
resolver misses collapse into one job.
|
|
21
|
+
|
|
22
|
+
## Locale-miss flow
|
|
23
|
+
|
|
24
|
+
When `resolveLanguageString` cannot find an exact `(key, locale, tenantId)`:
|
|
25
|
+
|
|
26
|
+
1. Walk the locale fallback chain (`buildLocaleFallbackChain`).
|
|
27
|
+
2. Return the first hit — same call returns immediately with
|
|
28
|
+
`source: 'fallback'`.
|
|
29
|
+
3. Fire-and-forget `enqueueTranslationJob` for the missing target, scoped to
|
|
30
|
+
the current tenant for glossary purposes. App-level translations are
|
|
31
|
+
reusable across tenants, so the resulting `LanguageOverride` row is written
|
|
32
|
+
with `tenantId: null`.
|
|
33
|
+
4. Subsequent requests hit the new app-level row and resolve at the requested
|
|
34
|
+
locale.
|
|
35
|
+
|
|
36
|
+
The translation job:
|
|
37
|
+
|
|
38
|
+
- Honors the `smrt-languages.auto_translate` feature flag (kill switch).
|
|
39
|
+
- Skips locales outside `supportedLocales` when configured.
|
|
40
|
+
- Skips when `LanguageOverride` already exists with a matching `source_hash`
|
|
41
|
+
(re-translation is hash-gated, never time-based).
|
|
42
|
+
- Never overwrites a row with `auto_generated: false` — human edits win
|
|
43
|
+
permanently.
|
|
44
|
+
- Pulls the tenant's existing overrides and renders them as a glossary so
|
|
45
|
+
auto-translations match tenant voice.
|
|
46
|
+
|
|
47
|
+
## Conventions
|
|
48
|
+
|
|
49
|
+
- Keys are namespaced by package: `users.role.member`, `commerce.invoice.dueText`.
|
|
50
|
+
- Locales follow BCP-47 (`en`, `fr-CA`, `pt-BR`) and are normalized to
|
|
51
|
+
lowercase-language / uppercase-region on persistence.
|
|
52
|
+
- The translation prompt itself is registered with `smrt-prompts` under
|
|
53
|
+
`smrt-languages.translation` so ops can tune wording without redeploying.
|
|
54
|
+
- `context` column on `_smrt_language_overrides` is set to `tenantId` or
|
|
55
|
+
`'__app__'` so the `(key, locale, context)` upsert key remains unique even
|
|
56
|
+
with nullable `tenantId`.
|
|
57
|
+
|
|
58
|
+
## Public API surface
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import {
|
|
62
|
+
defineLanguageString,
|
|
63
|
+
resolveLanguageString,
|
|
64
|
+
LanguageOverride,
|
|
65
|
+
LanguageOverrideCollection,
|
|
66
|
+
clearLanguageCache,
|
|
67
|
+
} from '@happyvertical/smrt-languages';
|
|
68
|
+
|
|
69
|
+
defineLanguageString({
|
|
70
|
+
key: 'users.role.member',
|
|
71
|
+
locale: 'en',
|
|
72
|
+
template: 'Member',
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const text = await resolveLanguageString('users.role.member', {
|
|
76
|
+
db,
|
|
77
|
+
tenantId: 'tenant-a',
|
|
78
|
+
locale: 'es',
|
|
79
|
+
vars: { name: 'Will' },
|
|
80
|
+
});
|
|
81
|
+
```
|
package/CLAUDE.md
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
@AGENTS.md
|
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright <2025> <Happy Vertical Corporation>
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @happyvertical/smrt-languages
|
|
2
|
+
|
|
3
|
+
Code-first language strings with config + tenant overrides and AI-driven
|
|
4
|
+
auto-translation for SMRT applications.
|
|
5
|
+
|
|
6
|
+
`smrt-languages` mirrors the architecture of `@happyvertical/smrt-prompts`:
|
|
7
|
+
packages declare their user-facing strings as code, applications and tenants
|
|
8
|
+
override them through file config or DB rows, and the resolver merges every
|
|
9
|
+
layer at runtime. v1 adds an automatic AI-translation step backed by
|
|
10
|
+
`@happyvertical/smrt-jobs`: the first time a string is requested in a locale
|
|
11
|
+
that has neither a code default nor an override, the resolver returns a
|
|
12
|
+
fallback locale immediately and enqueues a background translation. Subsequent
|
|
13
|
+
requests hit the new app-level row.
|
|
14
|
+
|
|
15
|
+
This package is a Phase 2 prerequisite of the broader package adoption epic —
|
|
16
|
+
issue [#1200](https://github.com/happyvertical/smrt/issues/1200).
|
|
17
|
+
|
|
18
|
+
## Why
|
|
19
|
+
|
|
20
|
+
- Today, packages hard-code labels like `'Member'`, `'Article'`, or
|
|
21
|
+
`'Payment due by {dueDate}'`. There is no way for a tenant to use
|
|
22
|
+
`'Subscriber'` instead of `'Member'` without forking, and no way for a
|
|
23
|
+
tenant to operate in their own language.
|
|
24
|
+
- `smrt-prompts` already proved the layered-override pattern for AI prompts.
|
|
25
|
+
`smrt-languages` applies the same pattern to display strings, plus an
|
|
26
|
+
AI-driven gap-filler that closes the loop on missing locales without
|
|
27
|
+
blocking on the first request.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```typescript
|
|
32
|
+
import {
|
|
33
|
+
defineLanguageString,
|
|
34
|
+
resolveLanguageString,
|
|
35
|
+
} from '@happyvertical/smrt-languages';
|
|
36
|
+
|
|
37
|
+
// Define defaults at startup, typically in your package's
|
|
38
|
+
// __smrt-register__.ts so the registry is populated before resolves run.
|
|
39
|
+
defineLanguageString({
|
|
40
|
+
key: 'users.role.member',
|
|
41
|
+
locale: 'en',
|
|
42
|
+
template: 'Member',
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
defineLanguageString({
|
|
46
|
+
key: 'commerce.invoice.dueText',
|
|
47
|
+
locale: 'en',
|
|
48
|
+
template: 'Payment due by {dueDate}',
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Resolve at runtime. Tenant context is read from AsyncLocalStorage by default.
|
|
52
|
+
const text = await resolveLanguageString('users.role.member', {
|
|
53
|
+
db,
|
|
54
|
+
locale: 'es',
|
|
55
|
+
vars: { dueDate: '2026-06-01' },
|
|
56
|
+
// strict: false → return the English fallback and enqueue an AI translation.
|
|
57
|
+
// strict: true → throw when no resolution exists.
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Resolution layers
|
|
62
|
+
|
|
63
|
+
In ascending priority:
|
|
64
|
+
|
|
65
|
+
1. **Code default** — `defineLanguageString({ key, locale, template })`
|
|
66
|
+
2. **File/config override** — `getPackageConfig('languages').overrides[key][locale]`
|
|
67
|
+
3. **App-level stored override** — `LanguageOverride` row with `tenantId = null`
|
|
68
|
+
4. **Tenant-level stored override** — `LanguageOverride` row with `tenantId = <current>`
|
|
69
|
+
5. **Runtime override** — `resolveLanguageString(key, { overrides: { template: '...' } })`
|
|
70
|
+
|
|
71
|
+
When the requested `(key, locale)` doesn't exist anywhere, the resolver walks
|
|
72
|
+
a fallback chain — `fr-CA` → `fr` → registered default-locale (`en`) — and
|
|
73
|
+
returns the first hit. Whenever the hit is at a different locale than what
|
|
74
|
+
was requested, an AI translation job is enqueued for the original target.
|
|
75
|
+
|
|
76
|
+
## Storage
|
|
77
|
+
|
|
78
|
+
Overrides live in `_smrt_language_overrides`:
|
|
79
|
+
|
|
80
|
+
| Column | Notes |
|
|
81
|
+
|--------|-------|
|
|
82
|
+
| `key` | Namespaced string key (`users.role.member`) |
|
|
83
|
+
| `locale` | BCP-47 tag (`en`, `fr-CA`) |
|
|
84
|
+
| `tenantId` | `null` for app-level, tenantId for tenant-level |
|
|
85
|
+
| `template` | The override string with `{var}` placeholders |
|
|
86
|
+
| `auto_generated` | `true` when produced by the AI translation job |
|
|
87
|
+
| `source_hash` | sha256 of the source template at translation time |
|
|
88
|
+
| `ai_model` | Model identifier; `null` for human-edited rows |
|
|
89
|
+
| `reviewed_at` / `reviewed_by` | Set when an admin approves an auto row |
|
|
90
|
+
|
|
91
|
+
Source-hash gating means re-translation only happens when the source actually
|
|
92
|
+
changes — auto-generated rows whose `source_hash` matches are left alone.
|
|
93
|
+
Human-edited rows (`auto_generated: false`) are **never** overwritten.
|
|
94
|
+
|
|
95
|
+
## AI auto-translation
|
|
96
|
+
|
|
97
|
+
When a `(key, targetLocale)` is missed, the resolver enqueues a
|
|
98
|
+
`LanguageTranslationTask` job into the `languages` queue with a deterministic
|
|
99
|
+
dedup ID — `smrt-languages.translate:<key>:<targetLocale>` — so concurrent
|
|
100
|
+
misses collapse into a single job. The job:
|
|
101
|
+
|
|
102
|
+
1. Reads the tenant's existing language overrides as a glossary (no-op when
|
|
103
|
+
no tenant context).
|
|
104
|
+
2. Calls `@happyvertical/ai` with a low-temperature translation prompt that
|
|
105
|
+
itself is registered via `smrt-prompts` under
|
|
106
|
+
`smrt-languages.translation` — operators can tune the wording without
|
|
107
|
+
redeploying.
|
|
108
|
+
3. Validates the response (non-empty, no obvious markup leaks).
|
|
109
|
+
4. Upserts an app-level `LanguageOverride` row with `auto_generated: true`,
|
|
110
|
+
`source_hash`, and `ai_model`.
|
|
111
|
+
5. Invalidates the resolver cache for `(key, targetLocale, *)`.
|
|
112
|
+
|
|
113
|
+
### Cost & abuse controls
|
|
114
|
+
|
|
115
|
+
- `smrt-features` flag `smrt-languages.auto_translate` — global / per-tenant
|
|
116
|
+
kill switch.
|
|
117
|
+
- `translationBudgetPerTenantPerDay` — daily cap per tenant.
|
|
118
|
+
- `supportedLocales` — optional allowlist; jobs for other locales are dropped
|
|
119
|
+
before any AI call.
|
|
120
|
+
- Source-hash gating prevents re-translation when nothing changed.
|
|
121
|
+
|
|
122
|
+
### Admin review
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
smrt languages translate --locales=es,fr,de # batch eager pre-population
|
|
126
|
+
smrt languages review --locale=es # list unreviewed auto rows
|
|
127
|
+
smrt languages approve <id> # mark reviewed
|
|
128
|
+
smrt languages edit <id> --template "..." # edit + flip auto_generated to false
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
CLI surfaces are auto-generated by SMRT from the `LanguageOverride` model and
|
|
132
|
+
helpers in `src/cli.ts`.
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
`smrt.config.{js,ts,json}`:
|
|
137
|
+
|
|
138
|
+
```js
|
|
139
|
+
export default {
|
|
140
|
+
packages: {
|
|
141
|
+
languages: {
|
|
142
|
+
defaultLocale: 'en',
|
|
143
|
+
supportedLocales: ['en', 'es', 'fr', 'de', 'ja'],
|
|
144
|
+
translationBudgetPerTenantPerDay: 200,
|
|
145
|
+
overrides: {
|
|
146
|
+
'users.role.member': {
|
|
147
|
+
es: 'Miembro',
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Subpath exports
|
|
156
|
+
|
|
157
|
+
The package root (`@happyvertical/smrt-languages`) only exposes the read path:
|
|
158
|
+
`defineLanguageString`, `resolveLanguageString`, `LanguageOverride`, the cache
|
|
159
|
+
helpers, the glossary helper, and shared types/utilities. Loading the root
|
|
160
|
+
does **not** pull `@happyvertical/ai`, smrt-jobs, smrt-features, or
|
|
161
|
+
smrt-prompts into the consumer bundle.
|
|
162
|
+
|
|
163
|
+
The translation-worker stack lives on a subpath:
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
// Background workers + tests that enqueue or run translation jobs.
|
|
167
|
+
import {
|
|
168
|
+
enqueueTranslationJob,
|
|
169
|
+
LanguageTranslationTask,
|
|
170
|
+
AUTO_TRANSLATE_FEATURE_KEY,
|
|
171
|
+
TRANSLATION_PROMPT_KEY,
|
|
172
|
+
} from '@happyvertical/smrt-languages/jobs';
|
|
173
|
+
|
|
174
|
+
// Admin / batch CLI helpers.
|
|
175
|
+
import {
|
|
176
|
+
translateMissing,
|
|
177
|
+
approveAutoTranslation,
|
|
178
|
+
editLanguageOverride,
|
|
179
|
+
listUnreviewedAutoTranslations,
|
|
180
|
+
} from '@happyvertical/smrt-languages/cli';
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The resolver itself dynamically imports the worker module on a soft-miss, so
|
|
184
|
+
calling `resolveLanguageString` still triggers a translation enqueue without
|
|
185
|
+
the caller having to pre-import the `/jobs` subpath.
|
|
186
|
+
|
|
187
|
+
## Out of scope (v1)
|
|
188
|
+
|
|
189
|
+
Pluralization, ICU MessageFormat, Svelte component i18n, RTL layout, locale
|
|
190
|
+
negotiation HTTP middleware, XLIFF/PO TM-tool integration, and richer quality
|
|
191
|
+
scoring of AI translations are all v1.1+ concerns. v1 sticks to plain
|
|
192
|
+
`{var}` substitution and the resolution chain above so adoption stays a
|
|
193
|
+
mechanical refactor across consumer packages.
|