@babelize/sdk 0.0.1
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/README.md +333 -0
- package/dist/index.cjs +451 -0
- package/dist/index.d.cts +47 -0
- package/dist/index.d.ts +47 -0
- package/dist/index.js +424 -0
- package/package.json +37 -0
package/README.md
ADDED
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
# @babelize/sdk
|
|
2
|
+
|
|
3
|
+
**Core runtime for Babelize** — wrap strings, translate, cache, done. No translation keys, no locale files, no configuration.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { babelize } from "@babelize/sdk";
|
|
7
|
+
|
|
8
|
+
babelize.init(process.env.BABELIZE_API_KEY);
|
|
9
|
+
|
|
10
|
+
babelize("Welcome"); // "ようこそ"
|
|
11
|
+
babelize("Hello {name}", { name }); // "こんにちは Mohit"
|
|
12
|
+
babelize.p("{count} item", "{count} items", 5); // "5 items"
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bun add @babelize/sdk
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
```ts
|
|
28
|
+
import { babelize } from "@babelize/sdk";
|
|
29
|
+
|
|
30
|
+
// 1. Initialize
|
|
31
|
+
babelize.init({
|
|
32
|
+
apiKey: process.env.BABELIZE_API_KEY,
|
|
33
|
+
locale: "ja",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// 2. Load production lockfile (generated at build time)
|
|
37
|
+
import lockfile from "virtual:babelize-lockfile";
|
|
38
|
+
lockfile && babelize.loadLockfile(lockfile);
|
|
39
|
+
|
|
40
|
+
// 3. Get translations
|
|
41
|
+
babelize("Welcome"); // "バビリゾーンへようこそ"
|
|
42
|
+
babelize("Hello {name}", { name: "World" }); // "こんにちは World"
|
|
43
|
+
|
|
44
|
+
// 4. Switch locale
|
|
45
|
+
babelize.setLocale("fr");
|
|
46
|
+
babelize("Welcome"); // "Bienvenue à Babelize"
|
|
47
|
+
|
|
48
|
+
// 5. Get current locale
|
|
49
|
+
const locale = babelize.getLocale(); // "fr"
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### With React
|
|
53
|
+
|
|
54
|
+
```tsx
|
|
55
|
+
import { BabelizeProvider, useBabelize, useLocale } from "@babelize/sdk-react";
|
|
56
|
+
|
|
57
|
+
// Wrap your app
|
|
58
|
+
root.render(
|
|
59
|
+
<BabelizeProvider>
|
|
60
|
+
<App />
|
|
61
|
+
</BabelizeProvider>,
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
---
|
|
66
|
+
|
|
67
|
+
## API
|
|
68
|
+
|
|
69
|
+
### `babelize(str, vars?)`
|
|
70
|
+
|
|
71
|
+
Translate a string into the current locale.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
babelize("Welcome"); // "ようこそ"
|
|
75
|
+
babelize("Hello {name}", { name: "Mohit" }); // "こんにちは Mohit"
|
|
76
|
+
babelize("You have {count} messages", { count: 5 }); // "5件のメッセージがあります"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
- Returns the translated string from cache or lockfile
|
|
80
|
+
- Returns the original string if no translation is found
|
|
81
|
+
- Background API call is queued if the string is not cached
|
|
82
|
+
- `{key}` placeholders are interpolated with `vars`
|
|
83
|
+
|
|
84
|
+
### `babelize.init(config)`
|
|
85
|
+
|
|
86
|
+
Initialize the SDK. Accepts an API key string or a config object.
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
// With API key string
|
|
90
|
+
babelize.init("blz_sk_...");
|
|
91
|
+
|
|
92
|
+
// With config object
|
|
93
|
+
babelize.init({
|
|
94
|
+
apiKey: "blz_sk_...",
|
|
95
|
+
locale: "ja",
|
|
96
|
+
fallbackLocale: "en",
|
|
97
|
+
apiUrl: "https://api.babelize.co/api",
|
|
98
|
+
});
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Options:**
|
|
102
|
+
|
|
103
|
+
| Option | Type | Default | Description |
|
|
104
|
+
|--------|------|---------|-------------|
|
|
105
|
+
| `apiKey` | `string` | — | Babelize API key (optional in production) |
|
|
106
|
+
| `locale` | `string` | auto-detected | Initial locale |
|
|
107
|
+
| `fallbackLocale` | `string` | `"en"` | Fallback when auto-detection fails |
|
|
108
|
+
| `apiUrl` | `string` | `"https://api.babelize.co/api"` | API endpoint |
|
|
109
|
+
|
|
110
|
+
### `babelize.setLocale(locale)`
|
|
111
|
+
|
|
112
|
+
Switch the active locale. All subsequent `babelize()` calls use this locale. Triggers re-render in React components.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
babelize.setLocale("fr");
|
|
116
|
+
babelize.getLocale(); // "fr"
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### `babelize.getLocale()`
|
|
120
|
+
|
|
121
|
+
Returns the currently active locale.
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const current = babelize.getLocale();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### `babelize.p(singular, plural, countOrVars)`
|
|
128
|
+
|
|
129
|
+
Simple two-form pluralization. Uses [`Intl.PluralRules`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/PluralRules) to select the correct form for the current locale.
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
// With count as number
|
|
133
|
+
babelize.p("{count} item", "{count} items", 1); // "1 item" (one)
|
|
134
|
+
babelize.p("{count} item", "{count} items", 5); // "5 items" (other)
|
|
135
|
+
|
|
136
|
+
// With vars object (count is extracted)
|
|
137
|
+
babelize.p("{count} item", "{count} items", { count: items.length });
|
|
138
|
+
|
|
139
|
+
// Polish automatically selects "few" for 2-4, "many" for 5+
|
|
140
|
+
babelize.setLocale("pl");
|
|
141
|
+
babelize.p("{count} element", "{count} elementów", 2); // "2 elementy" (few)
|
|
142
|
+
babelize.p("{count} element", "{count} elementów", 5); // "5 elementów" (many)
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### `babelize.plural(forms, vars?)`
|
|
146
|
+
|
|
147
|
+
Explicit CLDR plural forms for full control. Supports all 6 CLDR forms: `zero`, `one`, `two`, `few`, `many`, `other`.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
babelize.plural(
|
|
151
|
+
{
|
|
152
|
+
one: "{count} item",
|
|
153
|
+
other: "{count} items",
|
|
154
|
+
},
|
|
155
|
+
{ count: 5 },
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// Arabic has 6 forms
|
|
159
|
+
babelize.setLocale("ar");
|
|
160
|
+
babelize.plural(
|
|
161
|
+
{
|
|
162
|
+
zero: "لا {count} عناصر",
|
|
163
|
+
one: "{count} عنصر",
|
|
164
|
+
two: "{count} عنصرين",
|
|
165
|
+
few: "{count} عناصر",
|
|
166
|
+
many: "عنصر {count}",
|
|
167
|
+
other: "{count} عنصرًا",
|
|
168
|
+
},
|
|
169
|
+
{ count: 0 }, // zero
|
|
170
|
+
);
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### `babelize.tag(template)`
|
|
174
|
+
|
|
175
|
+
Translate a template string with segment markers as a single unit. The full sentence is sent to the translator, so word order and grammar are correct across segments.
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
const result = babelize.tag("Welcome to {Babelize}, a platform {for localization}");
|
|
179
|
+
// result.parts = [
|
|
180
|
+
// { type: 'text', value: 'ローカリゼーションのための' },
|
|
181
|
+
// { type: 'segment', value: 'for localization' },
|
|
182
|
+
// { type: 'text', value: 'プラットフォーム、' },
|
|
183
|
+
// { type: 'segment', value: 'Babelize' },
|
|
184
|
+
// { type: 'text', value: 'へようこそ' },
|
|
185
|
+
// ]
|
|
186
|
+
|
|
187
|
+
// Map segments to React elements
|
|
188
|
+
const segments = {
|
|
189
|
+
Babelize: <strong>Babelize</strong>,
|
|
190
|
+
"for localization": <em>for localization</em>,
|
|
191
|
+
};
|
|
192
|
+
const rendered = result.parts.map(p =>
|
|
193
|
+
p.type === 'segment' ? segments[p.value] ?? p.value : p.value,
|
|
194
|
+
);
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### `babelize.loadLockfile(data)`
|
|
198
|
+
|
|
199
|
+
Load a pre-generated lockfile into the translation cache. The lockfile is generated at build time by the Vite plugin.
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
import lockfile from "virtual:babelize-lockfile";
|
|
203
|
+
lockfile && babelize.loadLockfile(lockfile);
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
### `babelize.subscribe(cb)`
|
|
207
|
+
|
|
208
|
+
Subscribe to state changes (locale switch, cache update). Returns an unsubscribe function.
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
const unsubscribe = babelize.subscribe(() => {
|
|
212
|
+
console.log("Locale or translation cache updated");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// Later
|
|
216
|
+
unsubscribe();
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
### `babelize.getVersion()`
|
|
220
|
+
|
|
221
|
+
Returns a number that increments on every state change. Useful with `useSyncExternalStore` for reactivity.
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
const version = babelize.getVersion();
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
### `babelize.reset()`
|
|
228
|
+
|
|
229
|
+
Clear all state — cache, locale, subscribers, API client. Useful in testing.
|
|
230
|
+
|
|
231
|
+
```ts
|
|
232
|
+
babelize.reset();
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
---
|
|
236
|
+
|
|
237
|
+
## Lockfile Format
|
|
238
|
+
|
|
239
|
+
The lockfile is generated at build time by `@babelize/vite` or `@babelize/next`. In production, translations are read from the lockfile — zero API calls.
|
|
240
|
+
|
|
241
|
+
```json
|
|
242
|
+
{
|
|
243
|
+
"version": 1,
|
|
244
|
+
"meta": {
|
|
245
|
+
"generatedAt": "2026-07-30T12:00:00Z",
|
|
246
|
+
"locales": ["ja", "fr", "de"],
|
|
247
|
+
"totalStrings": 42
|
|
248
|
+
},
|
|
249
|
+
"strings": {
|
|
250
|
+
"Welcome": {
|
|
251
|
+
"ja": "バビリゾーンへようこそ",
|
|
252
|
+
"fr": "Bienvenue à Babelize",
|
|
253
|
+
"de": "Willkommen bei Babelize"
|
|
254
|
+
},
|
|
255
|
+
"Hello {name}": {
|
|
256
|
+
"ja": "こんにちは {name}",
|
|
257
|
+
"fr": "Bonjour {name}",
|
|
258
|
+
"de": "Hallo {name}"
|
|
259
|
+
}
|
|
260
|
+
},
|
|
261
|
+
"plurals": {
|
|
262
|
+
"{count} item": {
|
|
263
|
+
"forms": ["one", "other"],
|
|
264
|
+
"ja": { "other": "{count} 項目" },
|
|
265
|
+
"fr": { "one": "{count} élément", "other": "{count} éléments" },
|
|
266
|
+
"de": { "one": "{count} Gegenstand", "other": "{count} Gegenstände" }
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
## Cache Strategy
|
|
275
|
+
|
|
276
|
+
```
|
|
277
|
+
Memory Cache < 0.1ms
|
|
278
|
+
↓ (miss)
|
|
279
|
+
Lockfile Cache < 1ms
|
|
280
|
+
↓ (miss)
|
|
281
|
+
Babelize API < 100ms
|
|
282
|
+
↓ (miss)
|
|
283
|
+
Original string (fallback)
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
- Memory cache is checked first for every `babelize()` call
|
|
287
|
+
- Lockfile cache is loaded at startup from the build-time lockfile
|
|
288
|
+
- API calls are batched via `queueMicrotask` — all strings from one render cycle are sent as a single request
|
|
289
|
+
- Once a translation is returned by the API, it's cached in memory and never fetched again
|
|
290
|
+
- Production builds read from lockfile only — zero API calls at runtime
|
|
291
|
+
|
|
292
|
+
---
|
|
293
|
+
|
|
294
|
+
## TypeScript
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import type { BabelizeConfig, LockfileData, PluralForms, TagResult, TagPart } from "@babelize/sdk";
|
|
298
|
+
|
|
299
|
+
interface BabelizeConfig {
|
|
300
|
+
apiKey?: string;
|
|
301
|
+
locale?: string;
|
|
302
|
+
fallbackLocale?: string;
|
|
303
|
+
apiUrl?: string;
|
|
304
|
+
storage?: TranslationStorage;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
interface LockfileData {
|
|
308
|
+
version: number;
|
|
309
|
+
strings: Record<string, Record<string, string>>;
|
|
310
|
+
plurals?: Record<string, {
|
|
311
|
+
forms: string[];
|
|
312
|
+
[locale: string]: Record<string, string> | string[];
|
|
313
|
+
}>;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
type PluralForms = {
|
|
317
|
+
zero?: string;
|
|
318
|
+
one?: string;
|
|
319
|
+
two?: string;
|
|
320
|
+
few?: string;
|
|
321
|
+
many?: string;
|
|
322
|
+
other: string;
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
interface TagResult {
|
|
326
|
+
parts: TagPart[];
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
interface TagPart {
|
|
330
|
+
type: "text" | "segment";
|
|
331
|
+
value: string;
|
|
332
|
+
}
|
|
333
|
+
```
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
babelize: () => babelize
|
|
24
|
+
});
|
|
25
|
+
module.exports = __toCommonJS(index_exports);
|
|
26
|
+
|
|
27
|
+
// src/storage/JsonStorage.ts
|
|
28
|
+
var JsonStorage = class {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.data = null;
|
|
31
|
+
}
|
|
32
|
+
load(data) {
|
|
33
|
+
this.data = data;
|
|
34
|
+
}
|
|
35
|
+
get(locale, key) {
|
|
36
|
+
if (!this.data) return null;
|
|
37
|
+
const translations = this.data.strings[key];
|
|
38
|
+
if (!translations) return null;
|
|
39
|
+
return translations[locale] ?? null;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/cache/Cache.ts
|
|
44
|
+
var Cache = class {
|
|
45
|
+
constructor() {
|
|
46
|
+
this.memory = /* @__PURE__ */ new Map();
|
|
47
|
+
this.pluralMemory = /* @__PURE__ */ new Map();
|
|
48
|
+
this.pluralLockfile = /* @__PURE__ */ new Map();
|
|
49
|
+
this.lockfile = new JsonStorage();
|
|
50
|
+
}
|
|
51
|
+
loadLockfile(data) {
|
|
52
|
+
this.lockfile.load(data);
|
|
53
|
+
if (data.plurals) {
|
|
54
|
+
for (const [canonical, localeData] of Object.entries(data.plurals)) {
|
|
55
|
+
for (const [locale, forms] of Object.entries(localeData)) {
|
|
56
|
+
if (locale === "forms") continue;
|
|
57
|
+
const formMap = forms;
|
|
58
|
+
let localePlurals = this.pluralLockfile.get(locale);
|
|
59
|
+
if (!localePlurals) {
|
|
60
|
+
localePlurals = /* @__PURE__ */ new Map();
|
|
61
|
+
this.pluralLockfile.set(locale, localePlurals);
|
|
62
|
+
}
|
|
63
|
+
localePlurals.set(canonical, formMap);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
get(locale, str) {
|
|
69
|
+
const localeCache = this.memory.get(locale);
|
|
70
|
+
if (localeCache?.has(str)) {
|
|
71
|
+
return localeCache.get(str);
|
|
72
|
+
}
|
|
73
|
+
const fromLockfile = this.lockfile.get(locale, str);
|
|
74
|
+
if (fromLockfile !== null) {
|
|
75
|
+
this.setMemory(locale, str, fromLockfile);
|
|
76
|
+
return fromLockfile;
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
set(locale, str, translation) {
|
|
81
|
+
this.setMemory(locale, str, translation);
|
|
82
|
+
}
|
|
83
|
+
getPlural(locale, key, form) {
|
|
84
|
+
const localePluralMemory = this.pluralMemory.get(locale);
|
|
85
|
+
if (localePluralMemory?.get(key)?.has(form)) {
|
|
86
|
+
return localePluralMemory.get(key).get(form);
|
|
87
|
+
}
|
|
88
|
+
const localePluralLockfile = this.pluralLockfile.get(locale);
|
|
89
|
+
if (localePluralLockfile?.has(key)) {
|
|
90
|
+
const forms = localePluralLockfile.get(key);
|
|
91
|
+
const translation = forms[form] ?? null;
|
|
92
|
+
if (translation !== null) {
|
|
93
|
+
let keyMemory = this.pluralMemory.get(locale);
|
|
94
|
+
if (!keyMemory) {
|
|
95
|
+
keyMemory = /* @__PURE__ */ new Map();
|
|
96
|
+
this.pluralMemory.set(locale, keyMemory);
|
|
97
|
+
}
|
|
98
|
+
let formMemory = keyMemory.get(key);
|
|
99
|
+
if (!formMemory) {
|
|
100
|
+
formMemory = /* @__PURE__ */ new Map();
|
|
101
|
+
keyMemory.set(key, formMemory);
|
|
102
|
+
}
|
|
103
|
+
formMemory.set(form, translation);
|
|
104
|
+
return translation;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
setPlural(locale, key, form, translation) {
|
|
110
|
+
let localeMemory = this.pluralMemory.get(locale);
|
|
111
|
+
if (!localeMemory) {
|
|
112
|
+
localeMemory = /* @__PURE__ */ new Map();
|
|
113
|
+
this.pluralMemory.set(locale, localeMemory);
|
|
114
|
+
}
|
|
115
|
+
let keyMemory = localeMemory.get(key);
|
|
116
|
+
if (!keyMemory) {
|
|
117
|
+
keyMemory = /* @__PURE__ */ new Map();
|
|
118
|
+
localeMemory.set(key, keyMemory);
|
|
119
|
+
}
|
|
120
|
+
keyMemory.set(form, translation);
|
|
121
|
+
}
|
|
122
|
+
hasPlural(locale, key, form) {
|
|
123
|
+
if (this.pluralMemory.get(locale)?.get(key)?.has(form)) return true;
|
|
124
|
+
const lockfileForm = this.pluralLockfile.get(locale)?.get(key)?.[form];
|
|
125
|
+
return lockfileForm != null;
|
|
126
|
+
}
|
|
127
|
+
setMemory(locale, str, translation) {
|
|
128
|
+
let localeCache = this.memory.get(locale);
|
|
129
|
+
if (!localeCache) {
|
|
130
|
+
localeCache = /* @__PURE__ */ new Map();
|
|
131
|
+
this.memory.set(locale, localeCache);
|
|
132
|
+
}
|
|
133
|
+
localeCache.set(str, translation);
|
|
134
|
+
}
|
|
135
|
+
has(locale, str) {
|
|
136
|
+
return this.memory.get(locale)?.has(str) ?? this.lockfile.get(locale, str) !== null;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
// src/api/client.ts
|
|
141
|
+
var DEFAULT_API_URL = "https://api.babelize.co/api";
|
|
142
|
+
var ApiClient = class {
|
|
143
|
+
constructor(apiKey, apiUrl = DEFAULT_API_URL) {
|
|
144
|
+
this.apiKey = apiKey;
|
|
145
|
+
this.apiUrl = apiUrl;
|
|
146
|
+
}
|
|
147
|
+
async translate(locale, strings, plurals) {
|
|
148
|
+
if (strings.length === 0 && !plurals?.length) return { translations: {} };
|
|
149
|
+
const response = await fetch(`${this.apiUrl}/v1/translate`, {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: {
|
|
152
|
+
"Content-Type": "application/json",
|
|
153
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
154
|
+
},
|
|
155
|
+
body: JSON.stringify({ locale, strings, plurals })
|
|
156
|
+
});
|
|
157
|
+
if (!response.ok) {
|
|
158
|
+
throw new Error(`Translation API error: ${response.status}`);
|
|
159
|
+
}
|
|
160
|
+
return response.json();
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
// src/batching/Batcher.ts
|
|
165
|
+
var Batcher = class {
|
|
166
|
+
constructor(flushFn) {
|
|
167
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
168
|
+
this.pendingPlurals = /* @__PURE__ */ new Map();
|
|
169
|
+
this.scheduled = false;
|
|
170
|
+
this.flushFn = flushFn;
|
|
171
|
+
}
|
|
172
|
+
add(locale, str) {
|
|
173
|
+
if (!this.pending.has(locale)) {
|
|
174
|
+
this.pending.set(locale, /* @__PURE__ */ new Set());
|
|
175
|
+
}
|
|
176
|
+
this.pending.get(locale).add(str);
|
|
177
|
+
if (!this.scheduled) {
|
|
178
|
+
this.scheduled = true;
|
|
179
|
+
queueMicrotask(() => this.flush());
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
addPlural(locale, canonical, forms, templates) {
|
|
183
|
+
if (!this.pendingPlurals.has(locale)) {
|
|
184
|
+
this.pendingPlurals.set(locale, []);
|
|
185
|
+
}
|
|
186
|
+
this.pendingPlurals.get(locale).push({ canonical, forms, templates });
|
|
187
|
+
if (!this.scheduled) {
|
|
188
|
+
this.scheduled = true;
|
|
189
|
+
queueMicrotask(() => this.flush());
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
async flush() {
|
|
193
|
+
this.scheduled = false;
|
|
194
|
+
const batch = this.pending;
|
|
195
|
+
const batchPlurals = this.pendingPlurals;
|
|
196
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
197
|
+
this.pendingPlurals = /* @__PURE__ */ new Map();
|
|
198
|
+
const promises = [];
|
|
199
|
+
for (const [locale, strings] of batch) {
|
|
200
|
+
const plurals = batchPlurals.get(locale);
|
|
201
|
+
if (strings.size === 0 && !plurals?.length) continue;
|
|
202
|
+
promises.push(this.flushFn(locale, [...strings], plurals));
|
|
203
|
+
}
|
|
204
|
+
for (const [locale, plurals] of batchPlurals) {
|
|
205
|
+
if (batch.has(locale)) continue;
|
|
206
|
+
if (!plurals.length) continue;
|
|
207
|
+
promises.push(this.flushFn(locale, [], plurals));
|
|
208
|
+
}
|
|
209
|
+
await Promise.all(promises);
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// src/locale/detect.ts
|
|
214
|
+
function browserDetector() {
|
|
215
|
+
if (typeof navigator === "undefined") return null;
|
|
216
|
+
return navigator.language?.split("-")[0] ?? null;
|
|
217
|
+
}
|
|
218
|
+
function createDetectionChain(detectors) {
|
|
219
|
+
return () => {
|
|
220
|
+
for (const detector of detectors) {
|
|
221
|
+
const result = detector();
|
|
222
|
+
if (result !== null) return result;
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/utils/interpolate.ts
|
|
229
|
+
function interpolate(template, vars) {
|
|
230
|
+
if (!vars) return template;
|
|
231
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
232
|
+
const value = vars[key];
|
|
233
|
+
return value !== void 0 ? String(value) : `{${key}}`;
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// src/Babelize.ts
|
|
238
|
+
var DEFAULT_FALLBACK_LOCALE = "en";
|
|
239
|
+
var DEFAULT_API_URL2 = "https://api.babelize.co/api";
|
|
240
|
+
var Babelize = class {
|
|
241
|
+
constructor() {
|
|
242
|
+
this.initialized = false;
|
|
243
|
+
this.apiKey = "";
|
|
244
|
+
this.apiUrl = DEFAULT_API_URL2;
|
|
245
|
+
this.apiClient = null;
|
|
246
|
+
this.batcher = null;
|
|
247
|
+
this._locale = DEFAULT_FALLBACK_LOCALE;
|
|
248
|
+
this.fallbackLocale = DEFAULT_FALLBACK_LOCALE;
|
|
249
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
250
|
+
this._version = 0;
|
|
251
|
+
this.cache = new Cache();
|
|
252
|
+
this.detectLocale = createDetectionChain([browserDetector]);
|
|
253
|
+
this.batcher = new Batcher(async (locale, strings, plurals) => {
|
|
254
|
+
await this.flushBatch(locale, strings, plurals);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
init(config) {
|
|
258
|
+
if (this.initialized) return;
|
|
259
|
+
if (typeof config === "string") {
|
|
260
|
+
config = { apiKey: config };
|
|
261
|
+
}
|
|
262
|
+
const resolved = config ?? {};
|
|
263
|
+
this.apiKey = resolved.apiKey ?? "";
|
|
264
|
+
this.apiUrl = resolved.apiUrl ?? DEFAULT_API_URL2;
|
|
265
|
+
this.fallbackLocale = resolved.fallbackLocale ?? DEFAULT_FALLBACK_LOCALE;
|
|
266
|
+
if (resolved.locale) {
|
|
267
|
+
this._locale = resolved.locale;
|
|
268
|
+
} else {
|
|
269
|
+
const detected = this.detectLocale();
|
|
270
|
+
this._locale = detected ?? this.fallbackLocale;
|
|
271
|
+
}
|
|
272
|
+
if (this.apiKey) {
|
|
273
|
+
this.apiClient = new ApiClient(this.apiKey, this.apiUrl);
|
|
274
|
+
}
|
|
275
|
+
this.initialized = true;
|
|
276
|
+
}
|
|
277
|
+
reset() {
|
|
278
|
+
this.initialized = false;
|
|
279
|
+
this.apiKey = "";
|
|
280
|
+
this.apiClient = null;
|
|
281
|
+
this.cache = new Cache();
|
|
282
|
+
this._locale = DEFAULT_FALLBACK_LOCALE;
|
|
283
|
+
this.fallbackLocale = DEFAULT_FALLBACK_LOCALE;
|
|
284
|
+
this.subscribers.clear();
|
|
285
|
+
}
|
|
286
|
+
loadLockfile(data) {
|
|
287
|
+
this.cache.loadLockfile(data);
|
|
288
|
+
}
|
|
289
|
+
t(str, vars) {
|
|
290
|
+
const cached = this.cache.get(this._locale, str);
|
|
291
|
+
if (cached !== null) {
|
|
292
|
+
return interpolate(cached, vars);
|
|
293
|
+
}
|
|
294
|
+
if (this.apiClient && this.batcher) {
|
|
295
|
+
this.batcher.add(this._locale, str);
|
|
296
|
+
}
|
|
297
|
+
return interpolate(str, vars);
|
|
298
|
+
}
|
|
299
|
+
p(singular, plural, countOrVars) {
|
|
300
|
+
let count;
|
|
301
|
+
let vars;
|
|
302
|
+
if (typeof countOrVars === "number") {
|
|
303
|
+
count = countOrVars;
|
|
304
|
+
vars = { count };
|
|
305
|
+
} else {
|
|
306
|
+
count = countOrVars.count ?? 0;
|
|
307
|
+
vars = countOrVars;
|
|
308
|
+
}
|
|
309
|
+
const rules = new Intl.PluralRules(this._locale);
|
|
310
|
+
const form = rules.select(count);
|
|
311
|
+
const key = singular;
|
|
312
|
+
const cached = this.cache.getPlural(this._locale, key, form);
|
|
313
|
+
if (cached !== null) {
|
|
314
|
+
return interpolate(cached, vars);
|
|
315
|
+
}
|
|
316
|
+
if (this.apiClient && this.batcher) {
|
|
317
|
+
this.batcher.addPlural(this._locale, key, ["one", "other"], [singular, plural]);
|
|
318
|
+
}
|
|
319
|
+
const sourceForm = form === "one" ? singular : plural;
|
|
320
|
+
return interpolate(sourceForm, vars);
|
|
321
|
+
}
|
|
322
|
+
plural(forms, vars) {
|
|
323
|
+
const count = vars?.count ?? 0;
|
|
324
|
+
const rules = new Intl.PluralRules(this._locale);
|
|
325
|
+
const form = rules.select(count);
|
|
326
|
+
const key = forms.other;
|
|
327
|
+
const cached = this.cache.getPlural(this._locale, key, form);
|
|
328
|
+
if (cached !== null) {
|
|
329
|
+
return interpolate(cached, vars);
|
|
330
|
+
}
|
|
331
|
+
if (this.apiClient && this.batcher) {
|
|
332
|
+
const entries = Object.entries(forms);
|
|
333
|
+
const formNames = entries.map(([f]) => f);
|
|
334
|
+
const templates = entries.map(([, t]) => t);
|
|
335
|
+
this.batcher.addPlural(this._locale, key, formNames, templates);
|
|
336
|
+
}
|
|
337
|
+
const sourceForm = forms[form] ?? forms.other;
|
|
338
|
+
return interpolate(sourceForm, vars);
|
|
339
|
+
}
|
|
340
|
+
tag(template) {
|
|
341
|
+
const translated = this.t(template);
|
|
342
|
+
const parts = [];
|
|
343
|
+
const markerRegex = /\{([^}]+)\}/g;
|
|
344
|
+
let lastIndex = 0;
|
|
345
|
+
let match;
|
|
346
|
+
while ((match = markerRegex.exec(translated)) !== null) {
|
|
347
|
+
if (match.index > lastIndex) {
|
|
348
|
+
parts.push({ type: "text", value: translated.slice(lastIndex, match.index) });
|
|
349
|
+
}
|
|
350
|
+
parts.push({ type: "segment", value: match[1] });
|
|
351
|
+
lastIndex = match.index + match[0].length;
|
|
352
|
+
}
|
|
353
|
+
if (lastIndex < translated.length) {
|
|
354
|
+
parts.push({ type: "text", value: translated.slice(lastIndex) });
|
|
355
|
+
}
|
|
356
|
+
return { parts };
|
|
357
|
+
}
|
|
358
|
+
getVersion() {
|
|
359
|
+
return this._version;
|
|
360
|
+
}
|
|
361
|
+
setLocale(locale) {
|
|
362
|
+
if (locale === this._locale) return;
|
|
363
|
+
this._locale = locale;
|
|
364
|
+
this._version++;
|
|
365
|
+
this.subscribers.forEach((cb) => cb());
|
|
366
|
+
}
|
|
367
|
+
getLocale() {
|
|
368
|
+
return this._locale;
|
|
369
|
+
}
|
|
370
|
+
subscribe(cb) {
|
|
371
|
+
this.subscribers.add(cb);
|
|
372
|
+
return () => {
|
|
373
|
+
this.subscribers.delete(cb);
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
async flushBatch(locale, strings, plurals) {
|
|
377
|
+
if (!this.apiClient) return;
|
|
378
|
+
if (strings.length === 0 && !plurals?.length) return;
|
|
379
|
+
const uncached = strings.filter((s) => !this.cache.has(locale, s));
|
|
380
|
+
if (uncached.length === 0 && !plurals?.length) return;
|
|
381
|
+
try {
|
|
382
|
+
const { translations, plurals: pluralTranslations } = await this.apiClient.translate(
|
|
383
|
+
locale,
|
|
384
|
+
uncached,
|
|
385
|
+
plurals
|
|
386
|
+
);
|
|
387
|
+
let updated = false;
|
|
388
|
+
for (const [source, translation] of Object.entries(translations)) {
|
|
389
|
+
if (translation && translation !== source) {
|
|
390
|
+
this.cache.set(locale, source, translation);
|
|
391
|
+
updated = true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
if (pluralTranslations) {
|
|
395
|
+
for (const [canonical, forms] of Object.entries(pluralTranslations)) {
|
|
396
|
+
for (const [form, translation] of Object.entries(forms)) {
|
|
397
|
+
if (translation) {
|
|
398
|
+
this.cache.setPlural(locale, canonical, form, translation);
|
|
399
|
+
updated = true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
if (updated) {
|
|
405
|
+
this._version++;
|
|
406
|
+
this.subscribers.forEach((cb) => cb());
|
|
407
|
+
}
|
|
408
|
+
} catch {
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
// src/index.ts
|
|
414
|
+
var _babelize = new Babelize();
|
|
415
|
+
function babelize(str, vars) {
|
|
416
|
+
return _babelize.t(str, vars);
|
|
417
|
+
}
|
|
418
|
+
babelize.init = (config) => {
|
|
419
|
+
_babelize.init(config);
|
|
420
|
+
};
|
|
421
|
+
babelize.reset = () => {
|
|
422
|
+
_babelize.reset();
|
|
423
|
+
};
|
|
424
|
+
babelize.setLocale = (locale) => {
|
|
425
|
+
_babelize.setLocale(locale);
|
|
426
|
+
};
|
|
427
|
+
babelize.getLocale = () => {
|
|
428
|
+
return _babelize.getLocale();
|
|
429
|
+
};
|
|
430
|
+
babelize.loadLockfile = (data) => {
|
|
431
|
+
_babelize.loadLockfile(data);
|
|
432
|
+
};
|
|
433
|
+
babelize.subscribe = (cb) => {
|
|
434
|
+
return _babelize.subscribe(cb);
|
|
435
|
+
};
|
|
436
|
+
babelize.getVersion = () => {
|
|
437
|
+
return _babelize.getVersion();
|
|
438
|
+
};
|
|
439
|
+
babelize.tag = (template) => {
|
|
440
|
+
return _babelize.tag(template);
|
|
441
|
+
};
|
|
442
|
+
babelize.p = (singular, plural, countOrVars) => {
|
|
443
|
+
return _babelize.p(singular, plural, countOrVars);
|
|
444
|
+
};
|
|
445
|
+
babelize.plural = (forms, vars) => {
|
|
446
|
+
return _babelize.plural(forms, vars);
|
|
447
|
+
};
|
|
448
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
449
|
+
0 && (module.exports = {
|
|
450
|
+
babelize
|
|
451
|
+
});
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface BabelizeConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
locale?: string;
|
|
4
|
+
fallbackLocale?: string;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
storage?: TranslationStorage;
|
|
7
|
+
}
|
|
8
|
+
interface TranslationStorage {
|
|
9
|
+
get(locale: string, key: string): string | null;
|
|
10
|
+
}
|
|
11
|
+
type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
12
|
+
type PluralForms = {
|
|
13
|
+
[form in PluralForm]?: string;
|
|
14
|
+
} & {
|
|
15
|
+
other: string;
|
|
16
|
+
};
|
|
17
|
+
interface LockfileData {
|
|
18
|
+
version: number;
|
|
19
|
+
strings: Record<string, Record<string, string>>;
|
|
20
|
+
plurals?: Record<string, {
|
|
21
|
+
forms: string[];
|
|
22
|
+
[locale: string]: Record<string, string> | string[];
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
interface TagPart {
|
|
26
|
+
type: 'text' | 'segment';
|
|
27
|
+
value: string;
|
|
28
|
+
}
|
|
29
|
+
interface TagResult {
|
|
30
|
+
parts: TagPart[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare function babelize(str: string, vars?: Record<string, string | number>): string;
|
|
34
|
+
declare namespace babelize {
|
|
35
|
+
var init: (config?: string | BabelizeConfig) => void;
|
|
36
|
+
var reset: () => void;
|
|
37
|
+
var setLocale: (locale: string) => void;
|
|
38
|
+
var getLocale: () => string;
|
|
39
|
+
var loadLockfile: (data: LockfileData) => void;
|
|
40
|
+
var subscribe: (cb: () => void) => (() => void);
|
|
41
|
+
var getVersion: () => number;
|
|
42
|
+
var tag: (template: string) => TagResult;
|
|
43
|
+
var p: (singular: string, plural: string, countOrVars: number | Record<string, string | number>) => string;
|
|
44
|
+
var plural: (forms: PluralForms, vars?: Record<string, string | number>) => string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { type BabelizeConfig, type LockfileData, type PluralForm, type PluralForms, type TagPart, type TagResult, babelize };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
interface BabelizeConfig {
|
|
2
|
+
apiKey?: string;
|
|
3
|
+
locale?: string;
|
|
4
|
+
fallbackLocale?: string;
|
|
5
|
+
apiUrl?: string;
|
|
6
|
+
storage?: TranslationStorage;
|
|
7
|
+
}
|
|
8
|
+
interface TranslationStorage {
|
|
9
|
+
get(locale: string, key: string): string | null;
|
|
10
|
+
}
|
|
11
|
+
type PluralForm = 'zero' | 'one' | 'two' | 'few' | 'many' | 'other';
|
|
12
|
+
type PluralForms = {
|
|
13
|
+
[form in PluralForm]?: string;
|
|
14
|
+
} & {
|
|
15
|
+
other: string;
|
|
16
|
+
};
|
|
17
|
+
interface LockfileData {
|
|
18
|
+
version: number;
|
|
19
|
+
strings: Record<string, Record<string, string>>;
|
|
20
|
+
plurals?: Record<string, {
|
|
21
|
+
forms: string[];
|
|
22
|
+
[locale: string]: Record<string, string> | string[];
|
|
23
|
+
}>;
|
|
24
|
+
}
|
|
25
|
+
interface TagPart {
|
|
26
|
+
type: 'text' | 'segment';
|
|
27
|
+
value: string;
|
|
28
|
+
}
|
|
29
|
+
interface TagResult {
|
|
30
|
+
parts: TagPart[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
declare function babelize(str: string, vars?: Record<string, string | number>): string;
|
|
34
|
+
declare namespace babelize {
|
|
35
|
+
var init: (config?: string | BabelizeConfig) => void;
|
|
36
|
+
var reset: () => void;
|
|
37
|
+
var setLocale: (locale: string) => void;
|
|
38
|
+
var getLocale: () => string;
|
|
39
|
+
var loadLockfile: (data: LockfileData) => void;
|
|
40
|
+
var subscribe: (cb: () => void) => (() => void);
|
|
41
|
+
var getVersion: () => number;
|
|
42
|
+
var tag: (template: string) => TagResult;
|
|
43
|
+
var p: (singular: string, plural: string, countOrVars: number | Record<string, string | number>) => string;
|
|
44
|
+
var plural: (forms: PluralForms, vars?: Record<string, string | number>) => string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export { type BabelizeConfig, type LockfileData, type PluralForm, type PluralForms, type TagPart, type TagResult, babelize };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
// src/storage/JsonStorage.ts
|
|
2
|
+
var JsonStorage = class {
|
|
3
|
+
constructor() {
|
|
4
|
+
this.data = null;
|
|
5
|
+
}
|
|
6
|
+
load(data) {
|
|
7
|
+
this.data = data;
|
|
8
|
+
}
|
|
9
|
+
get(locale, key) {
|
|
10
|
+
if (!this.data) return null;
|
|
11
|
+
const translations = this.data.strings[key];
|
|
12
|
+
if (!translations) return null;
|
|
13
|
+
return translations[locale] ?? null;
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// src/cache/Cache.ts
|
|
18
|
+
var Cache = class {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.memory = /* @__PURE__ */ new Map();
|
|
21
|
+
this.pluralMemory = /* @__PURE__ */ new Map();
|
|
22
|
+
this.pluralLockfile = /* @__PURE__ */ new Map();
|
|
23
|
+
this.lockfile = new JsonStorage();
|
|
24
|
+
}
|
|
25
|
+
loadLockfile(data) {
|
|
26
|
+
this.lockfile.load(data);
|
|
27
|
+
if (data.plurals) {
|
|
28
|
+
for (const [canonical, localeData] of Object.entries(data.plurals)) {
|
|
29
|
+
for (const [locale, forms] of Object.entries(localeData)) {
|
|
30
|
+
if (locale === "forms") continue;
|
|
31
|
+
const formMap = forms;
|
|
32
|
+
let localePlurals = this.pluralLockfile.get(locale);
|
|
33
|
+
if (!localePlurals) {
|
|
34
|
+
localePlurals = /* @__PURE__ */ new Map();
|
|
35
|
+
this.pluralLockfile.set(locale, localePlurals);
|
|
36
|
+
}
|
|
37
|
+
localePlurals.set(canonical, formMap);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
get(locale, str) {
|
|
43
|
+
const localeCache = this.memory.get(locale);
|
|
44
|
+
if (localeCache?.has(str)) {
|
|
45
|
+
return localeCache.get(str);
|
|
46
|
+
}
|
|
47
|
+
const fromLockfile = this.lockfile.get(locale, str);
|
|
48
|
+
if (fromLockfile !== null) {
|
|
49
|
+
this.setMemory(locale, str, fromLockfile);
|
|
50
|
+
return fromLockfile;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
set(locale, str, translation) {
|
|
55
|
+
this.setMemory(locale, str, translation);
|
|
56
|
+
}
|
|
57
|
+
getPlural(locale, key, form) {
|
|
58
|
+
const localePluralMemory = this.pluralMemory.get(locale);
|
|
59
|
+
if (localePluralMemory?.get(key)?.has(form)) {
|
|
60
|
+
return localePluralMemory.get(key).get(form);
|
|
61
|
+
}
|
|
62
|
+
const localePluralLockfile = this.pluralLockfile.get(locale);
|
|
63
|
+
if (localePluralLockfile?.has(key)) {
|
|
64
|
+
const forms = localePluralLockfile.get(key);
|
|
65
|
+
const translation = forms[form] ?? null;
|
|
66
|
+
if (translation !== null) {
|
|
67
|
+
let keyMemory = this.pluralMemory.get(locale);
|
|
68
|
+
if (!keyMemory) {
|
|
69
|
+
keyMemory = /* @__PURE__ */ new Map();
|
|
70
|
+
this.pluralMemory.set(locale, keyMemory);
|
|
71
|
+
}
|
|
72
|
+
let formMemory = keyMemory.get(key);
|
|
73
|
+
if (!formMemory) {
|
|
74
|
+
formMemory = /* @__PURE__ */ new Map();
|
|
75
|
+
keyMemory.set(key, formMemory);
|
|
76
|
+
}
|
|
77
|
+
formMemory.set(form, translation);
|
|
78
|
+
return translation;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
setPlural(locale, key, form, translation) {
|
|
84
|
+
let localeMemory = this.pluralMemory.get(locale);
|
|
85
|
+
if (!localeMemory) {
|
|
86
|
+
localeMemory = /* @__PURE__ */ new Map();
|
|
87
|
+
this.pluralMemory.set(locale, localeMemory);
|
|
88
|
+
}
|
|
89
|
+
let keyMemory = localeMemory.get(key);
|
|
90
|
+
if (!keyMemory) {
|
|
91
|
+
keyMemory = /* @__PURE__ */ new Map();
|
|
92
|
+
localeMemory.set(key, keyMemory);
|
|
93
|
+
}
|
|
94
|
+
keyMemory.set(form, translation);
|
|
95
|
+
}
|
|
96
|
+
hasPlural(locale, key, form) {
|
|
97
|
+
if (this.pluralMemory.get(locale)?.get(key)?.has(form)) return true;
|
|
98
|
+
const lockfileForm = this.pluralLockfile.get(locale)?.get(key)?.[form];
|
|
99
|
+
return lockfileForm != null;
|
|
100
|
+
}
|
|
101
|
+
setMemory(locale, str, translation) {
|
|
102
|
+
let localeCache = this.memory.get(locale);
|
|
103
|
+
if (!localeCache) {
|
|
104
|
+
localeCache = /* @__PURE__ */ new Map();
|
|
105
|
+
this.memory.set(locale, localeCache);
|
|
106
|
+
}
|
|
107
|
+
localeCache.set(str, translation);
|
|
108
|
+
}
|
|
109
|
+
has(locale, str) {
|
|
110
|
+
return this.memory.get(locale)?.has(str) ?? this.lockfile.get(locale, str) !== null;
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
// src/api/client.ts
|
|
115
|
+
var DEFAULT_API_URL = "https://api.babelize.co/api";
|
|
116
|
+
var ApiClient = class {
|
|
117
|
+
constructor(apiKey, apiUrl = DEFAULT_API_URL) {
|
|
118
|
+
this.apiKey = apiKey;
|
|
119
|
+
this.apiUrl = apiUrl;
|
|
120
|
+
}
|
|
121
|
+
async translate(locale, strings, plurals) {
|
|
122
|
+
if (strings.length === 0 && !plurals?.length) return { translations: {} };
|
|
123
|
+
const response = await fetch(`${this.apiUrl}/v1/translate`, {
|
|
124
|
+
method: "POST",
|
|
125
|
+
headers: {
|
|
126
|
+
"Content-Type": "application/json",
|
|
127
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
128
|
+
},
|
|
129
|
+
body: JSON.stringify({ locale, strings, plurals })
|
|
130
|
+
});
|
|
131
|
+
if (!response.ok) {
|
|
132
|
+
throw new Error(`Translation API error: ${response.status}`);
|
|
133
|
+
}
|
|
134
|
+
return response.json();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/batching/Batcher.ts
|
|
139
|
+
var Batcher = class {
|
|
140
|
+
constructor(flushFn) {
|
|
141
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
142
|
+
this.pendingPlurals = /* @__PURE__ */ new Map();
|
|
143
|
+
this.scheduled = false;
|
|
144
|
+
this.flushFn = flushFn;
|
|
145
|
+
}
|
|
146
|
+
add(locale, str) {
|
|
147
|
+
if (!this.pending.has(locale)) {
|
|
148
|
+
this.pending.set(locale, /* @__PURE__ */ new Set());
|
|
149
|
+
}
|
|
150
|
+
this.pending.get(locale).add(str);
|
|
151
|
+
if (!this.scheduled) {
|
|
152
|
+
this.scheduled = true;
|
|
153
|
+
queueMicrotask(() => this.flush());
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
addPlural(locale, canonical, forms, templates) {
|
|
157
|
+
if (!this.pendingPlurals.has(locale)) {
|
|
158
|
+
this.pendingPlurals.set(locale, []);
|
|
159
|
+
}
|
|
160
|
+
this.pendingPlurals.get(locale).push({ canonical, forms, templates });
|
|
161
|
+
if (!this.scheduled) {
|
|
162
|
+
this.scheduled = true;
|
|
163
|
+
queueMicrotask(() => this.flush());
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async flush() {
|
|
167
|
+
this.scheduled = false;
|
|
168
|
+
const batch = this.pending;
|
|
169
|
+
const batchPlurals = this.pendingPlurals;
|
|
170
|
+
this.pending = /* @__PURE__ */ new Map();
|
|
171
|
+
this.pendingPlurals = /* @__PURE__ */ new Map();
|
|
172
|
+
const promises = [];
|
|
173
|
+
for (const [locale, strings] of batch) {
|
|
174
|
+
const plurals = batchPlurals.get(locale);
|
|
175
|
+
if (strings.size === 0 && !plurals?.length) continue;
|
|
176
|
+
promises.push(this.flushFn(locale, [...strings], plurals));
|
|
177
|
+
}
|
|
178
|
+
for (const [locale, plurals] of batchPlurals) {
|
|
179
|
+
if (batch.has(locale)) continue;
|
|
180
|
+
if (!plurals.length) continue;
|
|
181
|
+
promises.push(this.flushFn(locale, [], plurals));
|
|
182
|
+
}
|
|
183
|
+
await Promise.all(promises);
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
// src/locale/detect.ts
|
|
188
|
+
function browserDetector() {
|
|
189
|
+
if (typeof navigator === "undefined") return null;
|
|
190
|
+
return navigator.language?.split("-")[0] ?? null;
|
|
191
|
+
}
|
|
192
|
+
function createDetectionChain(detectors) {
|
|
193
|
+
return () => {
|
|
194
|
+
for (const detector of detectors) {
|
|
195
|
+
const result = detector();
|
|
196
|
+
if (result !== null) return result;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// src/utils/interpolate.ts
|
|
203
|
+
function interpolate(template, vars) {
|
|
204
|
+
if (!vars) return template;
|
|
205
|
+
return template.replace(/\{(\w+)\}/g, (_, key) => {
|
|
206
|
+
const value = vars[key];
|
|
207
|
+
return value !== void 0 ? String(value) : `{${key}}`;
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// src/Babelize.ts
|
|
212
|
+
var DEFAULT_FALLBACK_LOCALE = "en";
|
|
213
|
+
var DEFAULT_API_URL2 = "https://api.babelize.co/api";
|
|
214
|
+
var Babelize = class {
|
|
215
|
+
constructor() {
|
|
216
|
+
this.initialized = false;
|
|
217
|
+
this.apiKey = "";
|
|
218
|
+
this.apiUrl = DEFAULT_API_URL2;
|
|
219
|
+
this.apiClient = null;
|
|
220
|
+
this.batcher = null;
|
|
221
|
+
this._locale = DEFAULT_FALLBACK_LOCALE;
|
|
222
|
+
this.fallbackLocale = DEFAULT_FALLBACK_LOCALE;
|
|
223
|
+
this.subscribers = /* @__PURE__ */ new Set();
|
|
224
|
+
this._version = 0;
|
|
225
|
+
this.cache = new Cache();
|
|
226
|
+
this.detectLocale = createDetectionChain([browserDetector]);
|
|
227
|
+
this.batcher = new Batcher(async (locale, strings, plurals) => {
|
|
228
|
+
await this.flushBatch(locale, strings, plurals);
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
init(config) {
|
|
232
|
+
if (this.initialized) return;
|
|
233
|
+
if (typeof config === "string") {
|
|
234
|
+
config = { apiKey: config };
|
|
235
|
+
}
|
|
236
|
+
const resolved = config ?? {};
|
|
237
|
+
this.apiKey = resolved.apiKey ?? "";
|
|
238
|
+
this.apiUrl = resolved.apiUrl ?? DEFAULT_API_URL2;
|
|
239
|
+
this.fallbackLocale = resolved.fallbackLocale ?? DEFAULT_FALLBACK_LOCALE;
|
|
240
|
+
if (resolved.locale) {
|
|
241
|
+
this._locale = resolved.locale;
|
|
242
|
+
} else {
|
|
243
|
+
const detected = this.detectLocale();
|
|
244
|
+
this._locale = detected ?? this.fallbackLocale;
|
|
245
|
+
}
|
|
246
|
+
if (this.apiKey) {
|
|
247
|
+
this.apiClient = new ApiClient(this.apiKey, this.apiUrl);
|
|
248
|
+
}
|
|
249
|
+
this.initialized = true;
|
|
250
|
+
}
|
|
251
|
+
reset() {
|
|
252
|
+
this.initialized = false;
|
|
253
|
+
this.apiKey = "";
|
|
254
|
+
this.apiClient = null;
|
|
255
|
+
this.cache = new Cache();
|
|
256
|
+
this._locale = DEFAULT_FALLBACK_LOCALE;
|
|
257
|
+
this.fallbackLocale = DEFAULT_FALLBACK_LOCALE;
|
|
258
|
+
this.subscribers.clear();
|
|
259
|
+
}
|
|
260
|
+
loadLockfile(data) {
|
|
261
|
+
this.cache.loadLockfile(data);
|
|
262
|
+
}
|
|
263
|
+
t(str, vars) {
|
|
264
|
+
const cached = this.cache.get(this._locale, str);
|
|
265
|
+
if (cached !== null) {
|
|
266
|
+
return interpolate(cached, vars);
|
|
267
|
+
}
|
|
268
|
+
if (this.apiClient && this.batcher) {
|
|
269
|
+
this.batcher.add(this._locale, str);
|
|
270
|
+
}
|
|
271
|
+
return interpolate(str, vars);
|
|
272
|
+
}
|
|
273
|
+
p(singular, plural, countOrVars) {
|
|
274
|
+
let count;
|
|
275
|
+
let vars;
|
|
276
|
+
if (typeof countOrVars === "number") {
|
|
277
|
+
count = countOrVars;
|
|
278
|
+
vars = { count };
|
|
279
|
+
} else {
|
|
280
|
+
count = countOrVars.count ?? 0;
|
|
281
|
+
vars = countOrVars;
|
|
282
|
+
}
|
|
283
|
+
const rules = new Intl.PluralRules(this._locale);
|
|
284
|
+
const form = rules.select(count);
|
|
285
|
+
const key = singular;
|
|
286
|
+
const cached = this.cache.getPlural(this._locale, key, form);
|
|
287
|
+
if (cached !== null) {
|
|
288
|
+
return interpolate(cached, vars);
|
|
289
|
+
}
|
|
290
|
+
if (this.apiClient && this.batcher) {
|
|
291
|
+
this.batcher.addPlural(this._locale, key, ["one", "other"], [singular, plural]);
|
|
292
|
+
}
|
|
293
|
+
const sourceForm = form === "one" ? singular : plural;
|
|
294
|
+
return interpolate(sourceForm, vars);
|
|
295
|
+
}
|
|
296
|
+
plural(forms, vars) {
|
|
297
|
+
const count = vars?.count ?? 0;
|
|
298
|
+
const rules = new Intl.PluralRules(this._locale);
|
|
299
|
+
const form = rules.select(count);
|
|
300
|
+
const key = forms.other;
|
|
301
|
+
const cached = this.cache.getPlural(this._locale, key, form);
|
|
302
|
+
if (cached !== null) {
|
|
303
|
+
return interpolate(cached, vars);
|
|
304
|
+
}
|
|
305
|
+
if (this.apiClient && this.batcher) {
|
|
306
|
+
const entries = Object.entries(forms);
|
|
307
|
+
const formNames = entries.map(([f]) => f);
|
|
308
|
+
const templates = entries.map(([, t]) => t);
|
|
309
|
+
this.batcher.addPlural(this._locale, key, formNames, templates);
|
|
310
|
+
}
|
|
311
|
+
const sourceForm = forms[form] ?? forms.other;
|
|
312
|
+
return interpolate(sourceForm, vars);
|
|
313
|
+
}
|
|
314
|
+
tag(template) {
|
|
315
|
+
const translated = this.t(template);
|
|
316
|
+
const parts = [];
|
|
317
|
+
const markerRegex = /\{([^}]+)\}/g;
|
|
318
|
+
let lastIndex = 0;
|
|
319
|
+
let match;
|
|
320
|
+
while ((match = markerRegex.exec(translated)) !== null) {
|
|
321
|
+
if (match.index > lastIndex) {
|
|
322
|
+
parts.push({ type: "text", value: translated.slice(lastIndex, match.index) });
|
|
323
|
+
}
|
|
324
|
+
parts.push({ type: "segment", value: match[1] });
|
|
325
|
+
lastIndex = match.index + match[0].length;
|
|
326
|
+
}
|
|
327
|
+
if (lastIndex < translated.length) {
|
|
328
|
+
parts.push({ type: "text", value: translated.slice(lastIndex) });
|
|
329
|
+
}
|
|
330
|
+
return { parts };
|
|
331
|
+
}
|
|
332
|
+
getVersion() {
|
|
333
|
+
return this._version;
|
|
334
|
+
}
|
|
335
|
+
setLocale(locale) {
|
|
336
|
+
if (locale === this._locale) return;
|
|
337
|
+
this._locale = locale;
|
|
338
|
+
this._version++;
|
|
339
|
+
this.subscribers.forEach((cb) => cb());
|
|
340
|
+
}
|
|
341
|
+
getLocale() {
|
|
342
|
+
return this._locale;
|
|
343
|
+
}
|
|
344
|
+
subscribe(cb) {
|
|
345
|
+
this.subscribers.add(cb);
|
|
346
|
+
return () => {
|
|
347
|
+
this.subscribers.delete(cb);
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async flushBatch(locale, strings, plurals) {
|
|
351
|
+
if (!this.apiClient) return;
|
|
352
|
+
if (strings.length === 0 && !plurals?.length) return;
|
|
353
|
+
const uncached = strings.filter((s) => !this.cache.has(locale, s));
|
|
354
|
+
if (uncached.length === 0 && !plurals?.length) return;
|
|
355
|
+
try {
|
|
356
|
+
const { translations, plurals: pluralTranslations } = await this.apiClient.translate(
|
|
357
|
+
locale,
|
|
358
|
+
uncached,
|
|
359
|
+
plurals
|
|
360
|
+
);
|
|
361
|
+
let updated = false;
|
|
362
|
+
for (const [source, translation] of Object.entries(translations)) {
|
|
363
|
+
if (translation && translation !== source) {
|
|
364
|
+
this.cache.set(locale, source, translation);
|
|
365
|
+
updated = true;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
if (pluralTranslations) {
|
|
369
|
+
for (const [canonical, forms] of Object.entries(pluralTranslations)) {
|
|
370
|
+
for (const [form, translation] of Object.entries(forms)) {
|
|
371
|
+
if (translation) {
|
|
372
|
+
this.cache.setPlural(locale, canonical, form, translation);
|
|
373
|
+
updated = true;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
if (updated) {
|
|
379
|
+
this._version++;
|
|
380
|
+
this.subscribers.forEach((cb) => cb());
|
|
381
|
+
}
|
|
382
|
+
} catch {
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
// src/index.ts
|
|
388
|
+
var _babelize = new Babelize();
|
|
389
|
+
function babelize(str, vars) {
|
|
390
|
+
return _babelize.t(str, vars);
|
|
391
|
+
}
|
|
392
|
+
babelize.init = (config) => {
|
|
393
|
+
_babelize.init(config);
|
|
394
|
+
};
|
|
395
|
+
babelize.reset = () => {
|
|
396
|
+
_babelize.reset();
|
|
397
|
+
};
|
|
398
|
+
babelize.setLocale = (locale) => {
|
|
399
|
+
_babelize.setLocale(locale);
|
|
400
|
+
};
|
|
401
|
+
babelize.getLocale = () => {
|
|
402
|
+
return _babelize.getLocale();
|
|
403
|
+
};
|
|
404
|
+
babelize.loadLockfile = (data) => {
|
|
405
|
+
_babelize.loadLockfile(data);
|
|
406
|
+
};
|
|
407
|
+
babelize.subscribe = (cb) => {
|
|
408
|
+
return _babelize.subscribe(cb);
|
|
409
|
+
};
|
|
410
|
+
babelize.getVersion = () => {
|
|
411
|
+
return _babelize.getVersion();
|
|
412
|
+
};
|
|
413
|
+
babelize.tag = (template) => {
|
|
414
|
+
return _babelize.tag(template);
|
|
415
|
+
};
|
|
416
|
+
babelize.p = (singular, plural, countOrVars) => {
|
|
417
|
+
return _babelize.p(singular, plural, countOrVars);
|
|
418
|
+
};
|
|
419
|
+
babelize.plural = (forms, vars) => {
|
|
420
|
+
return _babelize.plural(forms, vars);
|
|
421
|
+
};
|
|
422
|
+
export {
|
|
423
|
+
babelize
|
|
424
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@babelize/sdk",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "The simplest way to localize your app — wrap strings, Babelize handles the rest",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js",
|
|
13
|
+
"require": "./dist/index.cjs"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist"],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"build": "tsup src/index.ts --format esm,cjs --dts --clean",
|
|
22
|
+
"dev": "tsup src/index.ts --format esm,cjs --dts --watch",
|
|
23
|
+
"typecheck": "tsc --noEmit",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"lint": "eslint src --ext .ts"
|
|
26
|
+
},
|
|
27
|
+
"dependencies": {},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"tsup": "^8.4.0",
|
|
30
|
+
"typescript": "^5.7.3",
|
|
31
|
+
"vitest": "^3.1.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=18"
|
|
35
|
+
},
|
|
36
|
+
"keywords": ["babelize", "i18n", "localization", "translation", "l10n"]
|
|
37
|
+
}
|