@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 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 (Premium)
37
- Synchronizes your local keys with the Kopynator Cloud. Uploads new keys and downloads approved translations.
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 import_chalk4 = __toESM(require("chalk"));
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.0.0");
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(import_chalk4.default.blue("\u{1F44B} Welcome to Kopynator CLI!"));
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.1.0",
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",
@@ -1,3 +1,4 @@
1
1
  export * from './init';
2
2
  export * from './check';
3
3
  export * from './sync';
4
+ export * from './upload';
@@ -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: 'What framework are you using?',
14
- choices: ['Angular', 'React', 'Vue', 'Other']
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.1.0');
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) {