solid-translate 0.2.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/LICENSE +21 -0
- package/README.md +451 -0
- package/dist/cli.js +502 -0
- package/dist/index.d.ts +239 -0
- package/dist/index.js +238 -0
- package/dist/index.js.map +1 -0
- package/dist/vite.d.ts +42 -0
- package/dist/vite.js +339 -0
- package/dist/vite.js.map +1 -0
- package/package.json +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Omni Aura
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
# solid-translate
|
|
2
|
+
|
|
3
|
+
AI-powered i18n for SolidJS — full feature parity with [General Translation](https://generaltranslation.com), but open-source and BYOK (bring your own API key).
|
|
4
|
+
|
|
5
|
+
Write your app in one language. Wrap text in `<T>`. Get translations generated automatically at build time. No JSON key management. No external service required.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **`<T>` Component** — wrap any text for translation. Source text = key (no JSON wrangling)
|
|
10
|
+
- **`<Var>`** — protect dynamic content from translation
|
|
11
|
+
- **`<Num>`** — locale-aware number formatting via `Intl.NumberFormat`
|
|
12
|
+
- **`<Currency>`** — locale-aware currency formatting
|
|
13
|
+
- **`<DateTime>`** — locale-aware date/time formatting
|
|
14
|
+
- **`<Plural>`** — CLDR plural rules (zero/one/two/few/many/other)
|
|
15
|
+
- **`<LocaleSelector>`** — drop-in locale picker component
|
|
16
|
+
- **AI Context** — `context` prop for disambiguation ("Save" = save file vs. save money)
|
|
17
|
+
- **Auto Locale Detection** — detects from `navigator.languages` when `locale` prop is omitted
|
|
18
|
+
- **`msg()`** — mark strings for extraction outside of JSX
|
|
19
|
+
- **CLI Tool** — translate JSON, Markdown, and MDX files from the command line
|
|
20
|
+
- **Vite Plugin** — build-time translation with smart change detection
|
|
21
|
+
- **BYOK** — use any [Vercel AI SDK](https://ai-sdk.dev/) provider (OpenRouter, OpenAI, Anthropic, Google, etc.)
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
bun add solid-translate
|
|
27
|
+
bun add -d ai @ai-sdk/openai # or any AI SDK provider
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
### 1. Configure the Vite plugin
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// vite.config.ts
|
|
36
|
+
import { defineConfig } from "vite";
|
|
37
|
+
import solidPlugin from "vite-plugin-solid";
|
|
38
|
+
import { solidTranslate } from "solid-translate/vite";
|
|
39
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
40
|
+
|
|
41
|
+
const openrouter = createOpenAI({
|
|
42
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
43
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export default defineConfig({
|
|
47
|
+
plugins: [
|
|
48
|
+
solidPlugin(),
|
|
49
|
+
solidTranslate({
|
|
50
|
+
sourceLocale: "en",
|
|
51
|
+
targetLocales: ["es", "fr", "de", "ja"],
|
|
52
|
+
localesDir: "./src/locales",
|
|
53
|
+
model: openrouter("openai/gpt-4o-mini"),
|
|
54
|
+
autoExtract: true, // auto-discover <T> and msg() strings
|
|
55
|
+
}),
|
|
56
|
+
],
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### 2. Write your app
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
import { TranslationProvider, T, Var, Num, Plural, useTranslation, LocaleSelector } from "solid-translate";
|
|
64
|
+
import translations from "virtual:solid-translate";
|
|
65
|
+
|
|
66
|
+
function App() {
|
|
67
|
+
return (
|
|
68
|
+
<TranslationProvider
|
|
69
|
+
sourceLocale="en"
|
|
70
|
+
translations={translations}
|
|
71
|
+
// locale="es" ← optional! auto-detects from browser if omitted
|
|
72
|
+
>
|
|
73
|
+
<Page />
|
|
74
|
+
</TranslationProvider>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function Page() {
|
|
79
|
+
const { t } = useTranslation();
|
|
80
|
+
const [count, setCount] = createSignal(3);
|
|
81
|
+
const userName = () => "Alice";
|
|
82
|
+
|
|
83
|
+
return (
|
|
84
|
+
<div>
|
|
85
|
+
<h1><T>Welcome to our app!</T></h1>
|
|
86
|
+
|
|
87
|
+
{/* Dynamic content protected with <Var> */}
|
|
88
|
+
<p><T>Hello <Var>{userName()}</Var>, nice to see you!</T></p>
|
|
89
|
+
|
|
90
|
+
{/* AI context for disambiguation */}
|
|
91
|
+
<button><T context="save a document to disk">Save</T></button>
|
|
92
|
+
|
|
93
|
+
{/* Explicit key */}
|
|
94
|
+
<a><T id="nav.home">Home</T></a>
|
|
95
|
+
|
|
96
|
+
{/* Interpolation */}
|
|
97
|
+
<p>{t("items.count", { count: count() })}</p>
|
|
98
|
+
|
|
99
|
+
{/* Pluralization */}
|
|
100
|
+
<Plural n={count()}
|
|
101
|
+
zero="No items in your cart"
|
|
102
|
+
one="1 item in your cart"
|
|
103
|
+
other={`${count()} items in your cart`}
|
|
104
|
+
/>
|
|
105
|
+
|
|
106
|
+
{/* Locale-aware number */}
|
|
107
|
+
<p>Total: <Num>{1234567.89}</Num></p>
|
|
108
|
+
|
|
109
|
+
{/* Locale switcher */}
|
|
110
|
+
<LocaleSelector labels={{ en: "English", es: "Español", fr: "Français" }} />
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### 3. Build
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
bun run build
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
On the first build, the plugin generates translation files:
|
|
123
|
+
|
|
124
|
+
```
|
|
125
|
+
src/locales/
|
|
126
|
+
├── en.json # Source (auto-generated or manual)
|
|
127
|
+
├── es.json # AI-generated
|
|
128
|
+
├── fr.json # AI-generated
|
|
129
|
+
├── de.json # AI-generated
|
|
130
|
+
├── ja.json # AI-generated
|
|
131
|
+
└── .solid-translate.lock # Change tracking
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
On subsequent builds, only changed/new keys are re-translated. Check everything into git.
|
|
135
|
+
|
|
136
|
+
## API Reference
|
|
137
|
+
|
|
138
|
+
### Components
|
|
139
|
+
|
|
140
|
+
#### `<T>` — Translatable Text
|
|
141
|
+
|
|
142
|
+
```tsx
|
|
143
|
+
// Source text as key (no JSON file entry needed)
|
|
144
|
+
<T>Hello world</T>
|
|
145
|
+
|
|
146
|
+
// Explicit key
|
|
147
|
+
<T id="greeting">Hello world</T>
|
|
148
|
+
|
|
149
|
+
// With interpolation
|
|
150
|
+
<T params={{ name: userName() }}>Hello {{name}}</T>
|
|
151
|
+
|
|
152
|
+
// AI context for disambiguation
|
|
153
|
+
<T context="financial institution, not river bank">Bank</T>
|
|
154
|
+
|
|
155
|
+
// Mixed JSX with Var
|
|
156
|
+
<T>Welcome <Var>{userName()}</Var>, you have <Num>{count()}</Num> items</T>
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
#### `<Var>` — Variable Protection
|
|
160
|
+
|
|
161
|
+
Marks dynamic content that should NOT be translated. When used inside `<T>`, the surrounding text is translated but `<Var>` content is preserved.
|
|
162
|
+
|
|
163
|
+
```tsx
|
|
164
|
+
<T>Hello <Var>{userName()}</Var></T>
|
|
165
|
+
// Spanish: "Hola {userName()}"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
#### `<Num>` — Number Formatting
|
|
169
|
+
|
|
170
|
+
Locale-aware number formatting using `Intl.NumberFormat`.
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
<Num>{1000000}</Num> // "1,000,000" (en) / "1.000.000" (de)
|
|
174
|
+
<Num options={{ style: "percent" }}>{0.42}</Num> // "42%"
|
|
175
|
+
<Num options={{ notation: "compact" }}>{1500}</Num> // "1.5K"
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
#### `<Currency>` — Currency Formatting
|
|
179
|
+
|
|
180
|
+
```tsx
|
|
181
|
+
<Currency currency="USD">{29.99}</Currency> // "$29.99" (en-US) / "29,99 $US" (fr)
|
|
182
|
+
<Currency currency="EUR">{1000}</Currency> // "€1,000.00" (en) / "1.000,00 €" (de)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
#### `<DateTime>` — Date/Time Formatting
|
|
186
|
+
|
|
187
|
+
```tsx
|
|
188
|
+
<DateTime>{new Date()}</DateTime>
|
|
189
|
+
<DateTime options={{ dateStyle: "long" }}>{new Date()}</DateTime>
|
|
190
|
+
<DateTime options={{ hour: "numeric", minute: "numeric" }}>{Date.now()}</DateTime>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
#### `<Plural>` — Pluralization (CLDR)
|
|
194
|
+
|
|
195
|
+
Uses `Intl.PluralRules` for locale-correct plural forms.
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
<Plural n={count()}
|
|
199
|
+
zero="No items"
|
|
200
|
+
one="1 item"
|
|
201
|
+
two="2 items" // Used in Arabic, Welsh, etc.
|
|
202
|
+
few={`${count()} items`} // Used in Polish, Czech, etc.
|
|
203
|
+
many={`${count()} items`} // Used in Arabic, etc.
|
|
204
|
+
other={`${count()} items`}
|
|
205
|
+
/>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
#### `<LocaleSelector>` — Locale Picker
|
|
209
|
+
|
|
210
|
+
Drop-in `<select>` for switching locales.
|
|
211
|
+
|
|
212
|
+
```tsx
|
|
213
|
+
// Auto-generates display names via Intl.DisplayNames
|
|
214
|
+
<LocaleSelector />
|
|
215
|
+
|
|
216
|
+
// Custom labels
|
|
217
|
+
<LocaleSelector labels={{ en: "English", es: "Español" }} />
|
|
218
|
+
|
|
219
|
+
// Subset of locales
|
|
220
|
+
<LocaleSelector locales={["en", "es"]} />
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Hooks
|
|
224
|
+
|
|
225
|
+
#### `useTranslation()`
|
|
226
|
+
|
|
227
|
+
Full translation context.
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
const { t, locale, setLocale, sourceLocale, availableLocales } = useTranslation();
|
|
231
|
+
|
|
232
|
+
t("greeting") // translated string
|
|
233
|
+
t("items.count", { count: 3 }) // with interpolation
|
|
234
|
+
locale() // "es"
|
|
235
|
+
setLocale("fr") // switch locale
|
|
236
|
+
availableLocales() // ["en", "es", "fr", ...]
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
#### `useLocale()`
|
|
240
|
+
|
|
241
|
+
Lightweight hook for just locale info.
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
const { locale, setLocale, sourceLocale, availableLocales } = useLocale();
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### `<TranslationProvider>`
|
|
248
|
+
|
|
249
|
+
Root provider. Wraps your app.
|
|
250
|
+
|
|
251
|
+
```tsx
|
|
252
|
+
<TranslationProvider
|
|
253
|
+
translations={translations} // Translation dictionaries
|
|
254
|
+
sourceLocale="en" // Source locale (default: "en")
|
|
255
|
+
// locale="es" // Optional: explicit locale
|
|
256
|
+
// // If omitted, auto-detects from navigator.languages
|
|
257
|
+
>
|
|
258
|
+
{children}
|
|
259
|
+
</TranslationProvider>
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### `msg()` — Shared Strings
|
|
263
|
+
|
|
264
|
+
Mark strings for extraction outside of JSX. At build time, the Vite plugin extracts them. At runtime, use `t()` to translate.
|
|
265
|
+
|
|
266
|
+
```tsx
|
|
267
|
+
import { msg } from "solid-translate";
|
|
268
|
+
|
|
269
|
+
// Mark for extraction (build-time)
|
|
270
|
+
const SAVE = msg("Save changes");
|
|
271
|
+
const DELETE = msg("Delete");
|
|
272
|
+
|
|
273
|
+
// Translate at runtime
|
|
274
|
+
function Toolbar() {
|
|
275
|
+
const { t } = useTranslation();
|
|
276
|
+
return (
|
|
277
|
+
<div>
|
|
278
|
+
<button>{t(SAVE)}</button>
|
|
279
|
+
<button>{t(DELETE)}</button>
|
|
280
|
+
</div>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
## Vite Plugin Config
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
solidTranslate({
|
|
289
|
+
sourceLocale: "en", // Source locale (default: "en")
|
|
290
|
+
targetLocales: ["es", "fr"], // Target locales
|
|
291
|
+
localesDir: "./src/locales", // Locale files dir (default: "./src/locales")
|
|
292
|
+
model: openai("gpt-4o-mini"), // Any Vercel AI SDK model
|
|
293
|
+
systemPrompt: "...", // Custom AI prompt (optional)
|
|
294
|
+
batchSize: 50, // Keys per API call (default: 50)
|
|
295
|
+
autoExtract: true, // Auto-extract <T> and msg() strings (default: false)
|
|
296
|
+
include: ["src/**/*.tsx"], // Files to scan for extraction
|
|
297
|
+
})
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
## CLI
|
|
301
|
+
|
|
302
|
+
For translating locale files, JSON, Markdown, and MDX outside of the Vite build.
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
# Initialize config
|
|
306
|
+
npx solid-translate init
|
|
307
|
+
|
|
308
|
+
# Extract strings from source files
|
|
309
|
+
npx solid-translate extract
|
|
310
|
+
|
|
311
|
+
# Translate everything
|
|
312
|
+
npx solid-translate translate
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
### CLI Config (`solid-translate.config.json`)
|
|
316
|
+
|
|
317
|
+
```json
|
|
318
|
+
{
|
|
319
|
+
"sourceLocale": "en",
|
|
320
|
+
"targetLocales": ["es", "fr", "de"],
|
|
321
|
+
"localesDir": "./src/locales",
|
|
322
|
+
"provider": "openrouter",
|
|
323
|
+
"model": "openai/gpt-4o-mini",
|
|
324
|
+
"batchSize": 50,
|
|
325
|
+
"include": ["src/**/*.tsx", "src/**/*.ts"],
|
|
326
|
+
"files": {
|
|
327
|
+
"json": {
|
|
328
|
+
"include": ["i18n/[locale]/*.json"]
|
|
329
|
+
},
|
|
330
|
+
"md": {
|
|
331
|
+
"include": ["docs/[locale]/**/*.md"]
|
|
332
|
+
},
|
|
333
|
+
"mdx": {
|
|
334
|
+
"include": ["content/[locale]/**/*.mdx"]
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
The `[locale]` placeholder is replaced with each target locale. Source files are found by replacing `[locale]` with the source locale.
|
|
341
|
+
|
|
342
|
+
### Environment Variables
|
|
343
|
+
|
|
344
|
+
```bash
|
|
345
|
+
OPENROUTER_API_KEY=... # OpenRouter
|
|
346
|
+
OPENAI_API_KEY=... # OpenAI
|
|
347
|
+
ANTHROPIC_API_KEY=... # Anthropic
|
|
348
|
+
GOOGLE_API_KEY=... # Google AI
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
## Using with different AI providers
|
|
352
|
+
|
|
353
|
+
The plugin and CLI accept any [Vercel AI SDK](https://ai-sdk.dev/) compatible model:
|
|
354
|
+
|
|
355
|
+
```ts
|
|
356
|
+
// OpenRouter (access to 100+ models)
|
|
357
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
358
|
+
const openrouter = createOpenAI({
|
|
359
|
+
baseURL: "https://openrouter.ai/api/v1",
|
|
360
|
+
apiKey: process.env.OPENROUTER_API_KEY,
|
|
361
|
+
});
|
|
362
|
+
const model = openrouter("anthropic/claude-sonnet-4-5");
|
|
363
|
+
|
|
364
|
+
// OpenAI directly
|
|
365
|
+
import { openai } from "@ai-sdk/openai";
|
|
366
|
+
const model = openai("gpt-4o-mini");
|
|
367
|
+
|
|
368
|
+
// Anthropic directly
|
|
369
|
+
import { anthropic } from "@ai-sdk/anthropic";
|
|
370
|
+
const model = anthropic("claude-haiku-4-5-20251001");
|
|
371
|
+
|
|
372
|
+
// Google
|
|
373
|
+
import { google } from "@ai-sdk/google";
|
|
374
|
+
const model = google("gemini-2.0-flash");
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
## CI/CD Integration
|
|
378
|
+
|
|
379
|
+
Add to your build script for automatic translations on every deploy:
|
|
380
|
+
|
|
381
|
+
```json
|
|
382
|
+
{
|
|
383
|
+
"scripts": {
|
|
384
|
+
"translate": "solid-translate translate",
|
|
385
|
+
"build": "bun run translate && vite build"
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
Or in GitHub Actions:
|
|
391
|
+
|
|
392
|
+
```yaml
|
|
393
|
+
- name: Translate
|
|
394
|
+
env:
|
|
395
|
+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
|
396
|
+
run: npx solid-translate translate
|
|
397
|
+
|
|
398
|
+
- name: Build
|
|
399
|
+
run: bun run build
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
## How change detection works
|
|
403
|
+
|
|
404
|
+
The `.solid-translate.lock` file tracks a content hash for each source key. On build:
|
|
405
|
+
|
|
406
|
+
1. Source locale file is read and each value is hashed
|
|
407
|
+
2. Hashes are compared against the lock file
|
|
408
|
+
3. Only new or changed keys are sent to the AI for translation
|
|
409
|
+
4. If a key's `context` prop changed, it's re-translated for better accuracy
|
|
410
|
+
5. Unchanged translations are preserved from existing locale files
|
|
411
|
+
6. Deleted source keys are removed from all target files
|
|
412
|
+
|
|
413
|
+
This means you can safely check in all translation files. Rebuilds are free unless you change source text.
|
|
414
|
+
|
|
415
|
+
## TypeScript
|
|
416
|
+
|
|
417
|
+
For the virtual module import, add to your `env.d.ts` or `vite-env.d.ts`:
|
|
418
|
+
|
|
419
|
+
```ts
|
|
420
|
+
declare module "virtual:solid-translate" {
|
|
421
|
+
const translations: Record<string, Record<string, string>>;
|
|
422
|
+
export default translations;
|
|
423
|
+
}
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
## Comparison with General Translation (gt-react)
|
|
427
|
+
|
|
428
|
+
| Feature | gt-react | solid-translate |
|
|
429
|
+
|---------|----------|-----------------|
|
|
430
|
+
| `<T>` component | ✅ | ✅ |
|
|
431
|
+
| `<Var>` variable protection | ✅ | ✅ |
|
|
432
|
+
| `<Num>` number formatting | ✅ | ✅ |
|
|
433
|
+
| `<Currency>` formatting | ✅ | ✅ |
|
|
434
|
+
| `<DateTime>` formatting | ✅ | ✅ |
|
|
435
|
+
| `<Plural>` CLDR rules | ✅ | ✅ |
|
|
436
|
+
| `<LocaleSelector>` | ✅ | ✅ |
|
|
437
|
+
| AI context disambiguation | ✅ | ✅ |
|
|
438
|
+
| Auto locale detection | ✅ | ✅ |
|
|
439
|
+
| Shared strings (`msg()`) | ✅ | ✅ |
|
|
440
|
+
| CLI for JSON/MD/MDX | ✅ | ✅ |
|
|
441
|
+
| CI/CD integration | ✅ | ✅ |
|
|
442
|
+
| Zero refactoring | ✅ | ✅ |
|
|
443
|
+
| BYOK (bring your own key) | ❌ (SaaS) | ✅ |
|
|
444
|
+
| No vendor lock-in | ❌ | ✅ |
|
|
445
|
+
| SolidJS native | ❌ (React) | ✅ |
|
|
446
|
+
| Build-time translation | ❌ (runtime) | ✅ |
|
|
447
|
+
| Open source | Partial | ✅ MIT |
|
|
448
|
+
|
|
449
|
+
## License
|
|
450
|
+
|
|
451
|
+
MIT
|