@vocoder/cli 0.1.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 +305 -0
- package/dist/bin.d.mts +1 -0
- package/dist/bin.mjs +12 -0
- package/dist/bin.mjs.map +1 -0
- package/dist/chunk-N45Q4R6O.mjs +635 -0
- package/dist/chunk-N45Q4R6O.mjs.map +1 -0
- package/dist/index.d.mts +97 -0
- package/dist/index.mjs +13 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +69 -0
package/README.md
ADDED
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
# @vocoder/cli
|
|
2
|
+
|
|
3
|
+
CLI tool for the Vocoder translation workflow. Extract translatable strings from your React code and get them translated automatically.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -D @vocoder/cli
|
|
9
|
+
# or
|
|
10
|
+
pnpm add -D @vocoder/cli
|
|
11
|
+
# or
|
|
12
|
+
yarn add -D @vocoder/cli
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
1. **Set up environment variables** (create `.env` in your project root):
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
VOCODER_API_KEY=your-api-key-here
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
2. **Add to your build script**:
|
|
24
|
+
|
|
25
|
+
```json
|
|
26
|
+
{
|
|
27
|
+
"scripts": {
|
|
28
|
+
"prebuild": "vocoder sync",
|
|
29
|
+
"build": "next build"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
3. **Use `<T>` components in your code**:
|
|
35
|
+
|
|
36
|
+
```tsx
|
|
37
|
+
import { T } from '@vocoder/react';
|
|
38
|
+
|
|
39
|
+
function MyComponent() {
|
|
40
|
+
return (
|
|
41
|
+
<div>
|
|
42
|
+
<T>Welcome to our app!</T>
|
|
43
|
+
<T name={userName}>Hello, {name}!</T>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
4. **Run the CLI**:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npx vocoder sync
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
This will:
|
|
56
|
+
- Extract all `<T>` components from your code
|
|
57
|
+
- Submit them to Vocoder for translation
|
|
58
|
+
- Download translations to `.vocoder/locales/*.json`
|
|
59
|
+
- Only translate NEW strings (incremental updates are fast!)
|
|
60
|
+
|
|
61
|
+
## Configuration
|
|
62
|
+
|
|
63
|
+
The CLI uses environment variables for configuration:
|
|
64
|
+
|
|
65
|
+
### Required
|
|
66
|
+
|
|
67
|
+
- **`VOCODER_API_KEY`**: Your Vocoder API key (get from https://vocoder.dev)
|
|
68
|
+
|
|
69
|
+
### Optional (Development Only)
|
|
70
|
+
|
|
71
|
+
- **`VOCODER_API_URL`**: Override API endpoint (defaults to `https://api.vocoder.dev`)
|
|
72
|
+
|
|
73
|
+
### Defaults (Not Configurable)
|
|
74
|
+
|
|
75
|
+
- **Target locales**: `es`, `fr`, `de`
|
|
76
|
+
- **Extraction pattern**: `src/**/*.{tsx,jsx,ts,js}`
|
|
77
|
+
- **Output directory**: `.vocoder/locales`
|
|
78
|
+
- **Target branches**: `main`, `master`, `production`, `staging`
|
|
79
|
+
|
|
80
|
+
To customize these defaults, configure them in your Vocoder dashboard.
|
|
81
|
+
|
|
82
|
+
## Commands
|
|
83
|
+
|
|
84
|
+
### `vocoder sync`
|
|
85
|
+
|
|
86
|
+
Extract and translate strings.
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
npx vocoder sync [options]
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
**Options:**
|
|
93
|
+
|
|
94
|
+
- `--branch <name>` - Specify branch name (auto-detected from git)
|
|
95
|
+
- `--force` - Translate even if not on a target branch
|
|
96
|
+
- `--dry-run` - Show what would be translated without making API calls
|
|
97
|
+
- `--verbose` - Show detailed output
|
|
98
|
+
|
|
99
|
+
**Examples:**
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
# Normal usage (auto-detects branch from git)
|
|
103
|
+
npx vocoder sync
|
|
104
|
+
|
|
105
|
+
# Specify branch manually
|
|
106
|
+
npx vocoder sync --branch feature/new-ui
|
|
107
|
+
|
|
108
|
+
# See what would be translated without making API calls
|
|
109
|
+
npx vocoder sync --dry-run
|
|
110
|
+
|
|
111
|
+
# Force translation even if not on a target branch
|
|
112
|
+
npx vocoder sync --force
|
|
113
|
+
|
|
114
|
+
# Verbose output for debugging
|
|
115
|
+
npx vocoder sync --verbose
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Workflow
|
|
119
|
+
|
|
120
|
+
### First Run (100 strings)
|
|
121
|
+
```bash
|
|
122
|
+
$ npx vocoder sync
|
|
123
|
+
✓ Detected branch: main
|
|
124
|
+
✓ Loaded config for project: abc123
|
|
125
|
+
✓ Extracted 100 strings from src/**/*.{tsx,jsx,ts,js}
|
|
126
|
+
✓ Submitted to API - Batch ID: batch-xyz
|
|
127
|
+
Found 100 new strings to translate
|
|
128
|
+
⏳ Translating to 3 locales (es, fr, de)
|
|
129
|
+
Estimated time: ~30 seconds
|
|
130
|
+
✓ Translations complete!
|
|
131
|
+
✓ Wrote 3 locale files
|
|
132
|
+
|
|
133
|
+
✅ Translation complete! (32.4s)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Second Run (Same strings, 0 new)
|
|
137
|
+
```bash
|
|
138
|
+
$ npx vocoder sync
|
|
139
|
+
✓ Detected branch: main
|
|
140
|
+
✓ Loaded config for project: abc123
|
|
141
|
+
✓ Extracted 100 strings from src/**/*.{tsx,jsx,ts,js}
|
|
142
|
+
✓ Submitted to API - Batch ID: batch-abc
|
|
143
|
+
Found 0 new strings to translate
|
|
144
|
+
|
|
145
|
+
✅ No new strings - using existing translations
|
|
146
|
+
✓ Wrote 3 locale files
|
|
147
|
+
|
|
148
|
+
✅ Translation complete! (0.8s)
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### Incremental Run (1 new string)
|
|
152
|
+
```bash
|
|
153
|
+
$ npx vocoder sync
|
|
154
|
+
✓ Detected branch: main
|
|
155
|
+
✓ Loaded config for project: abc123
|
|
156
|
+
✓ Extracted 101 strings from src/**/*.{tsx,jsx,ts,js}
|
|
157
|
+
✓ Submitted to API - Batch ID: batch-def
|
|
158
|
+
Found 1 new strings to translate
|
|
159
|
+
⏳ Translating to 3 locales (es, fr, de)
|
|
160
|
+
Estimated time: ~1 seconds
|
|
161
|
+
✓ Translations complete!
|
|
162
|
+
✓ Wrote 3 locale files
|
|
163
|
+
|
|
164
|
+
✅ Translation complete! (1.2s)
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Branch-Scoped Translations
|
|
168
|
+
|
|
169
|
+
Translations are isolated per git branch:
|
|
170
|
+
|
|
171
|
+
- **Main branch** translations are shared across the team
|
|
172
|
+
- **Feature branches** get their own translations
|
|
173
|
+
- Feature branches fall back to main branch translations
|
|
174
|
+
- Merge to main to promote feature translations
|
|
175
|
+
|
|
176
|
+
This allows you to:
|
|
177
|
+
- Test translations in feature branches
|
|
178
|
+
- Preview translations before merging
|
|
179
|
+
- Avoid conflicts between features
|
|
180
|
+
|
|
181
|
+
## Performance
|
|
182
|
+
|
|
183
|
+
The CLI is optimized for incremental updates:
|
|
184
|
+
|
|
185
|
+
| Scenario | Time | Cost |
|
|
186
|
+
|----------|------|------|
|
|
187
|
+
| 100 new strings | ~30s | 100 strings × 3 locales |
|
|
188
|
+
| 0 new strings | <1s | No API calls |
|
|
189
|
+
| 1 new string | ~1s | 1 string × 3 locales |
|
|
190
|
+
|
|
191
|
+
**Speedup: 30x faster** for incremental updates!
|
|
192
|
+
|
|
193
|
+
## Output
|
|
194
|
+
|
|
195
|
+
Translations are written to `.vocoder/locales/`:
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
.vocoder/
|
|
199
|
+
└── locales/
|
|
200
|
+
├── es.json
|
|
201
|
+
├── fr.json
|
|
202
|
+
└── de.json
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Each file contains a flat key-value mapping:
|
|
206
|
+
|
|
207
|
+
```json
|
|
208
|
+
{
|
|
209
|
+
"Welcome to our app!": "¡Bienvenido a nuestra aplicación!",
|
|
210
|
+
"Hello, {name}!": "¡Hola, {name}!",
|
|
211
|
+
"You have {count} messages": "Tienes {count} mensajes"
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Add to `.gitignore`:**
|
|
216
|
+
|
|
217
|
+
```
|
|
218
|
+
.vocoder/
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Translations are generated at build time, not checked into git.
|
|
222
|
+
|
|
223
|
+
## Integration with React
|
|
224
|
+
|
|
225
|
+
Use the generated locale files with `@vocoder/react`:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import { VocoderProvider } from '@vocoder/react';
|
|
229
|
+
import en from './.vocoder/locales/en.json';
|
|
230
|
+
import es from './.vocoder/locales/es.json';
|
|
231
|
+
import fr from './.vocoder/locales/fr.json';
|
|
232
|
+
|
|
233
|
+
export default function App({ children }) {
|
|
234
|
+
return (
|
|
235
|
+
<VocoderProvider
|
|
236
|
+
translations={{ en, es, fr }}
|
|
237
|
+
defaultLocale="en"
|
|
238
|
+
>
|
|
239
|
+
{children}
|
|
240
|
+
</VocoderProvider>
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
## Troubleshooting
|
|
246
|
+
|
|
247
|
+
### "VOCODER_API_KEY is required"
|
|
248
|
+
|
|
249
|
+
Create a `.env` file in your project root:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
VOCODER_API_KEY=your-api-key
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
### "No translatable strings found"
|
|
256
|
+
|
|
257
|
+
Make sure you're using `<T>` components from `@vocoder/react`:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import { T } from '@vocoder/react';
|
|
261
|
+
|
|
262
|
+
// ✅ Good
|
|
263
|
+
<T>Welcome!</T>
|
|
264
|
+
|
|
265
|
+
// ❌ Bad (not detected)
|
|
266
|
+
<span>Welcome!</span>
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### "Not a git repository"
|
|
270
|
+
|
|
271
|
+
The CLI auto-detects the branch from git. Either:
|
|
272
|
+
- Initialize git: `git init`
|
|
273
|
+
- Specify branch manually: `vocoder sync --branch main`
|
|
274
|
+
|
|
275
|
+
### "Skipping translations (not a target branch)"
|
|
276
|
+
|
|
277
|
+
The CLI only runs on target branches (`main`, `master`, `production`, `staging`) by default. Either:
|
|
278
|
+
- Merge to a target branch
|
|
279
|
+
- Use `--force` flag: `vocoder sync --force`
|
|
280
|
+
|
|
281
|
+
## Development
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
# Install dependencies
|
|
285
|
+
pnpm install
|
|
286
|
+
|
|
287
|
+
# Build
|
|
288
|
+
pnpm build
|
|
289
|
+
|
|
290
|
+
# Run tests
|
|
291
|
+
pnpm test
|
|
292
|
+
|
|
293
|
+
# Run unit tests only (fast)
|
|
294
|
+
pnpm test:unit
|
|
295
|
+
|
|
296
|
+
# Run integration tests (requires API)
|
|
297
|
+
pnpm test:integration
|
|
298
|
+
|
|
299
|
+
# Watch mode
|
|
300
|
+
pnpm test:watch
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## License
|
|
304
|
+
|
|
305
|
+
MIT
|
package/dist/bin.d.mts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
package/dist/bin.mjs
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
sync
|
|
4
|
+
} from "./chunk-N45Q4R6O.mjs";
|
|
5
|
+
|
|
6
|
+
// src/bin.ts
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
var program = new Command();
|
|
9
|
+
program.name("vocoder").description("Vocoder CLI - Sync translations for your application").version("0.1.0");
|
|
10
|
+
program.command("sync").description("Extract strings and sync translations").option("--branch <name>", "Override branch detection").option("--force", "Sync even if not a target branch").option("--dry-run", "Show what would be synced without doing it").option("--verbose", "Show detailed progress").option("--max-age <seconds>", "Use cache if younger than this (seconds)", parseInt).action(sync);
|
|
11
|
+
program.parse(process.argv);
|
|
12
|
+
//# sourceMappingURL=bin.mjs.map
|
package/dist/bin.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { Command } from 'commander';\nimport { sync } from './commands/sync.js';\n\nconst program = new Command();\n\nprogram\n .name('vocoder')\n .description('Vocoder CLI - Sync translations for your application')\n .version('0.1.0');\n\nprogram\n .command('sync')\n .description('Extract strings and sync translations')\n .option('--branch <name>', 'Override branch detection')\n .option('--force', 'Sync even if not a target branch')\n .option('--dry-run', 'Show what would be synced without doing it')\n .option('--verbose', 'Show detailed progress')\n .option('--max-age <seconds>', 'Use cache if younger than this (seconds)', parseInt)\n .action(sync);\n\nprogram.parse(process.argv);\n"],"mappings":";;;;;;AAEA,SAAS,eAAe;AAGxB,IAAM,UAAU,IAAI,QAAQ;AAE5B,QACG,KAAK,SAAS,EACd,YAAY,sDAAsD,EAClE,QAAQ,OAAO;AAElB,QACG,QAAQ,MAAM,EACd,YAAY,uCAAuC,EACnD,OAAO,mBAAmB,2BAA2B,EACrD,OAAO,WAAW,kCAAkC,EACpD,OAAO,aAAa,4CAA4C,EAChE,OAAO,aAAa,wBAAwB,EAC5C,OAAO,uBAAuB,4CAA4C,QAAQ,EAClF,OAAO,IAAI;AAEd,QAAQ,MAAM,QAAQ,IAAI;","names":[]}
|
|
@@ -0,0 +1,635 @@
|
|
|
1
|
+
// src/utils/branch.ts
|
|
2
|
+
import { execSync } from "child_process";
|
|
3
|
+
function detectBranch(override) {
|
|
4
|
+
if (override) {
|
|
5
|
+
return override;
|
|
6
|
+
}
|
|
7
|
+
const envBranch = process.env.GITHUB_REF_NAME || // GitHub Actions
|
|
8
|
+
process.env.VERCEL_GIT_COMMIT_REF || // Vercel
|
|
9
|
+
process.env.BRANCH || // Netlify, generic
|
|
10
|
+
process.env.CI_COMMIT_REF_NAME || // GitLab
|
|
11
|
+
process.env.BITBUCKET_BRANCH || // Bitbucket
|
|
12
|
+
process.env.CIRCLE_BRANCH;
|
|
13
|
+
if (envBranch) {
|
|
14
|
+
return envBranch;
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const branch = execSync("git rev-parse --abbrev-ref HEAD", {
|
|
18
|
+
encoding: "utf-8",
|
|
19
|
+
stdio: ["pipe", "pipe", "ignore"]
|
|
20
|
+
}).trim();
|
|
21
|
+
return branch;
|
|
22
|
+
} catch (error) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
"Failed to detect git branch. Make sure you are in a git repository or set the --branch flag."
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isTargetBranch(currentBranch, targetBranches) {
|
|
29
|
+
return targetBranches.includes(currentBranch);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// src/utils/config.ts
|
|
33
|
+
import { config as loadEnv } from "dotenv";
|
|
34
|
+
loadEnv();
|
|
35
|
+
function getLocalConfig() {
|
|
36
|
+
const apiKey = process.env.VOCODER_API_KEY;
|
|
37
|
+
if (!apiKey) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
'VOCODER_API_KEY is required. Set it in your .env file or environment:\n export VOCODER_API_KEY="your-api-key"\n\nGet your API key from: https://vocoder.app/settings/api-keys'
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
return {
|
|
43
|
+
apiKey,
|
|
44
|
+
apiUrl: process.env.VOCODER_API_URL || "https://vocoder.app"
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
function validateLocalConfig(config) {
|
|
48
|
+
if (!config.apiKey || config.apiKey.length === 0) {
|
|
49
|
+
throw new Error("Invalid API key");
|
|
50
|
+
}
|
|
51
|
+
if (!config.apiKey.startsWith("vc_")) {
|
|
52
|
+
throw new Error("Invalid API key format. Expected format: vc_...");
|
|
53
|
+
}
|
|
54
|
+
if (!config.apiUrl || !config.apiUrl.startsWith("http")) {
|
|
55
|
+
throw new Error("Invalid API URL");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/commands/sync.ts
|
|
60
|
+
import { mkdirSync, writeFileSync } from "fs";
|
|
61
|
+
import { join, dirname } from "path";
|
|
62
|
+
import chalk from "chalk";
|
|
63
|
+
import ora from "ora";
|
|
64
|
+
|
|
65
|
+
// src/utils/api.ts
|
|
66
|
+
var VocoderAPI = class {
|
|
67
|
+
constructor(config) {
|
|
68
|
+
this.apiUrl = config.apiUrl;
|
|
69
|
+
this.apiKey = config.apiKey;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Fetch project configuration from API
|
|
73
|
+
* Project is determined from the API key
|
|
74
|
+
*/
|
|
75
|
+
async getProjectConfig() {
|
|
76
|
+
const response = await fetch(
|
|
77
|
+
`${this.apiUrl}/api/cli/config`,
|
|
78
|
+
{
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
const error = await response.text();
|
|
86
|
+
throw new Error(`Failed to fetch project config: ${error}`);
|
|
87
|
+
}
|
|
88
|
+
const data = await response.json();
|
|
89
|
+
return {
|
|
90
|
+
sourceLocale: data.sourceLocale,
|
|
91
|
+
targetLocales: data.targetLocales,
|
|
92
|
+
targetBranches: data.targetBranches
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Submit strings for translation
|
|
97
|
+
* Project is determined from the API key
|
|
98
|
+
*/
|
|
99
|
+
async submitTranslation(branch, strings, targetLocales) {
|
|
100
|
+
const crypto = await import("crypto");
|
|
101
|
+
const sortedStrings = [...strings].sort();
|
|
102
|
+
const stringsHash = crypto.createHash("sha256").update(JSON.stringify(sortedStrings)).digest("hex");
|
|
103
|
+
const response = await fetch(`${this.apiUrl}/api/cli/sync`, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: {
|
|
106
|
+
"Content-Type": "application/json",
|
|
107
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
108
|
+
},
|
|
109
|
+
body: JSON.stringify({
|
|
110
|
+
branch,
|
|
111
|
+
strings,
|
|
112
|
+
targetLocales,
|
|
113
|
+
stringsHash
|
|
114
|
+
})
|
|
115
|
+
});
|
|
116
|
+
if (!response.ok) {
|
|
117
|
+
const error = await response.text();
|
|
118
|
+
throw new Error(`Translation submission failed: ${error}`);
|
|
119
|
+
}
|
|
120
|
+
return response.json();
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Check translation status
|
|
124
|
+
*/
|
|
125
|
+
async getTranslationStatus(batchId) {
|
|
126
|
+
const response = await fetch(
|
|
127
|
+
`${this.apiUrl}/api/cli/sync/status/${batchId}`,
|
|
128
|
+
{
|
|
129
|
+
headers: {
|
|
130
|
+
Authorization: `Bearer ${this.apiKey}`
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
);
|
|
134
|
+
if (!response.ok) {
|
|
135
|
+
const error = await response.text();
|
|
136
|
+
throw new Error(`Failed to check translation status: ${error}`);
|
|
137
|
+
}
|
|
138
|
+
return response.json();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Wait for translation to complete with polling
|
|
142
|
+
*/
|
|
143
|
+
async waitForCompletion(batchId, timeout = 6e4, onProgress) {
|
|
144
|
+
const startTime = Date.now();
|
|
145
|
+
const pollInterval = 1e3;
|
|
146
|
+
while (Date.now() - startTime < timeout) {
|
|
147
|
+
const status = await this.getTranslationStatus(batchId);
|
|
148
|
+
if (onProgress) {
|
|
149
|
+
onProgress(status.progress);
|
|
150
|
+
}
|
|
151
|
+
if (status.status === "COMPLETED") {
|
|
152
|
+
if (!status.translations) {
|
|
153
|
+
throw new Error("Translation completed but no translations returned");
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
translations: status.translations,
|
|
157
|
+
localeMetadata: status.localeMetadata
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (status.status === "FAILED") {
|
|
161
|
+
throw new Error(
|
|
162
|
+
`Translation failed: ${status.errorMessage || "Unknown error"}`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
await new Promise((resolve) => setTimeout(resolve, pollInterval));
|
|
166
|
+
}
|
|
167
|
+
throw new Error(`Translation timeout after ${timeout}ms`);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/utils/extract.ts
|
|
172
|
+
import { readFileSync } from "fs";
|
|
173
|
+
import { parse } from "@babel/parser";
|
|
174
|
+
import babelTraverse from "@babel/traverse";
|
|
175
|
+
import { glob } from "glob";
|
|
176
|
+
var traverse = babelTraverse.default || babelTraverse;
|
|
177
|
+
var StringExtractor = class {
|
|
178
|
+
/**
|
|
179
|
+
* Extract strings from all files matching the pattern
|
|
180
|
+
*/
|
|
181
|
+
async extractFromProject(pattern, projectRoot = process.cwd()) {
|
|
182
|
+
const files = await glob(pattern, {
|
|
183
|
+
cwd: projectRoot,
|
|
184
|
+
absolute: true,
|
|
185
|
+
ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"]
|
|
186
|
+
});
|
|
187
|
+
const allStrings = [];
|
|
188
|
+
for (const file of files) {
|
|
189
|
+
try {
|
|
190
|
+
const strings = await this.extractFromFile(file);
|
|
191
|
+
allStrings.push(...strings);
|
|
192
|
+
} catch (error) {
|
|
193
|
+
console.warn(`Warning: Failed to extract from ${file}:`, error);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const unique = this.deduplicateStrings(allStrings);
|
|
197
|
+
return unique;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Extract strings from a single file
|
|
201
|
+
*/
|
|
202
|
+
async extractFromFile(filePath) {
|
|
203
|
+
const code = readFileSync(filePath, "utf-8");
|
|
204
|
+
const strings = [];
|
|
205
|
+
try {
|
|
206
|
+
const ast = parse(code, {
|
|
207
|
+
sourceType: "module",
|
|
208
|
+
plugins: ["jsx", "typescript"]
|
|
209
|
+
});
|
|
210
|
+
const vocoderImports = /* @__PURE__ */ new Map();
|
|
211
|
+
const tFunctionNames = /* @__PURE__ */ new Set();
|
|
212
|
+
traverse(ast, {
|
|
213
|
+
// Track imports of <T> component and t function
|
|
214
|
+
ImportDeclaration: (path) => {
|
|
215
|
+
const source = path.node.source.value;
|
|
216
|
+
if (source === "@vocoder/react") {
|
|
217
|
+
path.node.specifiers.forEach((spec) => {
|
|
218
|
+
if (spec.type === "ImportSpecifier") {
|
|
219
|
+
const imported = spec.imported.type === "Identifier" ? spec.imported.name : null;
|
|
220
|
+
const local = spec.local.name;
|
|
221
|
+
if (imported === "T") {
|
|
222
|
+
vocoderImports.set(local, "T");
|
|
223
|
+
}
|
|
224
|
+
if (imported === "t") {
|
|
225
|
+
tFunctionNames.add(local);
|
|
226
|
+
}
|
|
227
|
+
if (imported === "useVocoder") {
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
},
|
|
233
|
+
// Track destructured 't' from useVocoder hook
|
|
234
|
+
VariableDeclarator: (path) => {
|
|
235
|
+
const init = path.node.init;
|
|
236
|
+
if (init && init.type === "CallExpression" && init.callee.type === "Identifier" && init.callee.name === "useVocoder" && path.node.id.type === "ObjectPattern") {
|
|
237
|
+
path.node.id.properties.forEach((prop) => {
|
|
238
|
+
if (prop.type === "ObjectProperty" && prop.key.type === "Identifier" && prop.key.name === "t") {
|
|
239
|
+
const localName = prop.value.type === "Identifier" ? prop.value.name : "t";
|
|
240
|
+
tFunctionNames.add(localName);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
// Extract from t() function calls
|
|
246
|
+
CallExpression: (path) => {
|
|
247
|
+
const callee = path.node.callee;
|
|
248
|
+
const isTFunction = callee.type === "Identifier" && tFunctionNames.has(callee.name);
|
|
249
|
+
if (!isTFunction) return;
|
|
250
|
+
const firstArg = path.node.arguments[0];
|
|
251
|
+
if (!firstArg) return;
|
|
252
|
+
let text = null;
|
|
253
|
+
if (firstArg.type === "StringLiteral") {
|
|
254
|
+
text = firstArg.value;
|
|
255
|
+
} else if (firstArg.type === "TemplateLiteral") {
|
|
256
|
+
text = this.extractTemplateText(firstArg);
|
|
257
|
+
}
|
|
258
|
+
if (!text || text.trim().length === 0) return;
|
|
259
|
+
const secondArg = path.node.arguments[1];
|
|
260
|
+
let context;
|
|
261
|
+
let formality;
|
|
262
|
+
if (secondArg && secondArg.type === "ObjectExpression") {
|
|
263
|
+
secondArg.properties.forEach((prop) => {
|
|
264
|
+
if (prop.type === "ObjectProperty" && prop.key.type === "Identifier") {
|
|
265
|
+
if (prop.key.name === "context" && prop.value.type === "StringLiteral") {
|
|
266
|
+
context = prop.value.value;
|
|
267
|
+
}
|
|
268
|
+
if (prop.key.name === "formality" && prop.value.type === "StringLiteral") {
|
|
269
|
+
formality = prop.value.value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
}
|
|
274
|
+
strings.push({
|
|
275
|
+
text: text.trim(),
|
|
276
|
+
file: filePath,
|
|
277
|
+
line: path.node.loc?.start.line || 0,
|
|
278
|
+
context,
|
|
279
|
+
formality
|
|
280
|
+
});
|
|
281
|
+
},
|
|
282
|
+
// Extract from JSX elements
|
|
283
|
+
JSXElement: (path) => {
|
|
284
|
+
const opening = path.node.openingElement;
|
|
285
|
+
const tagName = opening.name.type === "JSXIdentifier" ? opening.name.name : null;
|
|
286
|
+
if (!tagName) return;
|
|
287
|
+
const isTranslationComponent = vocoderImports.has(tagName);
|
|
288
|
+
if (!isTranslationComponent) return;
|
|
289
|
+
const text = this.extractTextContent(path.node.children);
|
|
290
|
+
if (!text || text.trim().length === 0) return;
|
|
291
|
+
const context = this.getStringAttribute(opening.attributes, "context");
|
|
292
|
+
const formality = this.getStringAttribute(
|
|
293
|
+
opening.attributes,
|
|
294
|
+
"formality"
|
|
295
|
+
);
|
|
296
|
+
strings.push({
|
|
297
|
+
text: text.trim(),
|
|
298
|
+
file: filePath,
|
|
299
|
+
line: path.node.loc?.start.line || 0,
|
|
300
|
+
context,
|
|
301
|
+
formality
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
});
|
|
305
|
+
} catch (error) {
|
|
306
|
+
throw new Error(
|
|
307
|
+
`Failed to parse ${filePath}: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
return strings;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Extract text from template literal
|
|
314
|
+
* Converts template literals like `Hello ${name}` to `Hello {name}`
|
|
315
|
+
*/
|
|
316
|
+
extractTemplateText(node) {
|
|
317
|
+
let text = "";
|
|
318
|
+
for (let i = 0; i < node.quasis.length; i++) {
|
|
319
|
+
const quasi = node.quasis[i];
|
|
320
|
+
text += quasi.value.raw;
|
|
321
|
+
if (i < node.expressions.length) {
|
|
322
|
+
const expr = node.expressions[i];
|
|
323
|
+
if (expr.type === "Identifier") {
|
|
324
|
+
text += `{${expr.name}}`;
|
|
325
|
+
} else {
|
|
326
|
+
text += "{value}";
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
return text;
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* Extract text content from JSX children
|
|
334
|
+
*/
|
|
335
|
+
extractTextContent(children) {
|
|
336
|
+
let text = "";
|
|
337
|
+
for (const child of children) {
|
|
338
|
+
if (child.type === "JSXText") {
|
|
339
|
+
text += child.value;
|
|
340
|
+
} else if (child.type === "JSXExpressionContainer") {
|
|
341
|
+
const expr = child.expression;
|
|
342
|
+
if (expr.type === "Identifier") {
|
|
343
|
+
text += `{${expr.name}}`;
|
|
344
|
+
} else if (expr.type === "StringLiteral") {
|
|
345
|
+
text += expr.value;
|
|
346
|
+
} else if (expr.type === "TemplateLiteral") {
|
|
347
|
+
text += this.extractTemplateText(expr);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return text;
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Get string value from JSX attribute
|
|
355
|
+
*/
|
|
356
|
+
getStringAttribute(attributes, name) {
|
|
357
|
+
const attr = attributes.find(
|
|
358
|
+
(a) => a.type === "JSXAttribute" && a.name.name === name
|
|
359
|
+
);
|
|
360
|
+
if (!attr || !attr.value) return void 0;
|
|
361
|
+
if (attr.value.type === "StringLiteral") {
|
|
362
|
+
return attr.value.value;
|
|
363
|
+
}
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Deduplicate strings (keep first occurrence)
|
|
368
|
+
*/
|
|
369
|
+
deduplicateStrings(strings) {
|
|
370
|
+
const seen = /* @__PURE__ */ new Set();
|
|
371
|
+
const unique = [];
|
|
372
|
+
for (const str of strings) {
|
|
373
|
+
const key = `${str.text}|${str.context || ""}|${str.formality || ""}`;
|
|
374
|
+
if (!seen.has(key)) {
|
|
375
|
+
seen.add(key);
|
|
376
|
+
unique.push(str);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return unique;
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
|
|
383
|
+
// src/commands/sync.ts
|
|
384
|
+
function generateIndexFile(locales, translations, localeMetadata) {
|
|
385
|
+
const toIdentifier = (locale) => locale.replace(/-/g, "_");
|
|
386
|
+
const imports = locales.map(
|
|
387
|
+
(locale) => `import ${toIdentifier(locale)} from './${locale}.json';`
|
|
388
|
+
).join("\n");
|
|
389
|
+
const translationsObj = locales.map(
|
|
390
|
+
(locale) => ` '${locale}': ${toIdentifier(locale)},`
|
|
391
|
+
).join("\n");
|
|
392
|
+
const localesObjEntries = locales.map((locale) => {
|
|
393
|
+
const metadata = localeMetadata?.[locale];
|
|
394
|
+
if (metadata) {
|
|
395
|
+
const escapedNativeName = metadata.nativeName.replace(/'/g, "\\'");
|
|
396
|
+
const dirProp = metadata.dir ? `, dir: '${metadata.dir}' as const` : "";
|
|
397
|
+
return ` '${locale}': { nativeName: '${escapedNativeName}'${dirProp} }`;
|
|
398
|
+
} else {
|
|
399
|
+
return ` '${locale}': { nativeName: '${locale}' }`;
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
const localesObjString = localesObjEntries.join(",\n");
|
|
403
|
+
return `// Auto-generated by Vocoder CLI
|
|
404
|
+
// This file imports all locale JSON files and exports them as a single object
|
|
405
|
+
// Usage: import { translations, locales } from './.vocoder/locales';
|
|
406
|
+
|
|
407
|
+
${imports}
|
|
408
|
+
|
|
409
|
+
export const translations = {
|
|
410
|
+
${translationsObj}
|
|
411
|
+
};
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Flat locale metadata map (O(N))
|
|
415
|
+
* Structure: locales[localeCode] = { nativeName, dir? }
|
|
416
|
+
* - nativeName: Name in the locale's own language (e.g., "Espa\xF1ol", "\u7B80\u4F53\u4E2D\u6587")
|
|
417
|
+
* - dir: Optional 'rtl' for right-to-left locales
|
|
418
|
+
*
|
|
419
|
+
* Translated names are generated at runtime using Intl.DisplayNames:
|
|
420
|
+
* Example: new Intl.DisplayNames(['es'], { type: 'language' }).of('en') \u2192 "ingl\xE9s"
|
|
421
|
+
* Display format: \`\${getDisplayName(code)} (\${locales[code].nativeName})\` \u2192 "ingl\xE9s (English)"
|
|
422
|
+
*/
|
|
423
|
+
export const locales = {
|
|
424
|
+
${localesObjString}
|
|
425
|
+
};
|
|
426
|
+
|
|
427
|
+
export type SupportedLocale = ${locales.map((l) => `'${l}'`).join(" | ")};
|
|
428
|
+
`;
|
|
429
|
+
}
|
|
430
|
+
async function sync(options = {}) {
|
|
431
|
+
const startTime = Date.now();
|
|
432
|
+
const projectRoot = process.cwd();
|
|
433
|
+
try {
|
|
434
|
+
const spinner = ora("Detecting branch...").start();
|
|
435
|
+
const branch = detectBranch(options.branch);
|
|
436
|
+
spinner.succeed(`Detected branch: ${chalk.cyan(branch)}`);
|
|
437
|
+
spinner.start("Loading project configuration...");
|
|
438
|
+
const localConfig = getLocalConfig();
|
|
439
|
+
validateLocalConfig(localConfig);
|
|
440
|
+
const api = new VocoderAPI(localConfig);
|
|
441
|
+
const apiConfig = await api.getProjectConfig();
|
|
442
|
+
const config = {
|
|
443
|
+
...localConfig,
|
|
444
|
+
...apiConfig,
|
|
445
|
+
extractionPattern: process.env.VOCODER_EXTRACTION_PATTERN || "src/**/*.{tsx,jsx,ts,js}",
|
|
446
|
+
outputDir: ".vocoder/locales",
|
|
447
|
+
timeout: 6e4
|
|
448
|
+
};
|
|
449
|
+
spinner.succeed("Project configuration loaded");
|
|
450
|
+
if (!options.force && !isTargetBranch(branch, config.targetBranches)) {
|
|
451
|
+
console.log(
|
|
452
|
+
chalk.yellow(
|
|
453
|
+
`\u2139\uFE0F Skipping translations (${branch} is not a target branch)`
|
|
454
|
+
)
|
|
455
|
+
);
|
|
456
|
+
console.log(
|
|
457
|
+
chalk.dim(
|
|
458
|
+
` Target branches: ${config.targetBranches.join(", ")}`
|
|
459
|
+
)
|
|
460
|
+
);
|
|
461
|
+
console.log(chalk.dim(` Use --force to translate anyway`));
|
|
462
|
+
process.exit(0);
|
|
463
|
+
}
|
|
464
|
+
spinner.start(`Extracting strings from ${config.extractionPattern}...`);
|
|
465
|
+
const extractor = new StringExtractor();
|
|
466
|
+
const extractedStrings = await extractor.extractFromProject(
|
|
467
|
+
config.extractionPattern,
|
|
468
|
+
projectRoot
|
|
469
|
+
);
|
|
470
|
+
if (extractedStrings.length === 0) {
|
|
471
|
+
spinner.warn("No translatable strings found");
|
|
472
|
+
console.log(chalk.yellow("Make sure you are using <T> components from @vocoder/react"));
|
|
473
|
+
process.exit(0);
|
|
474
|
+
}
|
|
475
|
+
spinner.succeed(
|
|
476
|
+
`Extracted ${chalk.cyan(extractedStrings.length)} strings from ${chalk.cyan(config.extractionPattern)}`
|
|
477
|
+
);
|
|
478
|
+
if (options.verbose) {
|
|
479
|
+
console.log(chalk.dim("\nSample strings:"));
|
|
480
|
+
extractedStrings.slice(0, 5).forEach((s) => {
|
|
481
|
+
console.log(chalk.dim(` - "${s.text}" (${s.file}:${s.line})`));
|
|
482
|
+
});
|
|
483
|
+
if (extractedStrings.length > 5) {
|
|
484
|
+
console.log(chalk.dim(` ... and ${extractedStrings.length - 5} more`));
|
|
485
|
+
}
|
|
486
|
+
console.log();
|
|
487
|
+
}
|
|
488
|
+
if (options.dryRun) {
|
|
489
|
+
console.log(chalk.cyan("\n\u{1F4CB} Dry run mode - would translate:"));
|
|
490
|
+
console.log(chalk.dim(` Strings: ${extractedStrings.length}`));
|
|
491
|
+
console.log(chalk.dim(` Branch: ${branch}`));
|
|
492
|
+
console.log(chalk.dim(` Target locales: ${config.targetLocales.join(", ")}`));
|
|
493
|
+
console.log(chalk.dim(`
|
|
494
|
+
No API calls made.`));
|
|
495
|
+
process.exit(0);
|
|
496
|
+
}
|
|
497
|
+
spinner.start("Submitting strings to Vocoder API...");
|
|
498
|
+
const strings = extractedStrings.map((s) => s.text);
|
|
499
|
+
const batchResponse = await api.submitTranslation(
|
|
500
|
+
branch,
|
|
501
|
+
strings,
|
|
502
|
+
config.targetLocales
|
|
503
|
+
);
|
|
504
|
+
spinner.succeed(
|
|
505
|
+
`Submitted to API - Batch ID: ${chalk.cyan(batchResponse.batchId)}`
|
|
506
|
+
);
|
|
507
|
+
if (batchResponse.status === "UP_TO_DATE" && batchResponse.noChanges) {
|
|
508
|
+
console.log(chalk.green("\n\u2714 No changes detected - strings are up to date"));
|
|
509
|
+
console.log(chalk.dim(" (Files will be written for build environment)\n"));
|
|
510
|
+
}
|
|
511
|
+
console.log(
|
|
512
|
+
chalk.dim(
|
|
513
|
+
` New strings: ${chalk.cyan(batchResponse.newStrings)}`
|
|
514
|
+
)
|
|
515
|
+
);
|
|
516
|
+
if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {
|
|
517
|
+
console.log(
|
|
518
|
+
chalk.dim(
|
|
519
|
+
` Deleted strings: ${chalk.yellow(batchResponse.deletedStrings)} (archived)`
|
|
520
|
+
)
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
console.log(
|
|
524
|
+
chalk.dim(
|
|
525
|
+
` Total strings: ${chalk.cyan(batchResponse.totalStrings)}`
|
|
526
|
+
)
|
|
527
|
+
);
|
|
528
|
+
if (batchResponse.newStrings === 0) {
|
|
529
|
+
console.log(
|
|
530
|
+
chalk.green("\n\u2705 No new strings - using existing translations")
|
|
531
|
+
);
|
|
532
|
+
} else {
|
|
533
|
+
console.log(
|
|
534
|
+
chalk.cyan(
|
|
535
|
+
`
|
|
536
|
+
\u23F3 Syncing to ${config.targetLocales.length} locales (${config.targetLocales.join(", ")})`
|
|
537
|
+
)
|
|
538
|
+
);
|
|
539
|
+
if (batchResponse.estimatedTime) {
|
|
540
|
+
console.log(
|
|
541
|
+
chalk.dim(
|
|
542
|
+
` Estimated time: ~${batchResponse.estimatedTime} seconds`
|
|
543
|
+
)
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
spinner.start("Waiting for translations to complete...");
|
|
548
|
+
let lastProgress = 0;
|
|
549
|
+
const result = await api.waitForCompletion(
|
|
550
|
+
batchResponse.batchId,
|
|
551
|
+
config.timeout,
|
|
552
|
+
(progress) => {
|
|
553
|
+
const percent = Math.round(progress * 100);
|
|
554
|
+
if (percent > lastProgress) {
|
|
555
|
+
spinner.text = `Syncing... ${percent}% complete`;
|
|
556
|
+
lastProgress = percent;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
);
|
|
560
|
+
const { translations, localeMetadata: apiLocaleMetadata } = result;
|
|
561
|
+
spinner.succeed("Translations complete!");
|
|
562
|
+
spinner.start(`Writing locale files to ${config.outputDir}...`);
|
|
563
|
+
const outputPath = join(projectRoot, config.outputDir);
|
|
564
|
+
mkdirSync(outputPath, { recursive: true });
|
|
565
|
+
let filesWritten = 0;
|
|
566
|
+
const localeNames = [];
|
|
567
|
+
for (const [locale, strings2] of Object.entries(translations)) {
|
|
568
|
+
const filePath = join(outputPath, `${locale}.json`);
|
|
569
|
+
const content = JSON.stringify(strings2, null, 2);
|
|
570
|
+
mkdirSync(dirname(filePath), { recursive: true });
|
|
571
|
+
writeFileSync(filePath, content, "utf-8");
|
|
572
|
+
filesWritten++;
|
|
573
|
+
localeNames.push(locale);
|
|
574
|
+
const sizeKB = (content.length / 1024).toFixed(1);
|
|
575
|
+
console.log(
|
|
576
|
+
chalk.dim(` \u2713 Wrote ${locale}.json (${sizeKB}KB)`)
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
const indexContent = generateIndexFile(localeNames, translations, apiLocaleMetadata);
|
|
580
|
+
const indexPath = join(outputPath, "index.ts");
|
|
581
|
+
writeFileSync(indexPath, indexContent, "utf-8");
|
|
582
|
+
console.log(chalk.dim(` \u2713 Generated index.ts (with flat locales map)`));
|
|
583
|
+
spinner.succeed(`Wrote ${chalk.cyan(filesWritten)} locale files`);
|
|
584
|
+
const duration = ((Date.now() - startTime) / 1e3).toFixed(1);
|
|
585
|
+
console.log(
|
|
586
|
+
chalk.green(`
|
|
587
|
+
\u2705 Sync complete! (${duration}s)
|
|
588
|
+
`)
|
|
589
|
+
);
|
|
590
|
+
console.log(chalk.dim("Next steps:"));
|
|
591
|
+
console.log(
|
|
592
|
+
chalk.dim(
|
|
593
|
+
` 1. Import translations: import { translations } from '${config.outputDir}'`
|
|
594
|
+
)
|
|
595
|
+
);
|
|
596
|
+
console.log(
|
|
597
|
+
chalk.dim(
|
|
598
|
+
` 2. Use VocoderProvider: <VocoderProvider translations={translations} defaultLocale="en">`
|
|
599
|
+
)
|
|
600
|
+
);
|
|
601
|
+
console.log(
|
|
602
|
+
chalk.dim(
|
|
603
|
+
` 3. Commit ${config.outputDir}/ to your repository`
|
|
604
|
+
)
|
|
605
|
+
);
|
|
606
|
+
} catch (error) {
|
|
607
|
+
if (error instanceof Error) {
|
|
608
|
+
console.error(chalk.red(`
|
|
609
|
+
\u274C Error: ${error.message}
|
|
610
|
+
`));
|
|
611
|
+
if (error.message.includes("VOCODER_API_KEY")) {
|
|
612
|
+
console.log(chalk.yellow("\u{1F4A1} Solution:"));
|
|
613
|
+
console.log(chalk.dim(" Set your API key:"));
|
|
614
|
+
console.log(chalk.dim(' export VOCODER_API_KEY="your-api-key"'));
|
|
615
|
+
console.log(chalk.dim(" or add it to your .env file"));
|
|
616
|
+
} else if (error.message.includes("git branch")) {
|
|
617
|
+
console.log(chalk.yellow("\u{1F4A1} Solution:"));
|
|
618
|
+
console.log(chalk.dim(" Run from a git repository, or use:"));
|
|
619
|
+
console.log(chalk.dim(" vocoder translate --branch main"));
|
|
620
|
+
}
|
|
621
|
+
if (options.verbose) {
|
|
622
|
+
console.error(chalk.dim("\nFull error:"), error);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
process.exit(1);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
export {
|
|
630
|
+
detectBranch,
|
|
631
|
+
getLocalConfig,
|
|
632
|
+
validateLocalConfig,
|
|
633
|
+
sync
|
|
634
|
+
};
|
|
635
|
+
//# sourceMappingURL=chunk-N45Q4R6O.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/utils/branch.ts","../src/utils/config.ts","../src/commands/sync.ts","../src/utils/api.ts","../src/utils/extract.ts"],"sourcesContent":["import { execSync } from 'child_process';\n\n/**\n * Detects the current git branch from multiple sources in priority order:\n * 1. Explicit --branch flag (passed as parameter)\n * 2. CI environment variables (GitHub Actions, Vercel, Netlify, etc.)\n * 3. Git command (local development)\n *\n * @param override - Optional branch name to override detection\n * @returns The current branch name\n */\nexport function detectBranch(override?: string): string {\n // 1. Explicit override (from --branch flag)\n if (override) {\n return override;\n }\n\n // 2. CI environment variables\n const envBranch =\n process.env.GITHUB_REF_NAME || // GitHub Actions\n process.env.VERCEL_GIT_COMMIT_REF || // Vercel\n process.env.BRANCH || // Netlify, generic\n process.env.CI_COMMIT_REF_NAME || // GitLab\n process.env.BITBUCKET_BRANCH || // Bitbucket\n process.env.CIRCLE_BRANCH; // CircleCI\n\n if (envBranch) {\n return envBranch;\n }\n\n // 3. Git command (local development)\n try {\n const branch = execSync('git rev-parse --abbrev-ref HEAD', {\n encoding: 'utf-8',\n stdio: ['pipe', 'pipe', 'ignore'],\n }).trim();\n\n return branch;\n } catch (error) {\n throw new Error(\n 'Failed to detect git branch. Make sure you are in a git repository or set the --branch flag.',\n );\n }\n}\n\n/**\n * Checks if the current branch is a target branch that should trigger translations\n *\n * @param currentBranch - The current branch name\n * @param targetBranches - List of branches that should trigger translations\n * @returns True if the branch should trigger translations\n */\nexport function isTargetBranch(\n currentBranch: string,\n targetBranches: string[],\n): boolean {\n return targetBranches.includes(currentBranch);\n}\n","import type { LocalConfig } from '../types.js';\nimport { config as loadEnv } from 'dotenv';\n\n// Load .env file if present\nloadEnv();\n\n/**\n * Loads local configuration from environment variables\n *\n * Required environment variables:\n * - VOCODER_API_KEY: Your Vocoder project API key\n *\n * Optional environment variables:\n * - VOCODER_API_URL: Override API URL (default: https://vocoder.app)\n *\n * @returns Local configuration\n */\nexport function getLocalConfig(): LocalConfig {\n const apiKey = process.env.VOCODER_API_KEY;\n\n if (!apiKey) {\n throw new Error(\n 'VOCODER_API_KEY is required. Set it in your .env file or environment:\\n' +\n ' export VOCODER_API_KEY=\"your-api-key\"\\n\\n' +\n 'Get your API key from: https://vocoder.app/settings/api-keys'\n );\n }\n\n return {\n apiKey,\n apiUrl: process.env.VOCODER_API_URL || 'https://vocoder.app',\n };\n}\n\n/**\n * Validates the local configuration\n */\nexport function validateLocalConfig(config: LocalConfig): void {\n if (!config.apiKey || config.apiKey.length === 0) {\n throw new Error('Invalid API key');\n }\n\n if (!config.apiKey.startsWith('vc_')) {\n throw new Error('Invalid API key format. Expected format: vc_...');\n }\n\n if (!config.apiUrl || !config.apiUrl.startsWith('http')) {\n throw new Error('Invalid API URL');\n }\n}\n","import { mkdirSync, writeFileSync } from 'fs';\nimport { join, dirname } from 'path';\nimport chalk from 'chalk';\nimport ora from 'ora';\nimport { detectBranch, isTargetBranch } from '../utils/branch.js';\nimport { getLocalConfig, validateLocalConfig } from '../utils/config.js';\nimport { VocoderAPI } from '../utils/api.js';\nimport { StringExtractor } from '../utils/extract.js';\nimport type { TranslateOptions, ProjectConfig } from '../types.js';\n\n/**\n * Generate index.ts file that auto-imports all locale files and creates a flat locales map (O(N))\n * Translated names are generated at runtime using Intl.DisplayNames\n */\nfunction generateIndexFile(\n locales: string[],\n translations: Record<string, Record<string, string>>,\n localeMetadata?: Record<string, { nativeName: string; dir?: 'rtl' }>\n): string {\n // Convert locale names to valid JS identifiers (replace hyphens with underscores)\n const toIdentifier = (locale: string) => locale.replace(/-/g, '_');\n\n const imports = locales.map(\n (locale: string) => `import ${toIdentifier(locale)} from './${locale}.json';`\n ).join('\\n');\n\n const translationsObj = locales.map(\n (locale: string) => ` '${locale}': ${toIdentifier(locale)},`\n ).join('\\n');\n\n // Build flat locales map (O(N) instead of O(N²))\n // Use API-provided locale metadata if available, otherwise fallback to locale code\n const localesObjEntries = locales.map((locale: string) => {\n const metadata = localeMetadata?.[locale];\n\n if (metadata) {\n const escapedNativeName = metadata.nativeName.replace(/'/g, \"\\\\'\");\n const dirProp = metadata.dir ? `, dir: '${metadata.dir}' as const` : '';\n return ` '${locale}': { nativeName: '${escapedNativeName}'${dirProp} }`;\n } else {\n // Fallback: just use locale code as nativeName\n return ` '${locale}': { nativeName: '${locale}' }`;\n }\n });\n\n const localesObjString = localesObjEntries.join(',\\n');\n\n return `// Auto-generated by Vocoder CLI\n// This file imports all locale JSON files and exports them as a single object\n// Usage: import { translations, locales } from './.vocoder/locales';\n\n${imports}\n\nexport const translations = {\n${translationsObj}\n};\n\n/**\n * Flat locale metadata map (O(N))\n * Structure: locales[localeCode] = { nativeName, dir? }\n * - nativeName: Name in the locale's own language (e.g., \"Español\", \"简体中文\")\n * - dir: Optional 'rtl' for right-to-left locales\n *\n * Translated names are generated at runtime using Intl.DisplayNames:\n * Example: new Intl.DisplayNames(['es'], { type: 'language' }).of('en') → \"inglés\"\n * Display format: \\`\\${getDisplayName(code)} (\\${locales[code].nativeName})\\` → \"inglés (English)\"\n */\nexport const locales = {\n${localesObjString}\n};\n\nexport type SupportedLocale = ${locales.map((l: string) => `'${l}'`).join(' | ')};\n`;\n}\n\n/**\n * Main sync command\n *\n * Workflow:\n * 1. Detect branch\n * 2. Load project config\n * 3. Check if target branch (skip if not)\n * 4. Extract strings from source code\n * 5. Submit to API for translation\n * 6. Poll for completion\n * 7. Write locale files to .vocoder/locales/\n */\nexport async function sync(options: TranslateOptions = {}): Promise<void> {\n const startTime = Date.now();\n const projectRoot = process.cwd();\n\n try {\n // 1. Detect branch\n const spinner = ora('Detecting branch...').start();\n const branch = detectBranch(options.branch);\n spinner.succeed(`Detected branch: ${chalk.cyan(branch)}`);\n\n // 2. Load local config and fetch project config from API\n spinner.start('Loading project configuration...');\n const localConfig = getLocalConfig();\n validateLocalConfig(localConfig);\n\n const api = new VocoderAPI(localConfig);\n const apiConfig = await api.getProjectConfig();\n\n // Merge local and API config\n const config: ProjectConfig = {\n ...localConfig,\n ...apiConfig,\n extractionPattern: process.env.VOCODER_EXTRACTION_PATTERN || 'src/**/*.{tsx,jsx,ts,js}',\n outputDir: '.vocoder/locales',\n timeout: 60000,\n };\n\n spinner.succeed('Project configuration loaded');\n\n // 3. Check if target branch\n if (!options.force && !isTargetBranch(branch, config.targetBranches)) {\n console.log(\n chalk.yellow(\n `ℹ️ Skipping translations (${branch} is not a target branch)`,\n ),\n );\n console.log(\n chalk.dim(\n ` Target branches: ${config.targetBranches.join(', ')}`,\n ),\n );\n console.log(chalk.dim(` Use --force to translate anyway`));\n process.exit(0);\n }\n\n // 4. Extract strings\n spinner.start(`Extracting strings from ${config.extractionPattern}...`);\n const extractor = new StringExtractor();\n const extractedStrings = await extractor.extractFromProject(\n config.extractionPattern,\n projectRoot,\n );\n\n if (extractedStrings.length === 0) {\n spinner.warn('No translatable strings found');\n console.log(chalk.yellow('Make sure you are using <T> components from @vocoder/react'));\n process.exit(0);\n }\n\n spinner.succeed(\n `Extracted ${chalk.cyan(extractedStrings.length)} strings from ${chalk.cyan(config.extractionPattern)}`,\n );\n\n // Show sample strings in verbose mode\n if (options.verbose) {\n console.log(chalk.dim('\\nSample strings:'));\n extractedStrings.slice(0, 5).forEach((s) => {\n console.log(chalk.dim(` - \"${s.text}\" (${s.file}:${s.line})`));\n });\n if (extractedStrings.length > 5) {\n console.log(chalk.dim(` ... and ${extractedStrings.length - 5} more`));\n }\n console.log();\n }\n\n // Dry run mode - stop here\n if (options.dryRun) {\n console.log(chalk.cyan('\\n📋 Dry run mode - would translate:'));\n console.log(chalk.dim(` Strings: ${extractedStrings.length}`));\n console.log(chalk.dim(` Branch: ${branch}`));\n console.log(chalk.dim(` Target locales: ${config.targetLocales.join(', ')}`));\n console.log(chalk.dim(`\\n No API calls made.`));\n process.exit(0);\n }\n\n // 5. Submit to API\n spinner.start('Submitting strings to Vocoder API...');\n\n const strings = extractedStrings.map((s) => s.text);\n const batchResponse = await api.submitTranslation(\n branch,\n strings,\n config.targetLocales,\n );\n\n spinner.succeed(\n `Submitted to API - Batch ID: ${chalk.cyan(batchResponse.batchId)}`,\n );\n\n // Handle UP_TO_DATE status (hash matched, no changes)\n // Note: We still write locale files even when up-to-date, because\n // .vocoder/ might be gitignored and not present in the build environment\n if (batchResponse.status === 'UP_TO_DATE' && batchResponse.noChanges) {\n console.log(chalk.green('\\n✔ No changes detected - strings are up to date'));\n console.log(chalk.dim(' (Files will be written for build environment)\\n'));\n }\n\n // Display diff metrics\n console.log(\n chalk.dim(\n ` New strings: ${chalk.cyan(batchResponse.newStrings)}`,\n ),\n );\n\n if (batchResponse.deletedStrings && batchResponse.deletedStrings > 0) {\n console.log(\n chalk.dim(\n ` Deleted strings: ${chalk.yellow(batchResponse.deletedStrings)} (archived)`,\n ),\n );\n }\n\n console.log(\n chalk.dim(\n ` Total strings: ${chalk.cyan(batchResponse.totalStrings)}`,\n ),\n );\n\n if (batchResponse.newStrings === 0) {\n console.log(\n chalk.green('\\n✅ No new strings - using existing translations'),\n );\n // Still fetch and write translations\n } else {\n console.log(\n chalk.cyan(\n `\\n⏳ Syncing to ${config.targetLocales.length} locales (${config.targetLocales.join(', ')})`,\n ),\n );\n\n if (batchResponse.estimatedTime) {\n console.log(\n chalk.dim(\n ` Estimated time: ~${batchResponse.estimatedTime} seconds`,\n ),\n );\n }\n }\n\n // 6. Poll for completion\n spinner.start('Waiting for translations to complete...');\n\n let lastProgress = 0;\n const result = await api.waitForCompletion(\n batchResponse.batchId,\n config.timeout,\n (progress) => {\n const percent = Math.round(progress * 100);\n if (percent > lastProgress) {\n spinner.text = `Syncing... ${percent}% complete`;\n lastProgress = percent;\n }\n },\n );\n\n const { translations, localeMetadata: apiLocaleMetadata } = result;\n spinner.succeed('Translations complete!');\n\n // 7. Write locale files\n spinner.start(`Writing locale files to ${config.outputDir}...`);\n\n const outputPath = join(projectRoot, config.outputDir);\n mkdirSync(outputPath, { recursive: true });\n\n let filesWritten = 0;\n const localeNames: string[] = [];\n\n // Write locale files from API response (includes source locale now)\n for (const [locale, strings] of Object.entries(translations)) {\n const filePath = join(outputPath, `${locale}.json`);\n const content = JSON.stringify(strings, null, 2);\n\n // Ensure directory exists\n mkdirSync(dirname(filePath), { recursive: true });\n\n writeFileSync(filePath, content, 'utf-8');\n filesWritten++;\n localeNames.push(locale);\n\n const sizeKB = (content.length / 1024).toFixed(1);\n console.log(\n chalk.dim(` ✓ Wrote ${locale}.json (${sizeKB}KB)`),\n );\n }\n\n // Generate index file for auto-importing all locales (including source)\n const indexContent = generateIndexFile(localeNames, translations, apiLocaleMetadata);\n const indexPath = join(outputPath, 'index.ts');\n writeFileSync(indexPath, indexContent, 'utf-8');\n console.log(chalk.dim(` ✓ Generated index.ts (with flat locales map)`));\n\n spinner.succeed(`Wrote ${chalk.cyan(filesWritten)} locale files`);\n\n // Success!\n const duration = ((Date.now() - startTime) / 1000).toFixed(1);\n console.log(\n chalk.green(`\\n✅ Sync complete! (${duration}s)\\n`),\n );\n\n // Show next steps\n console.log(chalk.dim('Next steps:'));\n console.log(\n chalk.dim(\n ` 1. Import translations: import { translations } from '${config.outputDir}'`,\n ),\n );\n console.log(\n chalk.dim(\n ` 2. Use VocoderProvider: <VocoderProvider translations={translations} defaultLocale=\"en\">`,\n ),\n );\n console.log(\n chalk.dim(\n ` 3. Commit ${config.outputDir}/ to your repository`,\n ),\n );\n } catch (error) {\n if (error instanceof Error) {\n console.error(chalk.red(`\\n❌ Error: ${error.message}\\n`));\n\n // Show helpful error messages\n if (error.message.includes('VOCODER_API_KEY')) {\n console.log(chalk.yellow('💡 Solution:'));\n console.log(chalk.dim(' Set your API key:'));\n console.log(chalk.dim(' export VOCODER_API_KEY=\"your-api-key\"'));\n console.log(chalk.dim(' or add it to your .env file'));\n } else if (error.message.includes('git branch')) {\n console.log(chalk.yellow('💡 Solution:'));\n console.log(chalk.dim(' Run from a git repository, or use:'));\n console.log(chalk.dim(' vocoder translate --branch main'));\n }\n\n if (options.verbose) {\n console.error(chalk.dim('\\nFull error:'), error);\n }\n }\n\n process.exit(1);\n }\n}\n","import type {\n TranslationBatchResponse,\n TranslationStatusResponse,\n LocalConfig,\n APIProjectConfig,\n} from '../types.js';\n\nexport class VocoderAPI {\n private apiUrl: string;\n private apiKey: string;\n\n constructor(config: LocalConfig) {\n this.apiUrl = config.apiUrl;\n this.apiKey = config.apiKey;\n }\n\n /**\n * Fetch project configuration from API\n * Project is determined from the API key\n */\n async getProjectConfig(): Promise<APIProjectConfig> {\n const response = await fetch(\n `${this.apiUrl}/api/cli/config`,\n {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n },\n },\n );\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to fetch project config: ${error}`);\n }\n\n const data = await response.json();\n\n return {\n sourceLocale: data.sourceLocale,\n targetLocales: data.targetLocales,\n targetBranches: data.targetBranches,\n };\n }\n\n /**\n * Submit strings for translation\n * Project is determined from the API key\n */\n async submitTranslation(\n branch: string,\n strings: string[],\n targetLocales: string[],\n ): Promise<TranslationBatchResponse> {\n // Compute hash of sorted strings for fast comparison\n const crypto = await import('crypto');\n const sortedStrings = [...strings].sort();\n const stringsHash = crypto\n .createHash('sha256')\n .update(JSON.stringify(sortedStrings))\n .digest('hex');\n\n const response = await fetch(`${this.apiUrl}/api/cli/sync`, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n Authorization: `Bearer ${this.apiKey}`,\n },\n body: JSON.stringify({\n branch,\n strings,\n targetLocales,\n stringsHash,\n }),\n });\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Translation submission failed: ${error}`);\n }\n\n return response.json();\n }\n\n /**\n * Check translation status\n */\n async getTranslationStatus(\n batchId: string,\n ): Promise<TranslationStatusResponse> {\n const response = await fetch(\n `${this.apiUrl}/api/cli/sync/status/${batchId}`,\n {\n headers: {\n Authorization: `Bearer ${this.apiKey}`,\n },\n },\n );\n\n if (!response.ok) {\n const error = await response.text();\n throw new Error(`Failed to check translation status: ${error}`);\n }\n\n return response.json();\n }\n\n /**\n * Wait for translation to complete with polling\n */\n async waitForCompletion(\n batchId: string,\n timeout: number = 60000,\n onProgress?: (progress: number) => void,\n ): Promise<{\n translations: Record<string, Record<string, string>>;\n localeMetadata?: Record<string, { nativeName: string; dir?: 'rtl' }>;\n }> {\n const startTime = Date.now();\n const pollInterval = 1000; // Poll every second\n\n while (Date.now() - startTime < timeout) {\n const status = await this.getTranslationStatus(batchId);\n\n // Call progress callback\n if (onProgress) {\n onProgress(status.progress);\n }\n\n if (status.status === 'COMPLETED') {\n if (!status.translations) {\n throw new Error('Translation completed but no translations returned');\n }\n return {\n translations: status.translations,\n localeMetadata: status.localeMetadata,\n };\n }\n\n if (status.status === 'FAILED') {\n throw new Error(\n `Translation failed: ${status.errorMessage || 'Unknown error'}`,\n );\n }\n\n // Wait before polling again\n await new Promise((resolve) => setTimeout(resolve, pollInterval));\n }\n\n throw new Error(`Translation timeout after ${timeout}ms`);\n }\n}\n","import { readFileSync } from 'fs';\nimport { parse } from '@babel/parser';\nimport babelTraverse from '@babel/traverse';\nimport { glob } from 'glob';\nimport type { ExtractedString } from '../types.js';\n\n// Handle default export difference between ESM and CommonJS\nconst traverse = (babelTraverse as any).default || babelTraverse;\n\n/**\n * Extract translatable strings from source files\n *\n * NOTE: This is a simplified version for the CLI MVP.\n * Eventually this logic should be moved to a shared @vocoder/extraction package\n * that can be used by both the CLI and the backend.\n */\nexport class StringExtractor {\n /**\n * Extract strings from all files matching the pattern\n */\n async extractFromProject(\n pattern: string,\n projectRoot: string = process.cwd(),\n ): Promise<ExtractedString[]> {\n // Find all files matching the pattern\n const files = await glob(pattern, {\n cwd: projectRoot,\n absolute: true,\n ignore: ['**/node_modules/**', '**/.next/**', '**/dist/**', '**/build/**'],\n });\n\n const allStrings: ExtractedString[] = [];\n\n // Extract from each file\n for (const file of files) {\n try {\n const strings = await this.extractFromFile(file);\n allStrings.push(...strings);\n } catch (error) {\n console.warn(`Warning: Failed to extract from ${file}:`, error);\n }\n }\n\n // Deduplicate strings (same text = one entry)\n const unique = this.deduplicateStrings(allStrings);\n\n return unique;\n }\n\n /**\n * Extract strings from a single file\n */\n private async extractFromFile(filePath: string): Promise<ExtractedString[]> {\n const code = readFileSync(filePath, 'utf-8');\n const strings: ExtractedString[] = [];\n\n try {\n // Parse the code\n const ast = parse(code, {\n sourceType: 'module',\n plugins: ['jsx', 'typescript'],\n });\n\n // Track imports from @vocoder/react\n const vocoderImports = new Map<string, string>();\n const tFunctionNames = new Set<string>(); // Track 't' function names\n\n // Traverse the AST\n traverse(ast, {\n // Track imports of <T> component and t function\n ImportDeclaration: (path) => {\n const source = path.node.source.value;\n\n if (source === '@vocoder/react') {\n path.node.specifiers.forEach((spec) => {\n if (spec.type === 'ImportSpecifier') {\n const imported =\n spec.imported.type === 'Identifier'\n ? spec.imported.name\n : null;\n const local = spec.local.name;\n\n if (imported === 'T') {\n vocoderImports.set(local, 'T');\n }\n // Track direct import of 't' function\n if (imported === 't') {\n tFunctionNames.add(local);\n }\n // Track useVocoder hook (which provides 't')\n if (imported === 'useVocoder') {\n // We'll track destructured 't' in VariableDeclarator\n }\n }\n });\n }\n },\n\n // Track destructured 't' from useVocoder hook\n VariableDeclarator: (path) => {\n const init = path.node.init;\n\n // Check if this is: const { t } = useVocoder()\n if (\n init &&\n init.type === 'CallExpression' &&\n init.callee.type === 'Identifier' &&\n init.callee.name === 'useVocoder' &&\n path.node.id.type === 'ObjectPattern'\n ) {\n path.node.id.properties.forEach((prop: any) => {\n if (\n prop.type === 'ObjectProperty' &&\n prop.key.type === 'Identifier' &&\n prop.key.name === 't'\n ) {\n const localName =\n prop.value.type === 'Identifier' ? prop.value.name : 't';\n tFunctionNames.add(localName);\n }\n });\n }\n },\n\n // Extract from t() function calls\n CallExpression: (path) => {\n const callee = path.node.callee;\n\n // Check if this is a call to 't' function\n const isTFunction =\n callee.type === 'Identifier' && tFunctionNames.has(callee.name);\n\n if (!isTFunction) return;\n\n // Get the first argument (the string to translate)\n const firstArg = path.node.arguments[0];\n if (!firstArg) return;\n\n let text: string | null = null;\n\n // Handle string literal: t('Hello')\n if (firstArg.type === 'StringLiteral') {\n text = firstArg.value;\n }\n // Handle template literal: t(`Hello ${name}`)\n else if (firstArg.type === 'TemplateLiteral') {\n text = this.extractTemplateText(firstArg);\n }\n\n if (!text || text.trim().length === 0) return;\n\n // Get options from second argument\n const secondArg = path.node.arguments[1];\n let context: string | undefined;\n let formality: 'formal' | 'informal' | 'auto' | undefined;\n\n if (secondArg && secondArg.type === 'ObjectExpression') {\n secondArg.properties.forEach((prop: any) => {\n if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {\n if (prop.key.name === 'context' && prop.value.type === 'StringLiteral') {\n context = prop.value.value;\n }\n if (prop.key.name === 'formality' && prop.value.type === 'StringLiteral') {\n formality = prop.value.value as 'formal' | 'informal' | 'auto';\n }\n }\n });\n }\n\n strings.push({\n text: text.trim(),\n file: filePath,\n line: path.node.loc?.start.line || 0,\n context,\n formality,\n });\n },\n\n // Extract from JSX elements\n JSXElement: (path) => {\n const opening = path.node.openingElement;\n const tagName =\n opening.name.type === 'JSXIdentifier'\n ? opening.name.name\n : null;\n\n if (!tagName) return;\n\n // Check if this is a <T> component (or aliased import)\n const isTranslationComponent = vocoderImports.has(tagName);\n\n if (!isTranslationComponent) return;\n\n // Extract text content\n const text = this.extractTextContent(path.node.children);\n\n if (!text || text.trim().length === 0) return;\n\n // Extract context and formality from props\n const context = this.getStringAttribute(opening.attributes, 'context');\n const formality = this.getStringAttribute(\n opening.attributes,\n 'formality',\n ) as 'formal' | 'informal' | 'auto' | undefined;\n\n strings.push({\n text: text.trim(),\n file: filePath,\n line: path.node.loc?.start.line || 0,\n context,\n formality,\n });\n },\n });\n } catch (error) {\n throw new Error(\n `Failed to parse ${filePath}: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n\n return strings;\n }\n\n /**\n * Extract text from template literal\n * Converts template literals like `Hello ${name}` to `Hello {name}`\n */\n private extractTemplateText(node: any): string {\n let text = '';\n\n for (let i = 0; i < node.quasis.length; i++) {\n const quasi = node.quasis[i];\n text += quasi.value.raw;\n\n // Add placeholder for expressions\n if (i < node.expressions.length) {\n const expr = node.expressions[i];\n if (expr.type === 'Identifier') {\n text += `{${expr.name}}`;\n } else {\n // For complex expressions, use generic placeholder\n text += '{value}';\n }\n }\n }\n\n return text;\n }\n\n /**\n * Extract text content from JSX children\n */\n private extractTextContent(children: any[]): string {\n let text = '';\n\n for (const child of children) {\n if (child.type === 'JSXText') {\n text += child.value;\n } else if (child.type === 'JSXExpressionContainer') {\n const expr = child.expression;\n\n // Handle {variableName} - actual identifier\n if (expr.type === 'Identifier') {\n text += `{${expr.name}}`;\n }\n // Handle {\"{variableName}\"} - string literal placeholder\n else if (expr.type === 'StringLiteral') {\n text += expr.value;\n }\n // Handle {`${variableName}`} - template literal\n // Convert template literal syntax to ICU MessageFormat: `$${price}` → ${price}\n else if (expr.type === 'TemplateLiteral') {\n text += this.extractTemplateText(expr);\n }\n }\n }\n\n return text;\n }\n\n /**\n * Get string value from JSX attribute\n */\n private getStringAttribute(\n attributes: any[],\n name: string,\n ): string | undefined {\n const attr = attributes.find(\n (a) => a.type === 'JSXAttribute' && a.name.name === name,\n );\n\n if (!attr || !attr.value) return undefined;\n\n if (attr.value.type === 'StringLiteral') {\n return attr.value.value;\n }\n\n return undefined;\n }\n\n /**\n * Deduplicate strings (keep first occurrence)\n */\n private deduplicateStrings(strings: ExtractedString[]): ExtractedString[] {\n const seen = new Set<string>();\n const unique: ExtractedString[] = [];\n\n for (const str of strings) {\n // Create a key based on text + context + formality\n const key = `${str.text}|${str.context || ''}|${str.formality || ''}`;\n\n if (!seen.has(key)) {\n seen.add(key);\n unique.push(str);\n }\n }\n\n return unique;\n }\n}\n"],"mappings":"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","names":["strings"]}
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
interface TranslateOptions {
|
|
2
|
+
branch?: string;
|
|
3
|
+
force?: boolean;
|
|
4
|
+
dryRun?: boolean;
|
|
5
|
+
verbose?: boolean;
|
|
6
|
+
maxAge?: number;
|
|
7
|
+
}
|
|
8
|
+
interface LocalConfig {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
apiUrl: string;
|
|
11
|
+
}
|
|
12
|
+
interface APIProjectConfig {
|
|
13
|
+
sourceLocale: string;
|
|
14
|
+
targetLocales: string[];
|
|
15
|
+
targetBranches: string[];
|
|
16
|
+
}
|
|
17
|
+
interface ProjectConfig extends LocalConfig, APIProjectConfig {
|
|
18
|
+
extractionPattern: string;
|
|
19
|
+
outputDir: string;
|
|
20
|
+
timeout: number;
|
|
21
|
+
}
|
|
22
|
+
interface ExtractedString {
|
|
23
|
+
text: string;
|
|
24
|
+
file: string;
|
|
25
|
+
line: number;
|
|
26
|
+
context?: string;
|
|
27
|
+
formality?: 'formal' | 'informal' | 'auto';
|
|
28
|
+
}
|
|
29
|
+
interface TranslationBatchResponse {
|
|
30
|
+
batchId: string;
|
|
31
|
+
newStrings: number;
|
|
32
|
+
deletedStrings?: number;
|
|
33
|
+
totalStrings: number;
|
|
34
|
+
status: 'PENDING' | 'TRANSLATING' | 'COMPLETED' | 'FAILED' | 'UP_TO_DATE';
|
|
35
|
+
noChanges?: boolean;
|
|
36
|
+
estimatedTime?: number;
|
|
37
|
+
translations?: Record<string, Record<string, string>>;
|
|
38
|
+
}
|
|
39
|
+
interface TranslationStatusResponse {
|
|
40
|
+
status: 'PENDING' | 'TRANSLATING' | 'COMPLETED' | 'FAILED';
|
|
41
|
+
progress: number;
|
|
42
|
+
jobs?: Array<{
|
|
43
|
+
locale: string;
|
|
44
|
+
status: string;
|
|
45
|
+
progress: number;
|
|
46
|
+
}>;
|
|
47
|
+
translations?: Record<string, Record<string, string>>;
|
|
48
|
+
localeMetadata?: Record<string, {
|
|
49
|
+
nativeName: string;
|
|
50
|
+
dir?: 'rtl';
|
|
51
|
+
}>;
|
|
52
|
+
errorMessage?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Main sync command
|
|
57
|
+
*
|
|
58
|
+
* Workflow:
|
|
59
|
+
* 1. Detect branch
|
|
60
|
+
* 2. Load project config
|
|
61
|
+
* 3. Check if target branch (skip if not)
|
|
62
|
+
* 4. Extract strings from source code
|
|
63
|
+
* 5. Submit to API for translation
|
|
64
|
+
* 6. Poll for completion
|
|
65
|
+
* 7. Write locale files to .vocoder/locales/
|
|
66
|
+
*/
|
|
67
|
+
declare function sync(options?: TranslateOptions): Promise<void>;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Detects the current git branch from multiple sources in priority order:
|
|
71
|
+
* 1. Explicit --branch flag (passed as parameter)
|
|
72
|
+
* 2. CI environment variables (GitHub Actions, Vercel, Netlify, etc.)
|
|
73
|
+
* 3. Git command (local development)
|
|
74
|
+
*
|
|
75
|
+
* @param override - Optional branch name to override detection
|
|
76
|
+
* @returns The current branch name
|
|
77
|
+
*/
|
|
78
|
+
declare function detectBranch(override?: string): string;
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Loads local configuration from environment variables
|
|
82
|
+
*
|
|
83
|
+
* Required environment variables:
|
|
84
|
+
* - VOCODER_API_KEY: Your Vocoder project API key
|
|
85
|
+
*
|
|
86
|
+
* Optional environment variables:
|
|
87
|
+
* - VOCODER_API_URL: Override API URL (default: https://vocoder.app)
|
|
88
|
+
*
|
|
89
|
+
* @returns Local configuration
|
|
90
|
+
*/
|
|
91
|
+
declare function getLocalConfig(): LocalConfig;
|
|
92
|
+
/**
|
|
93
|
+
* Validates the local configuration
|
|
94
|
+
*/
|
|
95
|
+
declare function validateLocalConfig(config: LocalConfig): void;
|
|
96
|
+
|
|
97
|
+
export { type APIProjectConfig, type ExtractedString, type LocalConfig, type ProjectConfig, type TranslateOptions, type TranslationBatchResponse, type TranslationStatusResponse, detectBranch, getLocalConfig, sync, validateLocalConfig };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@vocoder/cli",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "CLI tool for Vocoder translation workflow",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist"
|
|
9
|
+
],
|
|
10
|
+
"bin": {
|
|
11
|
+
"vocoder": "dist/bin.mjs"
|
|
12
|
+
},
|
|
13
|
+
"publishConfig": {
|
|
14
|
+
"access": "public"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "https://github.com/vocoder/vocoder-sdk.git",
|
|
19
|
+
"directory": "packages/cli"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"i18n",
|
|
23
|
+
"internationalization",
|
|
24
|
+
"translation",
|
|
25
|
+
"cli",
|
|
26
|
+
"localization",
|
|
27
|
+
"extraction",
|
|
28
|
+
"ast",
|
|
29
|
+
"babel"
|
|
30
|
+
],
|
|
31
|
+
"author": "Vocoder <admin@vocoder.app>",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"homepage": "https://github.com/vocoder/vocoder-sdk#readme",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/vocoder/vocoder-sdk/issues"
|
|
36
|
+
},
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"dev": "tsup --watch",
|
|
40
|
+
"watch": "tsup --watch",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"test:unit": "vitest run --exclude 'src/__tests__/integration/**'",
|
|
44
|
+
"test:integration": "vitest run src/__tests__/integration",
|
|
45
|
+
"test:integration:skip": "SKIP_INTEGRATION=true vitest run",
|
|
46
|
+
"lint": "eslint . --ext .ts",
|
|
47
|
+
"typecheck": "tsc --noEmit"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@babel/core": "^7.26.0",
|
|
51
|
+
"@babel/parser": "^7.26.0",
|
|
52
|
+
"@babel/traverse": "^7.26.0",
|
|
53
|
+
"@babel/types": "^7.26.0",
|
|
54
|
+
"@vocoder/types": "^0.1.0",
|
|
55
|
+
"chalk": "^5.3.0",
|
|
56
|
+
"commander": "^11.1.0",
|
|
57
|
+
"dotenv": "^16.3.1",
|
|
58
|
+
"glob": "^10.3.10",
|
|
59
|
+
"ora": "^8.0.1"
|
|
60
|
+
},
|
|
61
|
+
"devDependencies": {
|
|
62
|
+
"@types/babel__core": "^7.20.5",
|
|
63
|
+
"@types/babel__traverse": "^7.20.6",
|
|
64
|
+
"@types/node": "^20.19.9",
|
|
65
|
+
"tsup": "^8.0.0",
|
|
66
|
+
"typescript": "^5.4.0",
|
|
67
|
+
"vitest": "^1.0.0"
|
|
68
|
+
}
|
|
69
|
+
}
|