@kopynator/cli 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/PUBLISH.md +59 -0
- package/README.md +15 -2
- package/dist/index.js +238 -7
- package/package.json +2 -1
- package/src/commands/index.ts +1 -0
- package/src/commands/init.ts +125 -2
- package/src/commands/upload.ts +145 -0
- package/src/index.ts +9 -2
package/PUBLISH.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Publicar @kopynator/cli en npm
|
|
2
|
+
|
|
3
|
+
## Requisitos
|
|
4
|
+
|
|
5
|
+
- Cuenta en [npmjs.com](https://www.npmjs.com) con acceso al scope `@kopynator` (o ser el propietario de la organización `kopynator`).
|
|
6
|
+
- `npm` instalado y sesión iniciada.
|
|
7
|
+
|
|
8
|
+
## Pasos
|
|
9
|
+
|
|
10
|
+
### 1. Iniciar sesión en npm (si no lo has hecho)
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm login
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
Te pedirá usuario, contraseña y email (o 2FA si lo tienes activado).
|
|
17
|
+
|
|
18
|
+
### 2. Compilar y publicar desde el paquete CLI
|
|
19
|
+
|
|
20
|
+
Desde la **raíz del monorepo** (para que las workspaces estén bien) o desde `packages/cli`:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
cd packages/cli
|
|
24
|
+
npm run build
|
|
25
|
+
npm publish
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- El script `prepublishOnly` ejecuta `npm run build` antes de publicar, así que `npm publish` ya deja el `dist/` actualizado.
|
|
29
|
+
- `publishConfig.access: "public"` en `package.json` hace que el paquete scoped `@kopynator/cli` sea público.
|
|
30
|
+
|
|
31
|
+
### 3. Comprobar la versión publicada
|
|
32
|
+
|
|
33
|
+
- En npm: https://www.npmjs.com/package/@kopynator/cli
|
|
34
|
+
- Desde terminal: `npm view @kopynator/cli version`
|
|
35
|
+
|
|
36
|
+
## Subir una nueva versión (después de cambios)
|
|
37
|
+
|
|
38
|
+
1. **Subir versión** (elegir una):
|
|
39
|
+
- Parche (1.3.0 → 1.3.1): `npm run release:patch`
|
|
40
|
+
- Minor (1.3.0 → 1.4.0): `npm run release:minor`
|
|
41
|
+
- Major (1.4.0 → 2.0.0): `npm run release:major`
|
|
42
|
+
|
|
43
|
+
O editar a mano `version` en `package.json` y en `src/index.ts` (`.version('X.Y.Z')`).
|
|
44
|
+
|
|
45
|
+
2. **Compilar y publicar**:
|
|
46
|
+
```bash
|
|
47
|
+
npm run build
|
|
48
|
+
npm publish
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
3. (Opcional) **Crear tag en Git**:
|
|
52
|
+
```bash
|
|
53
|
+
git tag @kopynator/cli@1.3.0
|
|
54
|
+
git push origin @kopynator/cli@1.3.0
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Versión actual
|
|
58
|
+
|
|
59
|
+
Tras añadir el comando `upload`, la versión del CLI es **1.3.0**.
|
package/README.md
CHANGED
|
@@ -33,13 +33,26 @@ Validates your local JSON translation files for syntax errors. Useful for CI/CD
|
|
|
33
33
|
npx -y @kopynator/cli check
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
-
### 3. Sync
|
|
37
|
-
|
|
36
|
+
### 3. Sync
|
|
37
|
+
Downloads translations from the Kopynator Cloud and saves them as local JSON files (e.g. `src/assets/i18n/en.json`).
|
|
38
38
|
|
|
39
39
|
```bash
|
|
40
40
|
npx -y @kopynator/cli sync
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
+
### 4. Upload
|
|
44
|
+
Uploads a JSON translation file to the Kopynator Cloud. Keys are merged/updated for the given language. Use the same API key (token) as in your app or `kopynator.config.json`.
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
# Language inferred from filename (es.json → es)
|
|
48
|
+
npx -y @kopynator/cli upload --file=src/assets/i18n/es.json
|
|
49
|
+
|
|
50
|
+
# Explicit language
|
|
51
|
+
npx -y @kopynator/cli upload --file=locales/en.json --lang=en
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
After uploading, run `sync` to download the latest state from the cloud if needed.
|
|
55
|
+
|
|
43
56
|
## Configuration
|
|
44
57
|
The `init` command creates a `kopynator.config.json` file in your root:
|
|
45
58
|
|
package/dist/index.js
CHANGED
|
@@ -25,7 +25,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
25
25
|
|
|
26
26
|
// src/index.ts
|
|
27
27
|
var import_commander = require("commander");
|
|
28
|
-
var
|
|
28
|
+
var import_chalk5 = __toESM(require("chalk"));
|
|
29
29
|
|
|
30
30
|
// src/commands/init.ts
|
|
31
31
|
var import_inquirer = __toESM(require("inquirer"));
|
|
@@ -34,12 +34,53 @@ var import_fs = __toESM(require("fs"));
|
|
|
34
34
|
var import_path = __toESM(require("path"));
|
|
35
35
|
async function initCommand() {
|
|
36
36
|
console.log(import_chalk.default.bold.blue("\n\u{1F680} Initializing Kopynator...\n"));
|
|
37
|
+
let detectedFramework = null;
|
|
38
|
+
let isReactNative = false;
|
|
39
|
+
let isIonic = false;
|
|
40
|
+
let ionicFramework = null;
|
|
41
|
+
const packageJsonPath = import_path.default.join(process.cwd(), "package.json");
|
|
42
|
+
if (import_fs.default.existsSync(packageJsonPath)) {
|
|
43
|
+
try {
|
|
44
|
+
const packageJson = JSON.parse(import_fs.default.readFileSync(packageJsonPath, "utf-8"));
|
|
45
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
46
|
+
if (deps["react-native"]) {
|
|
47
|
+
isReactNative = true;
|
|
48
|
+
detectedFramework = "React Native";
|
|
49
|
+
console.log(import_chalk.default.cyan("\u{1F4F1} Detected React Native project!"));
|
|
50
|
+
} else if (deps["@ionic/angular"] || deps["@ionic/react"] || deps["@ionic/vue"] || deps["@capacitor/core"]) {
|
|
51
|
+
isIonic = true;
|
|
52
|
+
if (deps["@ionic/angular"] || deps["@angular/core"]) {
|
|
53
|
+
ionicFramework = "Angular";
|
|
54
|
+
detectedFramework = "Ionic (Angular)";
|
|
55
|
+
} else if (deps["@ionic/react"] || deps["react"]) {
|
|
56
|
+
ionicFramework = "React";
|
|
57
|
+
detectedFramework = "Ionic (React)";
|
|
58
|
+
} else if (deps["@ionic/vue"] || deps["vue"]) {
|
|
59
|
+
ionicFramework = "Vue";
|
|
60
|
+
detectedFramework = "Ionic (Vue)";
|
|
61
|
+
}
|
|
62
|
+
console.log(import_chalk.default.magenta(`\u{1F50C} Detected ${detectedFramework} project!`));
|
|
63
|
+
} else if (deps["@angular/core"]) {
|
|
64
|
+
detectedFramework = "Angular";
|
|
65
|
+
console.log(import_chalk.default.red("\u{1F170}\uFE0F Detected Angular project!"));
|
|
66
|
+
} else if (deps["react"] && !deps["react-native"]) {
|
|
67
|
+
detectedFramework = "React";
|
|
68
|
+
console.log(import_chalk.default.blue("\u269B\uFE0F Detected React project!"));
|
|
69
|
+
} else if (deps["vue"]) {
|
|
70
|
+
detectedFramework = "Vue";
|
|
71
|
+
console.log(import_chalk.default.green("\u{1F7E2} Detected Vue project!"));
|
|
72
|
+
}
|
|
73
|
+
} catch (error) {
|
|
74
|
+
console.log(import_chalk.default.yellow("\u26A0\uFE0F Could not read package.json"));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
37
77
|
const answers = await import_inquirer.default.prompt([
|
|
38
78
|
{
|
|
39
79
|
type: "list",
|
|
40
80
|
name: "framework",
|
|
41
|
-
message: "What framework are you using?",
|
|
42
|
-
choices: ["Angular", "React", "Vue", "Other"]
|
|
81
|
+
message: detectedFramework ? `Detected ${detectedFramework}. Is this correct?` : "What framework are you using?",
|
|
82
|
+
choices: detectedFramework ? [detectedFramework, "Other"] : ["Angular", "React", "Vue", "React Native", "Ionic", "Other"],
|
|
83
|
+
default: detectedFramework || "React"
|
|
43
84
|
},
|
|
44
85
|
{
|
|
45
86
|
type: "input",
|
|
@@ -146,6 +187,70 @@ async function initCommand() {
|
|
|
146
187
|
console.log(import_chalk.default.yellow("\u26A0\uFE0F Could not find app.config.ts or app.module.ts. Falling back to config file."));
|
|
147
188
|
}
|
|
148
189
|
}
|
|
190
|
+
if (isReactNative || answers.framework === "React Native") {
|
|
191
|
+
console.log(import_chalk.default.green("\n\u2705 React Native Setup"));
|
|
192
|
+
console.log(import_chalk.default.cyan("\nInstall the package:"));
|
|
193
|
+
console.log(import_chalk.default.white(" npm install @kopynator/react"));
|
|
194
|
+
console.log(import_chalk.default.cyan("\nWrap your App.tsx:"));
|
|
195
|
+
console.log(import_chalk.default.gray(`
|
|
196
|
+
import { KopyProvider } from '@kopynator/react';
|
|
197
|
+
|
|
198
|
+
export default function App() {
|
|
199
|
+
return (
|
|
200
|
+
<KopyProvider config={{ apiKey: 'YOUR_API_KEY', languages: ${JSON.stringify(answers.languages)} }}>
|
|
201
|
+
<YourApp />
|
|
202
|
+
</KopyProvider>
|
|
203
|
+
);
|
|
204
|
+
}`));
|
|
205
|
+
console.log(import_chalk.default.yellow("\n\u{1F4CC} Get your API Key from: https://www.kopynator.com"));
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
if (isIonic) {
|
|
209
|
+
console.log(import_chalk.default.green(`
|
|
210
|
+
\u2705 Ionic (${ionicFramework}) Setup`));
|
|
211
|
+
console.log(import_chalk.default.magenta("\n\u2139\uFE0F Ionic/Capacitor apps use the standard web framework packages."));
|
|
212
|
+
if (ionicFramework === "Angular") {
|
|
213
|
+
console.log(import_chalk.default.cyan("\nInstall the package:"));
|
|
214
|
+
console.log(import_chalk.default.white(" npm install @kopynator/angular"));
|
|
215
|
+
console.log(import_chalk.default.cyan("\nAdd to your app.config.ts or app.module.ts:"));
|
|
216
|
+
console.log(import_chalk.default.gray(`
|
|
217
|
+
import { provideKopynator } from '@kopynator/angular';
|
|
218
|
+
|
|
219
|
+
providers: [
|
|
220
|
+
provideKopynator({
|
|
221
|
+
apiKey: 'YOUR_API_KEY',
|
|
222
|
+
mode: 'hybrid',
|
|
223
|
+
languages: ${JSON.stringify(answers.languages)}
|
|
224
|
+
})
|
|
225
|
+
]`));
|
|
226
|
+
} else if (ionicFramework === "React") {
|
|
227
|
+
console.log(import_chalk.default.cyan("\nInstall the package:"));
|
|
228
|
+
console.log(import_chalk.default.white(" npm install @kopynator/react"));
|
|
229
|
+
console.log(import_chalk.default.cyan("\nWrap your App:"));
|
|
230
|
+
console.log(import_chalk.default.gray(`
|
|
231
|
+
import { KopyProvider } from '@kopynator/react';
|
|
232
|
+
|
|
233
|
+
<KopyProvider config={{ apiKey: 'YOUR_API_KEY', languages: ${JSON.stringify(answers.languages)} }}>
|
|
234
|
+
<IonApp>...</IonApp>
|
|
235
|
+
</KopyProvider>`));
|
|
236
|
+
} else if (ionicFramework === "Vue") {
|
|
237
|
+
console.log(import_chalk.default.cyan("\nInstall the package:"));
|
|
238
|
+
console.log(import_chalk.default.white(" npm install @kopynator/vue"));
|
|
239
|
+
console.log(import_chalk.default.cyan("\nAdd to your main.ts:"));
|
|
240
|
+
console.log(import_chalk.default.gray(`
|
|
241
|
+
import { createKopy } from '@kopynator/vue';
|
|
242
|
+
|
|
243
|
+
const kopy = createKopy({
|
|
244
|
+
apiKey: 'YOUR_API_KEY',
|
|
245
|
+
defaultLocale: '${answers.defaultLocale}'
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
app.use(kopy);`));
|
|
249
|
+
}
|
|
250
|
+
console.log(import_chalk.default.yellow("\n\u{1F4CC} Get your API Key from: https://www.kopynator.com"));
|
|
251
|
+
console.log(import_chalk.default.green("\n\u{1F4A1} Pro Tip: Your translations will work seamlessly across iOS, Android, and web!"));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
149
254
|
const configContent = {
|
|
150
255
|
apiKey: "YOUR_API_KEY_HERE",
|
|
151
256
|
defaultLocale: answers.defaultLocale,
|
|
@@ -339,7 +444,11 @@ async function syncCommand() {
|
|
|
339
444
|
try {
|
|
340
445
|
spinner.text = "\u{1F4E1} Fetching available languages...";
|
|
341
446
|
const languagesUrl = `${baseUrl}/languages?token=${token}`;
|
|
342
|
-
const languagesResponse = await fetch(languagesUrl
|
|
447
|
+
const languagesResponse = await fetch(languagesUrl, {
|
|
448
|
+
headers: {
|
|
449
|
+
"x-kopynator-version": "1.1.0"
|
|
450
|
+
}
|
|
451
|
+
});
|
|
343
452
|
if (!languagesResponse.ok) {
|
|
344
453
|
spinner.fail(`Failed to fetch languages: ${languagesResponse.status} ${languagesResponse.statusText}`);
|
|
345
454
|
return;
|
|
@@ -355,7 +464,11 @@ async function syncCommand() {
|
|
|
355
464
|
const downloadSpinner = (0, import_ora.default)(`\u2B07\uFE0F Downloading ${locale}...`).start();
|
|
356
465
|
const fetchUrl = `${baseUrl}/fetch?token=${token}&langs=${locale}&nested=${syncConfig.nested}&includeLangKey=${syncConfig.includeLangKey}&pretty=${syncConfig.pretty}&indent=${syncConfig.indent}`;
|
|
357
466
|
try {
|
|
358
|
-
const translationResponse = await fetch(fetchUrl
|
|
467
|
+
const translationResponse = await fetch(fetchUrl, {
|
|
468
|
+
headers: {
|
|
469
|
+
"x-kopynator-version": "1.1.0"
|
|
470
|
+
}
|
|
471
|
+
});
|
|
359
472
|
if (!translationResponse.ok) {
|
|
360
473
|
downloadSpinner.warn(`Failed to fetch ${locale}: ${translationResponse.status}`);
|
|
361
474
|
continue;
|
|
@@ -381,14 +494,132 @@ async function syncCommand() {
|
|
|
381
494
|
}
|
|
382
495
|
}
|
|
383
496
|
|
|
497
|
+
// src/commands/upload.ts
|
|
498
|
+
var import_chalk4 = __toESM(require("chalk"));
|
|
499
|
+
var import_fs4 = __toESM(require("fs"));
|
|
500
|
+
var import_path4 = __toESM(require("path"));
|
|
501
|
+
var import_ora2 = __toESM(require("ora"));
|
|
502
|
+
var BATCH_SIZE = 500;
|
|
503
|
+
function flatten(data, prefix = "") {
|
|
504
|
+
const result = {};
|
|
505
|
+
for (const key in data) {
|
|
506
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
507
|
+
const value = data[key];
|
|
508
|
+
if (value !== null && typeof value === "object" && !Array.isArray(value)) {
|
|
509
|
+
Object.assign(result, flatten(value, fullKey));
|
|
510
|
+
} else {
|
|
511
|
+
result[fullKey] = String(value);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
return result;
|
|
515
|
+
}
|
|
516
|
+
function extractApiKey2() {
|
|
517
|
+
const appConfigPath = import_path4.default.join(process.cwd(), "src/app/app.config.ts");
|
|
518
|
+
const appModulePath = import_path4.default.join(process.cwd(), "src/app/app.module.ts");
|
|
519
|
+
const jsonConfigPath = import_path4.default.join(process.cwd(), "kopynator.config.json");
|
|
520
|
+
if (import_fs4.default.existsSync(appConfigPath)) {
|
|
521
|
+
const content = import_fs4.default.readFileSync(appConfigPath, "utf-8");
|
|
522
|
+
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
523
|
+
if (apiKeyMatch) return { apiKey: apiKeyMatch[1] };
|
|
524
|
+
}
|
|
525
|
+
if (import_fs4.default.existsSync(appModulePath)) {
|
|
526
|
+
const content = import_fs4.default.readFileSync(appModulePath, "utf-8");
|
|
527
|
+
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
528
|
+
if (apiKeyMatch) return { apiKey: apiKeyMatch[1] };
|
|
529
|
+
}
|
|
530
|
+
if (import_fs4.default.existsSync(jsonConfigPath)) {
|
|
531
|
+
try {
|
|
532
|
+
const config = JSON.parse(import_fs4.default.readFileSync(jsonConfigPath, "utf-8"));
|
|
533
|
+
if (config.apiKey) return { apiKey: config.apiKey, baseUrl: config.baseUrl };
|
|
534
|
+
} catch {
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
function inferLangFromFile(filePath) {
|
|
540
|
+
const base = import_path4.default.basename(filePath, import_path4.default.extname(filePath));
|
|
541
|
+
return base;
|
|
542
|
+
}
|
|
543
|
+
async function uploadCommand(options) {
|
|
544
|
+
console.log(import_chalk4.default.bold.blue("\n\u{1F4E4} Upload translations to Kopynator Cloud...\n"));
|
|
545
|
+
const config = extractApiKey2();
|
|
546
|
+
if (!config) {
|
|
547
|
+
console.log(import_chalk4.default.red("\u274C Could not find API key. Run `npx kopynator init` or set apiKey in kopynator.config.json / app.config.ts"));
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const fileOption = options.file;
|
|
551
|
+
if (!fileOption) {
|
|
552
|
+
console.log(import_chalk4.default.red("\u274C Missing --file. Example: npx kopynator upload --file=es.json [--lang=es]"));
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
const resolvedPath = import_path4.default.isAbsolute(fileOption) ? fileOption : import_path4.default.join(process.cwd(), fileOption);
|
|
556
|
+
if (!import_fs4.default.existsSync(resolvedPath)) {
|
|
557
|
+
console.log(import_chalk4.default.red(`\u274C File not found: ${resolvedPath}`));
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
const lang = options.lang || inferLangFromFile(resolvedPath);
|
|
561
|
+
const baseUrl = config.baseUrl || "https://api.kopynator.com/api/tokens";
|
|
562
|
+
const token = config.apiKey;
|
|
563
|
+
let raw;
|
|
564
|
+
try {
|
|
565
|
+
raw = JSON.parse(import_fs4.default.readFileSync(resolvedPath, "utf-8"));
|
|
566
|
+
} catch (e) {
|
|
567
|
+
console.log(import_chalk4.default.red(`\u274C Invalid JSON: ${resolvedPath}`));
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
571
|
+
console.log(import_chalk4.default.red("\u274C JSON root must be an object (key-value)."));
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const flat = flatten(raw);
|
|
575
|
+
const entries = Object.entries(flat);
|
|
576
|
+
const total = entries.length;
|
|
577
|
+
if (total === 0) {
|
|
578
|
+
console.log(import_chalk4.default.yellow("\u26A0\uFE0F No keys to upload."));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
const batches = [];
|
|
582
|
+
for (let i = 0; i < total; i += BATCH_SIZE) {
|
|
583
|
+
batches.push(Object.fromEntries(entries.slice(i, i + BATCH_SIZE)));
|
|
584
|
+
}
|
|
585
|
+
const spinner = (0, import_ora2.default)(`Uploading ${total} keys (${lang})...`).start();
|
|
586
|
+
try {
|
|
587
|
+
let imported = 0;
|
|
588
|
+
for (let i = 0; i < batches.length; i++) {
|
|
589
|
+
spinner.text = `Uploading batch ${i + 1}/${batches.length}...`;
|
|
590
|
+
const res = await fetch(`${baseUrl}/import`, {
|
|
591
|
+
method: "POST",
|
|
592
|
+
headers: {
|
|
593
|
+
"Content-Type": "application/json",
|
|
594
|
+
"x-api-token": token,
|
|
595
|
+
"x-kopynator-version": "1.2.0"
|
|
596
|
+
},
|
|
597
|
+
body: JSON.stringify({ lang, data: batches[i] })
|
|
598
|
+
});
|
|
599
|
+
if (!res.ok) {
|
|
600
|
+
const errText = await res.text();
|
|
601
|
+
spinner.fail(`Upload failed: ${res.status} ${errText}`);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
const result = await res.json();
|
|
605
|
+
imported += result.importedCount ?? batches[i].length;
|
|
606
|
+
}
|
|
607
|
+
spinner.succeed(import_chalk4.default.green(`Uploaded ${imported} keys for language "${lang}".`));
|
|
608
|
+
console.log(import_chalk4.default.bold.green("\n\u{1F389} Upload completed. Run `npx kopynator sync` to pull latest from cloud.\n"));
|
|
609
|
+
} catch (error) {
|
|
610
|
+
spinner.fail(`Upload failed: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
384
614
|
// src/index.ts
|
|
385
615
|
var program = new import_commander.Command();
|
|
386
|
-
program.name("kopynator").description("Kopynator CLI - Manage your i18n workflow").version("1.
|
|
616
|
+
program.name("kopynator").description("Kopynator CLI - Manage your i18n workflow").version("1.3.0");
|
|
387
617
|
program.command("init").description("Initialize Kopynator in your project").action(initCommand);
|
|
388
618
|
program.command("check").description("Validate your local JSON translation files").action(checkCommand);
|
|
389
619
|
program.command("sync").description("Sync your translations with the Kopynator Cloud").action(syncCommand);
|
|
620
|
+
program.command("upload").description("Upload a JSON translation file to Kopynator Cloud").option("-f, --file <path>", "Path to the JSON file (e.g. es.json)").option("-l, --lang <code>", "Language code (default: inferred from filename)").action((opts) => uploadCommand({ file: opts.file, lang: opts.lang }));
|
|
390
621
|
program.parse(process.argv);
|
|
391
622
|
if (!process.argv.slice(2).length) {
|
|
392
|
-
console.log(
|
|
623
|
+
console.log(import_chalk5.default.blue("\u{1F44B} Welcome to Kopynator CLI!"));
|
|
393
624
|
program.outputHelp();
|
|
394
625
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kopynator/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"description": "CLI tool for Kopynator - The i18n management solution",
|
|
5
5
|
"bin": {
|
|
6
6
|
"kopynator": "dist/index.js"
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
"main": "./dist/index.js",
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "tsup src/index.ts --format cjs --dts --clean",
|
|
11
|
+
"prepublishOnly": "npm run build",
|
|
11
12
|
"dev": "tsup src/index.ts --format cjs --dts --watch",
|
|
12
13
|
"release:patch": "npm version patch",
|
|
13
14
|
"release:minor": "npm version minor",
|
package/src/commands/index.ts
CHANGED
package/src/commands/init.ts
CHANGED
|
@@ -6,12 +6,66 @@ import path from 'path';
|
|
|
6
6
|
export async function initCommand() {
|
|
7
7
|
console.log(chalk.bold.blue('\n🚀 Initializing Kopynator...\n'));
|
|
8
8
|
|
|
9
|
+
// Auto-detect framework from package.json
|
|
10
|
+
let detectedFramework: string | null = null;
|
|
11
|
+
let isReactNative = false;
|
|
12
|
+
let isIonic = false;
|
|
13
|
+
let ionicFramework: string | null = null;
|
|
14
|
+
|
|
15
|
+
const packageJsonPath = path.join(process.cwd(), 'package.json');
|
|
16
|
+
if (fs.existsSync(packageJsonPath)) {
|
|
17
|
+
try {
|
|
18
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
19
|
+
const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
|
|
20
|
+
|
|
21
|
+
// Check for React Native
|
|
22
|
+
if (deps['react-native']) {
|
|
23
|
+
isReactNative = true;
|
|
24
|
+
detectedFramework = 'React Native';
|
|
25
|
+
console.log(chalk.cyan('📱 Detected React Native project!'));
|
|
26
|
+
}
|
|
27
|
+
// Check for Ionic/Capacitor
|
|
28
|
+
else if (deps['@ionic/angular'] || deps['@ionic/react'] || deps['@ionic/vue'] || deps['@capacitor/core']) {
|
|
29
|
+
isIonic = true;
|
|
30
|
+
if (deps['@ionic/angular'] || deps['@angular/core']) {
|
|
31
|
+
ionicFramework = 'Angular';
|
|
32
|
+
detectedFramework = 'Ionic (Angular)';
|
|
33
|
+
} else if (deps['@ionic/react'] || deps['react']) {
|
|
34
|
+
ionicFramework = 'React';
|
|
35
|
+
detectedFramework = 'Ionic (React)';
|
|
36
|
+
} else if (deps['@ionic/vue'] || deps['vue']) {
|
|
37
|
+
ionicFramework = 'Vue';
|
|
38
|
+
detectedFramework = 'Ionic (Vue)';
|
|
39
|
+
}
|
|
40
|
+
console.log(chalk.magenta(`🔌 Detected ${detectedFramework} project!`));
|
|
41
|
+
}
|
|
42
|
+
// Check for standard frameworks
|
|
43
|
+
else if (deps['@angular/core']) {
|
|
44
|
+
detectedFramework = 'Angular';
|
|
45
|
+
console.log(chalk.red('🅰️ Detected Angular project!'));
|
|
46
|
+
} else if (deps['react'] && !deps['react-native']) {
|
|
47
|
+
detectedFramework = 'React';
|
|
48
|
+
console.log(chalk.blue('⚛️ Detected React project!'));
|
|
49
|
+
} else if (deps['vue']) {
|
|
50
|
+
detectedFramework = 'Vue';
|
|
51
|
+
console.log(chalk.green('🟢 Detected Vue project!'));
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
console.log(chalk.yellow('⚠️ Could not read package.json'));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
9
58
|
const answers = await inquirer.prompt([
|
|
10
59
|
{
|
|
11
60
|
type: 'list',
|
|
12
61
|
name: 'framework',
|
|
13
|
-
message:
|
|
14
|
-
|
|
62
|
+
message: detectedFramework
|
|
63
|
+
? `Detected ${detectedFramework}. Is this correct?`
|
|
64
|
+
: 'What framework are you using?',
|
|
65
|
+
choices: detectedFramework
|
|
66
|
+
? [detectedFramework, 'Other']
|
|
67
|
+
: ['Angular', 'React', 'Vue', 'React Native', 'Ionic', 'Other'],
|
|
68
|
+
default: detectedFramework || 'React'
|
|
15
69
|
},
|
|
16
70
|
{
|
|
17
71
|
type: 'input',
|
|
@@ -133,6 +187,75 @@ export async function initCommand() {
|
|
|
133
187
|
}
|
|
134
188
|
}
|
|
135
189
|
|
|
190
|
+
// React Native specific instructions
|
|
191
|
+
if (isReactNative || answers.framework === 'React Native') {
|
|
192
|
+
console.log(chalk.green('\n✅ React Native Setup'));
|
|
193
|
+
console.log(chalk.cyan('\nInstall the package:'));
|
|
194
|
+
console.log(chalk.white(' npm install @kopynator/react'));
|
|
195
|
+
console.log(chalk.cyan('\nWrap your App.tsx:'));
|
|
196
|
+
console.log(chalk.gray(`
|
|
197
|
+
import { KopyProvider } from '@kopynator/react';
|
|
198
|
+
|
|
199
|
+
export default function App() {
|
|
200
|
+
return (
|
|
201
|
+
<KopyProvider config={{ apiKey: 'YOUR_API_KEY', languages: ${JSON.stringify(answers.languages)} }}>
|
|
202
|
+
<YourApp />
|
|
203
|
+
</KopyProvider>
|
|
204
|
+
);
|
|
205
|
+
}`));
|
|
206
|
+
console.log(chalk.yellow('\n📌 Get your API Key from: https://www.kopynator.com'));
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Ionic/Capacitor specific instructions
|
|
211
|
+
if (isIonic) {
|
|
212
|
+
console.log(chalk.green(`\n✅ Ionic (${ionicFramework}) Setup`));
|
|
213
|
+
console.log(chalk.magenta('\nℹ️ Ionic/Capacitor apps use the standard web framework packages.'));
|
|
214
|
+
|
|
215
|
+
if (ionicFramework === 'Angular') {
|
|
216
|
+
console.log(chalk.cyan('\nInstall the package:'));
|
|
217
|
+
console.log(chalk.white(' npm install @kopynator/angular'));
|
|
218
|
+
console.log(chalk.cyan('\nAdd to your app.config.ts or app.module.ts:'));
|
|
219
|
+
console.log(chalk.gray(`
|
|
220
|
+
import { provideKopynator } from '@kopynator/angular';
|
|
221
|
+
|
|
222
|
+
providers: [
|
|
223
|
+
provideKopynator({
|
|
224
|
+
apiKey: 'YOUR_API_KEY',
|
|
225
|
+
mode: 'hybrid',
|
|
226
|
+
languages: ${JSON.stringify(answers.languages)}
|
|
227
|
+
})
|
|
228
|
+
]`));
|
|
229
|
+
} else if (ionicFramework === 'React') {
|
|
230
|
+
console.log(chalk.cyan('\nInstall the package:'));
|
|
231
|
+
console.log(chalk.white(' npm install @kopynator/react'));
|
|
232
|
+
console.log(chalk.cyan('\nWrap your App:'));
|
|
233
|
+
console.log(chalk.gray(`
|
|
234
|
+
import { KopyProvider } from '@kopynator/react';
|
|
235
|
+
|
|
236
|
+
<KopyProvider config={{ apiKey: 'YOUR_API_KEY', languages: ${JSON.stringify(answers.languages)} }}>
|
|
237
|
+
<IonApp>...</IonApp>
|
|
238
|
+
</KopyProvider>`));
|
|
239
|
+
} else if (ionicFramework === 'Vue') {
|
|
240
|
+
console.log(chalk.cyan('\nInstall the package:'));
|
|
241
|
+
console.log(chalk.white(' npm install @kopynator/vue'));
|
|
242
|
+
console.log(chalk.cyan('\nAdd to your main.ts:'));
|
|
243
|
+
console.log(chalk.gray(`
|
|
244
|
+
import { createKopy } from '@kopynator/vue';
|
|
245
|
+
|
|
246
|
+
const kopy = createKopy({
|
|
247
|
+
apiKey: 'YOUR_API_KEY',
|
|
248
|
+
defaultLocale: '${answers.defaultLocale}'
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
app.use(kopy);`));
|
|
252
|
+
}
|
|
253
|
+
console.log(chalk.yellow('\n📌 Get your API Key from: https://www.kopynator.com'));
|
|
254
|
+
console.log(chalk.green('\n💡 Pro Tip: Your translations will work seamlessly across iOS, Android, and web!'));
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
|
|
136
259
|
// Fallback: Creates a basic config file
|
|
137
260
|
const configContent = {
|
|
138
261
|
apiKey: "YOUR_API_KEY_HERE",
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
|
|
6
|
+
const BATCH_SIZE = 500;
|
|
7
|
+
|
|
8
|
+
interface KopyConfig {
|
|
9
|
+
apiKey: string;
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Flatten nested JSON to dot-notation keys (same as backend/frontend).
|
|
15
|
+
*/
|
|
16
|
+
function flatten(data: Record<string, unknown>, prefix = ''): Record<string, string> {
|
|
17
|
+
const result: Record<string, string> = {};
|
|
18
|
+
for (const key in data) {
|
|
19
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
20
|
+
const value = data[key];
|
|
21
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
22
|
+
Object.assign(result, flatten(value as Record<string, unknown>, fullKey));
|
|
23
|
+
} else {
|
|
24
|
+
result[fullKey] = String(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractApiKey(): KopyConfig | null {
|
|
31
|
+
const appConfigPath = path.join(process.cwd(), 'src/app/app.config.ts');
|
|
32
|
+
const appModulePath = path.join(process.cwd(), 'src/app/app.module.ts');
|
|
33
|
+
const jsonConfigPath = path.join(process.cwd(), 'kopynator.config.json');
|
|
34
|
+
|
|
35
|
+
if (fs.existsSync(appConfigPath)) {
|
|
36
|
+
const content = fs.readFileSync(appConfigPath, 'utf-8');
|
|
37
|
+
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
38
|
+
if (apiKeyMatch) return { apiKey: apiKeyMatch[1] };
|
|
39
|
+
}
|
|
40
|
+
if (fs.existsSync(appModulePath)) {
|
|
41
|
+
const content = fs.readFileSync(appModulePath, 'utf-8');
|
|
42
|
+
const apiKeyMatch = content.match(/apiKey:\s*['"]([^'"]+)['"]/);
|
|
43
|
+
if (apiKeyMatch) return { apiKey: apiKeyMatch[1] };
|
|
44
|
+
}
|
|
45
|
+
if (fs.existsSync(jsonConfigPath)) {
|
|
46
|
+
try {
|
|
47
|
+
const config = JSON.parse(fs.readFileSync(jsonConfigPath, 'utf-8'));
|
|
48
|
+
if (config.apiKey) return { apiKey: config.apiKey, baseUrl: config.baseUrl };
|
|
49
|
+
} catch {
|
|
50
|
+
// ignore
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Infer language code from filename (e.g. es.json -> es, en-US.json -> en-US).
|
|
58
|
+
*/
|
|
59
|
+
function inferLangFromFile(filePath: string): string {
|
|
60
|
+
const base = path.basename(filePath, path.extname(filePath));
|
|
61
|
+
return base;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function uploadCommand(options: { file?: string; lang?: string }) {
|
|
65
|
+
console.log(chalk.bold.blue('\n📤 Upload translations to Kopynator Cloud...\n'));
|
|
66
|
+
|
|
67
|
+
const config = extractApiKey();
|
|
68
|
+
if (!config) {
|
|
69
|
+
console.log(chalk.red('❌ Could not find API key. Run `npx kopynator init` or set apiKey in kopynator.config.json / app.config.ts'));
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const fileOption = options.file;
|
|
74
|
+
if (!fileOption) {
|
|
75
|
+
console.log(chalk.red('❌ Missing --file. Example: npx kopynator upload --file=es.json [--lang=es]'));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const resolvedPath = path.isAbsolute(fileOption) ? fileOption : path.join(process.cwd(), fileOption);
|
|
80
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
81
|
+
console.log(chalk.red(`❌ File not found: ${resolvedPath}`));
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const lang = options.lang || inferLangFromFile(resolvedPath);
|
|
86
|
+
const baseUrl = config.baseUrl || 'https://api.kopynator.com/api/tokens';
|
|
87
|
+
const token = config.apiKey;
|
|
88
|
+
|
|
89
|
+
let raw: unknown;
|
|
90
|
+
try {
|
|
91
|
+
raw = JSON.parse(fs.readFileSync(resolvedPath, 'utf-8'));
|
|
92
|
+
} catch (e) {
|
|
93
|
+
console.log(chalk.red(`❌ Invalid JSON: ${resolvedPath}`));
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
98
|
+
console.log(chalk.red('❌ JSON root must be an object (key-value).'));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const flat = flatten(raw as Record<string, unknown>);
|
|
103
|
+
const entries = Object.entries(flat);
|
|
104
|
+
const total = entries.length;
|
|
105
|
+
if (total === 0) {
|
|
106
|
+
console.log(chalk.yellow('⚠️ No keys to upload.'));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const batches: Record<string, string>[] = [];
|
|
111
|
+
for (let i = 0; i < total; i += BATCH_SIZE) {
|
|
112
|
+
batches.push(Object.fromEntries(entries.slice(i, i + BATCH_SIZE)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const spinner = ora(`Uploading ${total} keys (${lang})...`).start();
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
let imported = 0;
|
|
119
|
+
for (let i = 0; i < batches.length; i++) {
|
|
120
|
+
spinner.text = `Uploading batch ${i + 1}/${batches.length}...`;
|
|
121
|
+
const res = await fetch(`${baseUrl}/import`, {
|
|
122
|
+
method: 'POST',
|
|
123
|
+
headers: {
|
|
124
|
+
'Content-Type': 'application/json',
|
|
125
|
+
'x-api-token': token,
|
|
126
|
+
'x-kopynator-version': '1.2.0',
|
|
127
|
+
},
|
|
128
|
+
body: JSON.stringify({ lang, data: batches[i] }),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
if (!res.ok) {
|
|
132
|
+
const errText = await res.text();
|
|
133
|
+
spinner.fail(`Upload failed: ${res.status} ${errText}`);
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const result = (await res.json()) as { importedCount?: number };
|
|
137
|
+
imported += result.importedCount ?? batches[i].length;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
spinner.succeed(chalk.green(`Uploaded ${imported} keys for language "${lang}".`));
|
|
141
|
+
console.log(chalk.bold.green('\n🎉 Upload completed. Run `npx kopynator sync` to pull latest from cloud.\n'));
|
|
142
|
+
} catch (error) {
|
|
143
|
+
spinner.fail(`Upload failed: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
|
144
|
+
}
|
|
145
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
-
import { initCommand, checkCommand, syncCommand } from './commands';
|
|
4
|
+
import { initCommand, checkCommand, syncCommand, uploadCommand } from './commands';
|
|
5
5
|
|
|
6
6
|
const program = new Command();
|
|
7
7
|
|
|
8
8
|
program
|
|
9
9
|
.name('kopynator')
|
|
10
10
|
.description('Kopynator CLI - Manage your i18n workflow')
|
|
11
|
-
.version('1.
|
|
11
|
+
.version('1.3.0');
|
|
12
12
|
|
|
13
13
|
program
|
|
14
14
|
.command('init')
|
|
@@ -25,6 +25,13 @@ program
|
|
|
25
25
|
.description('Sync your translations with the Kopynator Cloud')
|
|
26
26
|
.action(syncCommand);
|
|
27
27
|
|
|
28
|
+
program
|
|
29
|
+
.command('upload')
|
|
30
|
+
.description('Upload a JSON translation file to Kopynator Cloud')
|
|
31
|
+
.option('-f, --file <path>', 'Path to the JSON file (e.g. es.json)')
|
|
32
|
+
.option('-l, --lang <code>', 'Language code (default: inferred from filename)')
|
|
33
|
+
.action((opts) => uploadCommand({ file: opts.file, lang: opts.lang }));
|
|
34
|
+
|
|
28
35
|
program.parse(process.argv);
|
|
29
36
|
|
|
30
37
|
if (!process.argv.slice(2).length) {
|