@babelize/vite 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 ADDED
@@ -0,0 +1,215 @@
1
+ # @babelize/vite
2
+
3
+ **Vite plugin for Babelize** — automatically discover strings, translate them, and generate a lockfile at build time. Zero configuration required.
4
+
5
+ ```ts
6
+ // vite.config.ts
7
+ import babelize from "@babelize/vite";
8
+
9
+ export default defineConfig({
10
+ plugins: [
11
+ react(),
12
+ babelize({
13
+ locales: ["ja", "fr", "de"],
14
+ }),
15
+ ],
16
+ });
17
+ ```
18
+
19
+ ---
20
+
21
+ ## Install
22
+
23
+ ```bash
24
+ bun add @babelize/vite -D
25
+ ```
26
+
27
+ ---
28
+
29
+ ## How It Works
30
+
31
+ At build time, the plugin:
32
+
33
+ 1. **Scans** every source file for `babelize("...")`, `babelize.p(...)`, and `<BabelizeTag>` calls using AST analysis
34
+ 2. **Extracts** all unique strings and plural templates
35
+ 3. **Translates** them via the Babelize API for each configured locale
36
+ 4. **Generates** `babelize-lock.json` with all translations
37
+
38
+ At runtime, the lockfile is served via a virtual module `virtual:babelize-lockfile` so the SDK can load it without any manual import path configuration.
39
+
40
+ ---
41
+
42
+ ## Quick Start
43
+
44
+ ### 1. Configure the plugin
45
+
46
+ ```ts
47
+ // vite.config.ts
48
+ import babelize from "@babelize/vite";
49
+
50
+ export default defineConfig({
51
+ plugins: [
52
+ react(),
53
+ babelize({
54
+ locales: ["ja", "fr", "de"],
55
+ }),
56
+ ],
57
+ });
58
+ ```
59
+
60
+ ### 2. Set your API key
61
+
62
+ ```env
63
+ # .env
64
+ BABELIZE_API_KEY=blz_sk_xxxxxxxxx
65
+ ```
66
+
67
+ ### 3. Use the lockfile in your app
68
+
69
+ ```tsx
70
+ // src/main.tsx
71
+ import { babelize } from "@babelize/sdk";
72
+ import lockfile from "virtual:babelize-lockfile";
73
+
74
+ lockfile && babelize.loadLockfile(lockfile);
75
+ babelize.init(import.meta.env.VITE_BABELIZE_API_KEY);
76
+ ```
77
+
78
+ ### 4. Build
79
+
80
+ ```bash
81
+ bun run build
82
+ ```
83
+
84
+ The lockfile is automatically generated at `babelize-lock.json` in your project root.
85
+
86
+ ---
87
+
88
+ ## Options
89
+
90
+ ```ts
91
+ interface BabelizePluginOptions {
92
+ apiKey?: string;
93
+ locales?: string[];
94
+ output?: string;
95
+ functionName?: string;
96
+ apiUrl?: string;
97
+ mock?: boolean;
98
+ }
99
+ ```
100
+
101
+ | Option | Type | Default | Description |
102
+ |--------|------|---------|-------------|
103
+ | `locales` | `string[]` | — | Target locales to translate to (e.g. `["ja", "fr"]`) |
104
+ | `apiKey` | `string` | `process.env.BABELIZE_API_KEY` | Babelize API key |
105
+ | `functionName` | `string` | `"babelize"` | Function name to scan for |
106
+ | `output` | `string` | `"babelize-lock.json"` | Output path for the lockfile |
107
+ | `apiUrl` | `string` | `"https://api.babelize.co/api"` | API base URL |
108
+ | `mock` | `boolean` | `false` | Generate fake translations (`"Welcome [ja]"`) for testing without API key |
109
+
110
+ ---
111
+
112
+ ## What Gets Scanned
113
+
114
+ The plugin scans these patterns in your source files:
115
+
116
+ ### `babelize("static string")`
117
+
118
+ ```tsx
119
+ babelize("Welcome") // ✓ extracted
120
+ babelize("Hello {name}", { name }) // ✓ extracted (variables stripped)
121
+ babelize(dynamicVariable) // ✗ skipped (can't statically analyze)
122
+ ```
123
+
124
+ ### `babelize.p(singular, plural, count)`
125
+
126
+ ```tsx
127
+ babelize.p("{count} item", "{count} items", items.length)
128
+ // ✓ extracted as plural with forms ["one", "other"]
129
+ ```
130
+
131
+ ### `babelize.plural({...}, vars)`
132
+
133
+ ```tsx
134
+ babelize.plural({ one: "1 item", other: "{count} items" }, { count })
135
+ // ✓ extracted as plural with explicit forms
136
+ ```
137
+
138
+ ### `<BabelizeTag template="..." />`
139
+
140
+ ```tsx
141
+ <BabelizeTag
142
+ template="Welcome to {Babelize}, a platform {for localization}"
143
+ segments={{ Babelize: <strong>Babelize</strong> }}
144
+ />
145
+ // ✓ template extracted as a single translation unit
146
+ ```
147
+
148
+ ---
149
+
150
+ ## Lockfile Output
151
+
152
+ The plugin generates `babelize-lock.json` in your project root:
153
+
154
+ ```json
155
+ {
156
+ "version": 1,
157
+ "meta": {
158
+ "generatedAt": "2026-07-30T12:00:00Z",
159
+ "locales": ["ja", "fr", "de"],
160
+ "totalStrings": 42
161
+ },
162
+ "strings": {
163
+ "Welcome": {
164
+ "ja": "ようこそ",
165
+ "fr": "Bienvenue"
166
+ },
167
+ "Hello {name}": {
168
+ "ja": "こんにちは {name}",
169
+ "fr": "Bonjour {name}"
170
+ }
171
+ },
172
+ "plurals": {
173
+ "{count} item": {
174
+ "forms": ["one", "other"],
175
+ "ja": { "other": "{count} 項目" },
176
+ "fr": { "one": "{count} élément", "other": "{count} éléments" }
177
+ }
178
+ }
179
+ }
180
+ ```
181
+
182
+ ---
183
+
184
+ ## Virtual Module
185
+
186
+ The plugin provides a virtual module `virtual:babelize-lockfile` that serves the current lockfile content. If a lockfile exists from a previous build, its data is used. If not, an empty lockfile is returned.
187
+
188
+ ```ts
189
+ // Type declaration for the virtual module
190
+ declare module "virtual:babelize-lockfile" {
191
+ const data: {
192
+ version: number;
193
+ strings: Record<string, Record<string, string>>;
194
+ plurals?: Record<string, any>;
195
+ };
196
+ export default data;
197
+ }
198
+ ```
199
+
200
+ ---
201
+
202
+ ## TypeScript
203
+
204
+ ```ts
205
+ import type { BabelizePluginOptions } from "@babelize/vite";
206
+
207
+ interface BabelizePluginOptions {
208
+ apiKey?: string;
209
+ locales?: string[];
210
+ output?: string;
211
+ functionName?: string;
212
+ apiUrl?: string;
213
+ mock?: boolean;
214
+ }
215
+ ```
@@ -0,0 +1,13 @@
1
+ import { Plugin } from 'vite';
2
+
3
+ interface BabelizePluginOptions {
4
+ apiKey?: string;
5
+ locales?: string[];
6
+ output?: string;
7
+ functionName?: string;
8
+ apiUrl?: string;
9
+ mock?: boolean;
10
+ }
11
+ declare function babelize(options?: BabelizePluginOptions): Plugin;
12
+
13
+ export { type BabelizePluginOptions, babelize, babelize as default };
package/dist/index.js ADDED
@@ -0,0 +1,311 @@
1
+ // src/index.ts
2
+ import { readFileSync, writeFileSync, existsSync, readdirSync, statSync } from "fs";
3
+ import { resolve, extname } from "path";
4
+
5
+ // src/scanner.ts
6
+ import { createRequire } from "module";
7
+ import { parse } from "@babel/parser";
8
+ var _require = createRequire(import.meta.url);
9
+ var _traverse = _require("@babel/traverse");
10
+ var traverse = _traverse.default?.default ?? _traverse.default ?? _traverse;
11
+ var DEFAULT_FUNCTION_NAME = "babelize";
12
+ var PLURAL_FORMS = ["zero", "one", "two", "few", "many", "other"];
13
+ function extractLiteralArg(node) {
14
+ if (!node) return null;
15
+ if (node.type === "StringLiteral") return node.value;
16
+ if (node.type === "TemplateLiteral") {
17
+ if (node.expressions.length > 0) {
18
+ let pattern = "";
19
+ for (let i = 0; i < node.quasis.length; i++) {
20
+ pattern += node.quasis[i].value.cooked ?? "";
21
+ if (i < node.expressions.length) {
22
+ const expr = node.expressions[i];
23
+ if (expr.type === "Identifier") {
24
+ pattern += `{${expr.name}}`;
25
+ } else {
26
+ pattern += "{*}";
27
+ }
28
+ }
29
+ }
30
+ return pattern;
31
+ }
32
+ return node.quasis[0]?.value?.cooked ?? null;
33
+ }
34
+ return null;
35
+ }
36
+ function extractStrings(source, options) {
37
+ const fnName = options?.functionName ?? DEFAULT_FUNCTION_NAME;
38
+ const strings = [];
39
+ let ast;
40
+ try {
41
+ ast = parse(source, {
42
+ sourceType: "module",
43
+ plugins: ["typescript", "jsx", "decorators-legacy"]
44
+ });
45
+ } catch {
46
+ return strings;
47
+ }
48
+ traverse(ast, {
49
+ CallExpression(path) {
50
+ const callee = path.node.callee;
51
+ if (callee.type === "Identifier" && callee.name === fnName) {
52
+ const arg = path.node.arguments[0];
53
+ const val = extractLiteralArg(arg);
54
+ if (val) strings.push(val);
55
+ return;
56
+ }
57
+ },
58
+ JSXOpeningElement(path) {
59
+ const name = path.node.name;
60
+ if (name.type !== "JSXIdentifier" || name.name !== "BabelizeTag") return;
61
+ const templateAttr = path.node.attributes.find(
62
+ (attr) => attr.type === "JSXAttribute" && attr.name?.name === "template"
63
+ );
64
+ if (!templateAttr?.value) return;
65
+ const extractLiteral = (node) => {
66
+ if (node.type === "StringLiteral") return node.value;
67
+ if (node.type === "TemplateLiteral") {
68
+ if (node.expressions.length > 0) return null;
69
+ return node.quasis[0]?.value?.cooked ?? null;
70
+ }
71
+ return null;
72
+ };
73
+ const value = templateAttr.value.type === "JSXExpressionContainer" ? extractLiteral(templateAttr.value.expression) : extractLiteral(templateAttr.value);
74
+ if (value) strings.push(value);
75
+ }
76
+ });
77
+ return strings;
78
+ }
79
+ function extractPlurals(source, options) {
80
+ const fnName = options?.functionName ?? DEFAULT_FUNCTION_NAME;
81
+ const plurals = [];
82
+ let ast;
83
+ try {
84
+ ast = parse(source, {
85
+ sourceType: "module",
86
+ plugins: ["typescript", "jsx", "decorators-legacy"]
87
+ });
88
+ } catch {
89
+ return plurals;
90
+ }
91
+ traverse(ast, {
92
+ CallExpression(path) {
93
+ const callee = path.node.callee;
94
+ if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === fnName && callee.property.type === "Identifier" && (callee.property.name === "p" || callee.property.name === "plural")) {
95
+ const singular = extractLiteralArg(path.node.arguments[0]);
96
+ const plural = extractLiteralArg(path.node.arguments[1]);
97
+ if (singular && plural) {
98
+ plurals.push({
99
+ canonical: singular,
100
+ forms: ["one", "other"],
101
+ templates: [singular, plural]
102
+ });
103
+ }
104
+ }
105
+ if (callee.type === "MemberExpression" && callee.object.type === "Identifier" && callee.object.name === fnName && callee.property.type === "Identifier" && callee.property.name === "plural") {
106
+ const formsArg = path.node.arguments[0];
107
+ if (formsArg?.type !== "ObjectExpression") return;
108
+ const formNames = [];
109
+ const templates = [];
110
+ for (const prop of formsArg.properties) {
111
+ if (prop.type !== "ObjectProperty") continue;
112
+ const key = prop.key.type === "Identifier" ? prop.key.name : prop.key.type === "StringLiteral" ? prop.key.value : null;
113
+ if (!key || !PLURAL_FORMS.includes(key)) continue;
114
+ const val = extractLiteralArg(prop.value);
115
+ if (val) {
116
+ formNames.push(key);
117
+ templates.push(val);
118
+ }
119
+ }
120
+ const otherIdx = formNames.indexOf("other");
121
+ const canonical = otherIdx >= 0 ? templates[otherIdx] : templates[0];
122
+ if (canonical) {
123
+ plurals.push({ canonical, forms: formNames, templates });
124
+ }
125
+ }
126
+ }
127
+ });
128
+ return plurals;
129
+ }
130
+
131
+ // src/lockfile.ts
132
+ function createLockfile(strings, translationsByLocale, plurals, pluralTranslationsByLocale) {
133
+ const locales = Object.keys(translationsByLocale);
134
+ const uniqueStrings = [...new Set(strings)];
135
+ const stringsMap = {};
136
+ for (const str of uniqueStrings) {
137
+ stringsMap[str] = {};
138
+ for (const locale of locales) {
139
+ stringsMap[str][locale] = translationsByLocale[locale]?.[str] ?? str;
140
+ }
141
+ }
142
+ const result = {
143
+ version: 1,
144
+ meta: {
145
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
146
+ locales,
147
+ totalStrings: uniqueStrings.length
148
+ },
149
+ strings: stringsMap
150
+ };
151
+ if (plurals && plurals.length > 0) {
152
+ const pluralsMap = {};
153
+ for (const plural of plurals) {
154
+ pluralsMap[plural.canonical] = { forms: plural.forms };
155
+ for (const locale of locales) {
156
+ const localeTranslations = pluralTranslationsByLocale?.[locale]?.[plural.canonical];
157
+ if (localeTranslations) {
158
+ pluralsMap[plural.canonical][locale] = {};
159
+ for (const form of plural.forms) {
160
+ pluralsMap[plural.canonical][locale][form] = localeTranslations[form] ?? plural.templates[plural.forms.indexOf(form)];
161
+ }
162
+ } else {
163
+ pluralsMap[plural.canonical][locale] = {};
164
+ for (let i = 0; i < plural.forms.length; i++) {
165
+ pluralsMap[plural.canonical][locale][plural.forms[i]] = plural.templates[i];
166
+ }
167
+ }
168
+ }
169
+ }
170
+ result.plurals = pluralsMap;
171
+ }
172
+ return result;
173
+ }
174
+
175
+ // src/index.ts
176
+ var DEFAULT_OUTPUT = "babelize-lock.json";
177
+ var DEFAULT_API_URL = "https://api.babelize.co/api";
178
+ var VALID_EXTS = /* @__PURE__ */ new Set([".tsx", ".ts", ".jsx", ".js"]);
179
+ var VIRTUAL_MODULE_ID = "virtual:babelize-lockfile";
180
+ var RESOLVED_VIRTUAL_MODULE_ID = "\0" + VIRTUAL_MODULE_ID;
181
+ function collectSourceFiles(dir) {
182
+ const files = [];
183
+ try {
184
+ const entries = readdirSync(dir);
185
+ for (const entry of entries) {
186
+ if (entry === "node_modules" || entry === "dist" || entry === ".git") continue;
187
+ const fullPath = resolve(dir, entry);
188
+ if (statSync(fullPath).isDirectory()) {
189
+ files.push(...collectSourceFiles(fullPath));
190
+ } else if (VALID_EXTS.has(extname(entry))) {
191
+ files.push(fullPath);
192
+ }
193
+ }
194
+ } catch {
195
+ }
196
+ return files;
197
+ }
198
+ function babelize(options) {
199
+ const mock = options?.mock ?? false;
200
+ const apiKey = options?.apiKey ?? process.env.BABELIZE_API_KEY;
201
+ const locales = options?.locales;
202
+ const output = options?.output ?? DEFAULT_OUTPUT;
203
+ const fnName = options?.functionName ?? "babelize";
204
+ const apiUrl = options?.apiUrl ?? DEFAULT_API_URL;
205
+ let config;
206
+ function generateMockTranslations(strings, locales2) {
207
+ const result = {};
208
+ for (const locale of locales2) {
209
+ result[locale] = {};
210
+ for (const str of strings) {
211
+ result[locale][str] = `${str} [${locale}]`;
212
+ }
213
+ }
214
+ return result;
215
+ }
216
+ return {
217
+ name: "babelize",
218
+ resolveId(id) {
219
+ if (id === VIRTUAL_MODULE_ID) {
220
+ return RESOLVED_VIRTUAL_MODULE_ID;
221
+ }
222
+ },
223
+ load(id) {
224
+ if (id !== RESOLVED_VIRTUAL_MODULE_ID) return;
225
+ const outputPath = resolve(config.root, output);
226
+ let lockfile = { version: 1, strings: {} };
227
+ if (existsSync(outputPath)) {
228
+ try {
229
+ lockfile = JSON.parse(readFileSync(outputPath, "utf-8"));
230
+ } catch {
231
+ }
232
+ }
233
+ return `export default ${JSON.stringify(lockfile)};`;
234
+ },
235
+ configResolved(resolvedConfig) {
236
+ config = resolvedConfig;
237
+ },
238
+ async closeBundle() {
239
+ const srcDir = resolve(config.root, "src");
240
+ if (!existsSync(srcDir)) return;
241
+ const sourceFiles = collectSourceFiles(srcDir);
242
+ if (sourceFiles.length === 0) return;
243
+ const allStrings = [];
244
+ const allPlurals = [];
245
+ for (const file of sourceFiles) {
246
+ try {
247
+ const code = readFileSync(file, "utf-8");
248
+ const strings = extractStrings(code, { functionName: fnName });
249
+ allStrings.push(...strings);
250
+ const plurals = extractPlurals(code, { functionName: fnName });
251
+ allPlurals.push(...plurals);
252
+ } catch {
253
+ }
254
+ }
255
+ if (allStrings.length === 0 && allPlurals.length === 0) return;
256
+ const uniqueStrings = [...new Set(allStrings)];
257
+ const uniquePlurals = allPlurals.filter(
258
+ (p, i, arr) => arr.findIndex((x) => x.canonical === p.canonical) === i
259
+ );
260
+ const translationsByLocale = {};
261
+ const pluralTranslationsByLocale = {};
262
+ if (locales && locales.length > 0) {
263
+ if (mock) {
264
+ Object.assign(translationsByLocale, generateMockTranslations(uniqueStrings, locales));
265
+ } else if (apiKey) {
266
+ for (const locale of locales) {
267
+ try {
268
+ const response = await fetch(`${apiUrl}/v1/translate`, {
269
+ method: "POST",
270
+ headers: {
271
+ "Content-Type": "application/json",
272
+ Authorization: `Bearer ${apiKey}`
273
+ },
274
+ body: JSON.stringify({
275
+ locale,
276
+ strings: uniqueStrings,
277
+ plurals: uniquePlurals.map((p) => ({
278
+ canonical: p.canonical,
279
+ forms: p.forms,
280
+ templates: p.templates
281
+ }))
282
+ })
283
+ });
284
+ if (response.ok) {
285
+ const data = await response.json();
286
+ translationsByLocale[locale] = data.translations;
287
+ if (data.plurals) {
288
+ pluralTranslationsByLocale[locale] = data.plurals;
289
+ }
290
+ }
291
+ } catch {
292
+ }
293
+ }
294
+ }
295
+ }
296
+ const lockfile = createLockfile(
297
+ uniqueStrings,
298
+ translationsByLocale,
299
+ uniquePlurals,
300
+ pluralTranslationsByLocale
301
+ );
302
+ const outputPath = resolve(config.root, output);
303
+ writeFileSync(outputPath, JSON.stringify(lockfile, null, 2));
304
+ }
305
+ };
306
+ }
307
+ var index_default = babelize;
308
+ export {
309
+ babelize,
310
+ index_default as default
311
+ };
package/package.json ADDED
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "@babelize/vite",
3
+ "version": "0.0.1",
4
+ "description": "Vite plugin for Babelize — auto-discover strings and generate translation lockfile at build time",
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 --dts --clean --external vite --external @babel/parser --external @babel/traverse",
22
+ "dev": "tsup src/index.ts --format esm --dts --watch --external vite --external @babel/parser --external @babel/traverse",
23
+ "typecheck": "tsc --noEmit"
24
+ },
25
+ "peerDependencies": {
26
+ "vite": "^5.x || ^6.x"
27
+ },
28
+ "dependencies": {
29
+ "@babel/parser": "^7.28.5",
30
+ "@babel/traverse": "^7.28.5"
31
+ },
32
+ "devDependencies": {
33
+ "@types/babel__traverse": "^7.28.0",
34
+ "tsup": "^8.4.0",
35
+ "typescript": "^5.7.3",
36
+ "vite": "^6.x"
37
+ }
38
+ }