@contentstorage/core 3.0.1 → 3.2.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/README.md +82 -10
- package/dist/commands/cli.js +39 -2
- package/dist/commands/pull.js +66 -4
- package/dist/commands/push.d.ts +2 -0
- package/dist/commands/push.js +156 -0
- package/dist/commands/stats.js +94 -3
- package/dist/commands/translate.d.ts +2 -0
- package/dist/commands/translate.js +244 -0
- package/dist/core/api-client.d.ts +122 -0
- package/dist/core/api-client.js +118 -0
- package/dist/core/config-loader.js +3 -0
- package/dist/types.d.ts +2 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -11,11 +11,12 @@ Contentstorage Core is a powerful CLI tool for managing translations and content
|
|
|
11
11
|
|
|
12
12
|
## Features
|
|
13
13
|
|
|
14
|
-
- **Translation Management** - Pull content from Contentstorage
|
|
14
|
+
- **Translation Management** - Pull and push content to/from Contentstorage
|
|
15
15
|
- **TypeScript Generation** - Automatic type generation from your content
|
|
16
16
|
- **Translation Statistics** - Analyze translation completeness across languages
|
|
17
17
|
- **Multi-Language Support** - Built-in support for 40+ languages
|
|
18
18
|
- **CLI Tools** - Professional command-line interface
|
|
19
|
+
- **API Key Authentication** - Secure project-level API keys for CI/CD integration
|
|
19
20
|
- **Plugin Ecosystem** - Integrate with i18next, react-intl, vue-i18n, and more
|
|
20
21
|
|
|
21
22
|
## Installation
|
|
@@ -38,13 +39,21 @@ Create `contentstorage.config.js` in your project root:
|
|
|
38
39
|
|
|
39
40
|
```javascript
|
|
40
41
|
export default {
|
|
41
|
-
|
|
42
|
+
// Option A: Content Key (read-only access)
|
|
43
|
+
contentKey: 'your-team-id/your-project-id',
|
|
44
|
+
|
|
45
|
+
// Option B: API Key (read + write access) - recommended for CI/CD
|
|
46
|
+
// apiKey: 'csk_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx',
|
|
47
|
+
// projectId: 'your-project-uuid',
|
|
48
|
+
|
|
42
49
|
languageCodes: ['EN', 'FR', 'DE'],
|
|
43
50
|
contentDir: 'src/content/json',
|
|
44
51
|
typesOutputFile: 'src/content/content-types.d.ts'
|
|
45
52
|
};
|
|
46
53
|
```
|
|
47
54
|
|
|
55
|
+
> **Note:** Use `contentKey` for read-only access (pull, stats). Use `apiKey` + `projectId` for full access including push operations. API keys can be created in Project Settings → API Keys.
|
|
56
|
+
|
|
48
57
|
### 2. Pull Content & Generate Types
|
|
49
58
|
|
|
50
59
|
```bash
|
|
@@ -85,18 +94,59 @@ npx contentstorage pull [options]
|
|
|
85
94
|
```
|
|
86
95
|
|
|
87
96
|
**Options:**
|
|
88
|
-
- `--content-key <key>` - Content key
|
|
97
|
+
- `--content-key <key>` - Content key (read-only access)
|
|
98
|
+
- `--api-key <key>` - API key (read + write access)
|
|
99
|
+
- `--project-id <id>` - Project ID (required with --api-key)
|
|
89
100
|
- `--content-dir <dir>` - Directory to save content files
|
|
90
101
|
- `--lang <code>` - Language code (e.g., EN, FR)
|
|
91
102
|
- `--pending-changes` - Fetch pending/draft content
|
|
103
|
+
- `--flatten` - Output flattened key-value pairs
|
|
92
104
|
|
|
93
105
|
**Examples:**
|
|
94
106
|
```bash
|
|
95
|
-
|
|
107
|
+
# Using content key (read-only)
|
|
108
|
+
npx contentstorage pull --content-key teamId/projectId
|
|
109
|
+
|
|
110
|
+
# Using API key (recommended for CI/CD)
|
|
111
|
+
npx contentstorage pull --api-key csk_xxx --project-id uuid
|
|
112
|
+
|
|
113
|
+
# Pull specific language with draft content
|
|
96
114
|
npx contentstorage pull --lang EN --pending-changes
|
|
97
|
-
npx contentstorage pull --content-dir src/content
|
|
98
115
|
```
|
|
99
116
|
|
|
117
|
+
### `contentstorage push`
|
|
118
|
+
|
|
119
|
+
Push local content changes to Contentstorage (requires API key)
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
npx contentstorage push [options]
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
**Options:**
|
|
126
|
+
- `--api-key <key>` - API key (required)
|
|
127
|
+
- `--project-id <id>` - Project ID (required)
|
|
128
|
+
- `--content-dir <dir>` - Directory with content files
|
|
129
|
+
- `--lang <code>` - Language code to push (e.g., EN)
|
|
130
|
+
- `--dry-run` - Preview changes without applying
|
|
131
|
+
|
|
132
|
+
**Examples:**
|
|
133
|
+
```bash
|
|
134
|
+
# Push all configured languages
|
|
135
|
+
npx contentstorage push --api-key csk_xxx --project-id uuid
|
|
136
|
+
|
|
137
|
+
# Push specific language
|
|
138
|
+
npx contentstorage push --api-key csk_xxx --project-id uuid --lang EN
|
|
139
|
+
|
|
140
|
+
# Preview changes without applying
|
|
141
|
+
npx contentstorage push --api-key csk_xxx --project-id uuid --dry-run
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
**Behavior:**
|
|
145
|
+
- Adds new keys that don't exist in Contentstorage
|
|
146
|
+
- Updates existing keys with different values
|
|
147
|
+
- Never removes keys (safe by design)
|
|
148
|
+
- If change tracking is enabled, creates pending changes for review
|
|
149
|
+
|
|
100
150
|
### `contentstorage generate-types`
|
|
101
151
|
|
|
102
152
|
Generate TypeScript type definitions from content
|
|
@@ -126,15 +176,20 @@ npx contentstorage stats [options]
|
|
|
126
176
|
```
|
|
127
177
|
|
|
128
178
|
**Options:**
|
|
129
|
-
- `--content-key <key>` - Content key
|
|
179
|
+
- `--content-key <key>` - Content key (read-only access)
|
|
180
|
+
- `--api-key <key>` - API key (fetches stats directly from API)
|
|
181
|
+
- `--project-id <id>` - Project ID (required with --api-key)
|
|
130
182
|
- `--content-dir <dir>` - Directory with content files
|
|
131
183
|
- `--pending-changes` - Analyze pending/draft content
|
|
132
184
|
|
|
133
185
|
**Examples:**
|
|
134
186
|
```bash
|
|
187
|
+
# Using local files + content key
|
|
135
188
|
npx contentstorage stats
|
|
136
|
-
npx contentstorage stats --content-key
|
|
137
|
-
|
|
189
|
+
npx contentstorage stats --content-key teamId/projectId
|
|
190
|
+
|
|
191
|
+
# Using API key (fetches directly from server)
|
|
192
|
+
npx contentstorage stats --api-key csk_xxx --project-id uuid
|
|
138
193
|
```
|
|
139
194
|
|
|
140
195
|
**What it shows:**
|
|
@@ -167,8 +222,14 @@ npx contentstorage stats --help
|
|
|
167
222
|
|
|
168
223
|
```javascript
|
|
169
224
|
export default {
|
|
170
|
-
//
|
|
171
|
-
|
|
225
|
+
// Authentication Option A: Content Key (read-only)
|
|
226
|
+
// Format: teamId/projectId
|
|
227
|
+
contentKey: 'your-team-id/your-project-id',
|
|
228
|
+
|
|
229
|
+
// Authentication Option B: API Key (read + write)
|
|
230
|
+
// Create in Project Settings → API Keys
|
|
231
|
+
// apiKey: 'csk_xxxxxxxx_xxxxxxxxxxxxxxxxxxxxxxxx',
|
|
232
|
+
// projectId: 'your-project-uuid',
|
|
172
233
|
|
|
173
234
|
// Required: Array of language codes
|
|
174
235
|
languageCodes: ['EN', 'FR', 'DE', 'ES'],
|
|
@@ -184,6 +245,17 @@ export default {
|
|
|
184
245
|
};
|
|
185
246
|
```
|
|
186
247
|
|
|
248
|
+
### Authentication Methods
|
|
249
|
+
|
|
250
|
+
| Method | Access Level | Use Case |
|
|
251
|
+
|--------|-------------|----------|
|
|
252
|
+
| `contentKey` | Read-only | Pull content, view stats |
|
|
253
|
+
| `apiKey` + `projectId` | Read + Write | Pull, push, CI/CD pipelines |
|
|
254
|
+
|
|
255
|
+
**Getting your credentials:**
|
|
256
|
+
- **Content Key**: Found in Project Settings → General (format: `teamId/projectId`)
|
|
257
|
+
- **API Key**: Create in Project Settings → API Keys
|
|
258
|
+
|
|
187
259
|
### Supported Languages
|
|
188
260
|
|
|
189
261
|
The CLI supports 40+ languages including:
|
package/dist/commands/cli.js
CHANGED
|
@@ -1,22 +1,38 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { pullContent } from './pull.js';
|
|
4
|
+
import { pushContent } from './push.js';
|
|
4
5
|
import { generateTypes } from './generate-types.js';
|
|
5
6
|
import { showStats } from './stats.js';
|
|
6
7
|
import { captureScreenshot } from './screenshot.js';
|
|
8
|
+
import { translateContent } from './translate.js';
|
|
7
9
|
const COMMANDS = {
|
|
8
10
|
pull: {
|
|
9
11
|
name: 'pull',
|
|
10
12
|
description: 'Pull content from Contentstorage CDN',
|
|
11
13
|
usage: 'contentstorage pull [options]',
|
|
12
14
|
options: [
|
|
13
|
-
' --content-key <key> Content key
|
|
15
|
+
' --content-key <key> Content key (read-only access)',
|
|
16
|
+
' --api-key <key> API key (read + write access)',
|
|
17
|
+
' --project-id <id> Project ID (required with --api-key)',
|
|
14
18
|
' --content-dir <dir> Directory to save content files',
|
|
15
19
|
' --lang <code> Language code (e.g., EN, FR)',
|
|
16
20
|
' --pending-changes Fetch pending/draft content',
|
|
17
21
|
' --flatten Output flattened key-value pairs',
|
|
18
22
|
],
|
|
19
23
|
},
|
|
24
|
+
push: {
|
|
25
|
+
name: 'push',
|
|
26
|
+
description: 'Push local content changes to Contentstorage',
|
|
27
|
+
usage: 'contentstorage push [options]',
|
|
28
|
+
options: [
|
|
29
|
+
' --api-key <key> API key for authentication (required)',
|
|
30
|
+
' --project-id <id> Project ID (required)',
|
|
31
|
+
' --content-dir <dir> Directory with content files',
|
|
32
|
+
' --lang <code> Language code to push (e.g., EN)',
|
|
33
|
+
' --dry-run Preview changes without applying',
|
|
34
|
+
],
|
|
35
|
+
},
|
|
20
36
|
'generate-types': {
|
|
21
37
|
name: 'generate-types',
|
|
22
38
|
description: 'Generate TypeScript type definitions from content',
|
|
@@ -33,7 +49,9 @@ const COMMANDS = {
|
|
|
33
49
|
description: 'Show translation completeness statistics',
|
|
34
50
|
usage: 'contentstorage stats [options]',
|
|
35
51
|
options: [
|
|
36
|
-
' --content-key <key> Content key
|
|
52
|
+
' --content-key <key> Content key (read-only access)',
|
|
53
|
+
' --api-key <key> API key (read + write access)',
|
|
54
|
+
' --project-id <id> Project ID (required with --api-key)',
|
|
37
55
|
' --content-dir <dir> Directory with content files',
|
|
38
56
|
' --pending-changes Analyze pending/draft content',
|
|
39
57
|
],
|
|
@@ -47,6 +65,19 @@ const COMMANDS = {
|
|
|
47
65
|
' --content-key <key> Content key for your project',
|
|
48
66
|
],
|
|
49
67
|
},
|
|
68
|
+
translate: {
|
|
69
|
+
name: 'translate',
|
|
70
|
+
description: 'Push new/changed keys and create a translate session',
|
|
71
|
+
usage: 'contentstorage translate [options]',
|
|
72
|
+
options: [
|
|
73
|
+
' --api-key <key> API key for authentication',
|
|
74
|
+
' --project-id <id> Project ID',
|
|
75
|
+
' --content-dir <dir> Directory with content files',
|
|
76
|
+
' --lang <code> Source language code (e.g., EN)',
|
|
77
|
+
' --dry-run Preview changes without pushing',
|
|
78
|
+
' -y Skip confirmation prompt',
|
|
79
|
+
],
|
|
80
|
+
},
|
|
50
81
|
};
|
|
51
82
|
function showHelp() {
|
|
52
83
|
console.log(chalk.bold('\nContentstorage CLI'));
|
|
@@ -103,6 +134,9 @@ async function main() {
|
|
|
103
134
|
case 'pull':
|
|
104
135
|
await pullContent();
|
|
105
136
|
break;
|
|
137
|
+
case 'push':
|
|
138
|
+
await pushContent();
|
|
139
|
+
break;
|
|
106
140
|
case 'generate-types':
|
|
107
141
|
await generateTypes();
|
|
108
142
|
break;
|
|
@@ -112,6 +146,9 @@ async function main() {
|
|
|
112
146
|
case 'screenshot':
|
|
113
147
|
await captureScreenshot();
|
|
114
148
|
break;
|
|
149
|
+
case 'translate':
|
|
150
|
+
await translateContent();
|
|
151
|
+
break;
|
|
115
152
|
default:
|
|
116
153
|
console.error(chalk.red(`Unknown command: ${command}\n`));
|
|
117
154
|
console.log(chalk.dim('Run "contentstorage --help" for usage'));
|
package/dist/commands/pull.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'fs/promises';
|
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import chalk from 'chalk';
|
|
6
6
|
import { loadConfig } from '../core/config-loader.js';
|
|
7
|
+
import { createApiClient } from '../core/api-client.js';
|
|
7
8
|
import { CONTENTSTORAGE_CONFIG } from '../utils/constants.js';
|
|
8
9
|
import { flattenJson } from '../utils/flatten-json.js';
|
|
9
10
|
export async function pullContent() {
|
|
@@ -31,6 +32,12 @@ export async function pullContent() {
|
|
|
31
32
|
else if (key === 'content-dir') {
|
|
32
33
|
cliConfig.contentDir = value;
|
|
33
34
|
}
|
|
35
|
+
else if (key === 'api-key') {
|
|
36
|
+
cliConfig.apiKey = value;
|
|
37
|
+
}
|
|
38
|
+
else if (key === 'project-id') {
|
|
39
|
+
cliConfig.projectId = value;
|
|
40
|
+
}
|
|
34
41
|
// Skip the value in the next iteration
|
|
35
42
|
i++;
|
|
36
43
|
}
|
|
@@ -44,18 +51,42 @@ export async function pullContent() {
|
|
|
44
51
|
console.log(chalk.yellow('Could not load a configuration file. Proceeding with CLI arguments.'));
|
|
45
52
|
}
|
|
46
53
|
const config = { ...fileConfig, ...cliConfig };
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
|
|
54
|
+
// Check if using API key authentication or contentKey (read-only)
|
|
55
|
+
const useApiClient = !!(config.apiKey && config.projectId);
|
|
56
|
+
// Validate required fields based on auth method
|
|
57
|
+
if (!useApiClient && !config.contentKey) {
|
|
58
|
+
console.error(chalk.red('Error: Either "contentKey" or "apiKey" + "projectId" is required.\n' +
|
|
59
|
+
' Use --content-key for read-only access, or\n' +
|
|
60
|
+
' Use --api-key and --project-id for full API access (read + write).'));
|
|
50
61
|
process.exit(1);
|
|
51
62
|
}
|
|
52
63
|
if (!config.contentDir) {
|
|
53
64
|
console.error(chalk.red('Error: Configuration is missing the required "contentDir" property.'));
|
|
54
65
|
process.exit(1);
|
|
55
66
|
}
|
|
56
|
-
|
|
67
|
+
if (useApiClient) {
|
|
68
|
+
console.log(chalk.blue(`Using API key authentication`));
|
|
69
|
+
console.log(chalk.blue(`Project ID: ${config.projectId}`));
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
console.log(chalk.blue(`Content key: ${config.contentKey}`));
|
|
73
|
+
}
|
|
57
74
|
console.log(chalk.blue(`Saving content to: ${config.contentDir}`));
|
|
58
75
|
try {
|
|
76
|
+
// Create API client if using new authentication
|
|
77
|
+
const apiClient = useApiClient
|
|
78
|
+
? createApiClient({
|
|
79
|
+
apiKey: config.apiKey,
|
|
80
|
+
projectId: config.projectId,
|
|
81
|
+
})
|
|
82
|
+
: null;
|
|
83
|
+
// If using API client and no languages specified, fetch from project info
|
|
84
|
+
if (apiClient && (!config.languageCodes || config.languageCodes.length === 0)) {
|
|
85
|
+
console.log(chalk.dim('Fetching available languages from project...'));
|
|
86
|
+
const projectInfo = await apiClient.getProject();
|
|
87
|
+
config.languageCodes = projectInfo.project.languages;
|
|
88
|
+
console.log(chalk.blue(`Found languages: ${config.languageCodes.join(', ')}`));
|
|
89
|
+
}
|
|
59
90
|
// Validate languageCodes array
|
|
60
91
|
if (!Array.isArray(config.languageCodes)) {
|
|
61
92
|
console.log(chalk.red(`Expected array from config.languageCodes, but received type ${typeof config.languageCodes}. Cannot pull files.`));
|
|
@@ -67,6 +98,37 @@ export async function pullContent() {
|
|
|
67
98
|
}
|
|
68
99
|
// Ensure the output directory exists (create it once before the loop)
|
|
69
100
|
await fs.mkdir(config.contentDir, { recursive: true });
|
|
101
|
+
// Use API client if available
|
|
102
|
+
if (apiClient) {
|
|
103
|
+
// Fetch all content at once using new API
|
|
104
|
+
const format = config.flatten ? 'flat' : 'nested';
|
|
105
|
+
const draft = config.pendingChanges !== false; // Default to draft
|
|
106
|
+
console.log(chalk.dim(`\nFetching ${draft ? 'draft' : 'published'} content...`));
|
|
107
|
+
const response = await apiClient.getContent({
|
|
108
|
+
format,
|
|
109
|
+
draft,
|
|
110
|
+
});
|
|
111
|
+
// Save each language to a file
|
|
112
|
+
for (const languageCode of config.languageCodes) {
|
|
113
|
+
const upperLang = languageCode.toUpperCase();
|
|
114
|
+
const jsonData = response.data[upperLang];
|
|
115
|
+
if (!jsonData) {
|
|
116
|
+
console.log(chalk.yellow(`\nNo content found for language: ${upperLang}`));
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
const filename = `${upperLang}.json`;
|
|
120
|
+
const outputPath = path.join(config.contentDir, filename);
|
|
121
|
+
console.log(chalk.blue(`\nProcessing language: ${upperLang}`));
|
|
122
|
+
await fs.writeFile(outputPath, JSON.stringify(jsonData, null, 2));
|
|
123
|
+
console.log(chalk.green(`Successfully saved ${outputPath}`));
|
|
124
|
+
}
|
|
125
|
+
if (response.metadata.hasPendingChanges) {
|
|
126
|
+
console.log(chalk.cyan('\n📝 Note: This project has pending changes awaiting publish.'));
|
|
127
|
+
}
|
|
128
|
+
console.log(chalk.green('\nAll content successfully pulled and saved.'));
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
// contentKey path: read-only access via CDN/API
|
|
70
132
|
// Process each language code
|
|
71
133
|
for (const languageCode of config.languageCodes) {
|
|
72
134
|
let fileUrl;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { loadConfig } from '../core/config-loader.js';
|
|
6
|
+
import { createApiClient } from '../core/api-client.js';
|
|
7
|
+
export async function pushContent() {
|
|
8
|
+
console.log(chalk.blue('Starting content push...'));
|
|
9
|
+
// Parse CLI arguments
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const cliConfig = {};
|
|
12
|
+
for (let i = 0; i < args.length; i++) {
|
|
13
|
+
const arg = args[i];
|
|
14
|
+
if (arg.startsWith('--')) {
|
|
15
|
+
const key = arg.substring(2);
|
|
16
|
+
const value = args[i + 1];
|
|
17
|
+
if (key === 'dry-run') {
|
|
18
|
+
cliConfig.dryRun = true;
|
|
19
|
+
}
|
|
20
|
+
else if (value && !value.startsWith('--')) {
|
|
21
|
+
if (key === 'lang') {
|
|
22
|
+
cliConfig.languageCodes = [value.toUpperCase()];
|
|
23
|
+
}
|
|
24
|
+
else if (key === 'api-key') {
|
|
25
|
+
cliConfig.apiKey = value;
|
|
26
|
+
}
|
|
27
|
+
else if (key === 'project-id') {
|
|
28
|
+
cliConfig.projectId = value;
|
|
29
|
+
}
|
|
30
|
+
else if (key === 'content-dir') {
|
|
31
|
+
cliConfig.contentDir = value;
|
|
32
|
+
}
|
|
33
|
+
i++; // Skip the value in next iteration
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
// Load config from file
|
|
38
|
+
let fileConfig = {};
|
|
39
|
+
try {
|
|
40
|
+
fileConfig = await loadConfig();
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
console.log(chalk.yellow('Could not load a configuration file. Proceeding with CLI arguments.'));
|
|
44
|
+
}
|
|
45
|
+
const config = { ...fileConfig, ...cliConfig };
|
|
46
|
+
// Validate required fields
|
|
47
|
+
if (!config.apiKey) {
|
|
48
|
+
console.error(chalk.red('\n❌ Error: API key is required for push.\n' +
|
|
49
|
+
' Provide it via config file (apiKey) or --api-key argument.\n' +
|
|
50
|
+
' You can create an API key in Project Settings → API Keys.'));
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
if (!config.projectId) {
|
|
54
|
+
console.error(chalk.red('\n❌ Error: Project ID is required for push.\n' +
|
|
55
|
+
' Provide it via config file (projectId) or --project-id argument.'));
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (!config.contentDir) {
|
|
59
|
+
console.error(chalk.red('\n❌ Error: Content directory is required.\n' +
|
|
60
|
+
' Provide it via config file (contentDir) or --content-dir argument.'));
|
|
61
|
+
process.exit(1);
|
|
62
|
+
}
|
|
63
|
+
if (!config.languageCodes || config.languageCodes.length === 0) {
|
|
64
|
+
console.error(chalk.red('\n❌ Error: At least one language code is required.\n' +
|
|
65
|
+
' Provide it via config file (languageCodes) or --lang argument.'));
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
// Create API client
|
|
69
|
+
const apiClient = createApiClient({
|
|
70
|
+
apiKey: config.apiKey,
|
|
71
|
+
projectId: config.projectId,
|
|
72
|
+
});
|
|
73
|
+
if (!apiClient) {
|
|
74
|
+
console.error(chalk.red('Failed to create API client'));
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
console.log(chalk.blue(`Project ID: ${config.projectId}`));
|
|
78
|
+
console.log(chalk.blue(`Content directory: ${config.contentDir}`));
|
|
79
|
+
console.log(chalk.blue(`Languages to push: ${config.languageCodes.join(', ')}`));
|
|
80
|
+
if (config.dryRun) {
|
|
81
|
+
console.log(chalk.yellow('\n🔍 Dry run mode - no changes will be made\n'));
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
let totalAdded = 0;
|
|
85
|
+
let totalUpdated = 0;
|
|
86
|
+
let totalSkipped = 0;
|
|
87
|
+
for (const languageCode of config.languageCodes) {
|
|
88
|
+
const filePath = path.join(config.contentDir, `${languageCode}.json`);
|
|
89
|
+
console.log(chalk.blue(`\nProcessing ${languageCode}...`));
|
|
90
|
+
console.log(chalk.dim(` Reading from: ${filePath}`));
|
|
91
|
+
// Read the local content file
|
|
92
|
+
let content;
|
|
93
|
+
try {
|
|
94
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
95
|
+
content = JSON.parse(fileContent);
|
|
96
|
+
}
|
|
97
|
+
catch (error) {
|
|
98
|
+
if (error.code === 'ENOENT') {
|
|
99
|
+
console.error(chalk.red(` ❌ File not found: ${filePath}`));
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
console.error(chalk.red(` ❌ Error reading file: ${error.message}`));
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
// Push to API
|
|
106
|
+
try {
|
|
107
|
+
const response = await apiClient.pushContent(languageCode, content, {
|
|
108
|
+
dryRun: config.dryRun,
|
|
109
|
+
});
|
|
110
|
+
const { result } = response;
|
|
111
|
+
if (config.dryRun) {
|
|
112
|
+
console.log(chalk.cyan(` 📊 Dry run results for ${languageCode}:`));
|
|
113
|
+
console.log(chalk.green(` Keys to add: ${result.keysAdded}`));
|
|
114
|
+
console.log(chalk.yellow(` Keys to update: ${result.keysUpdated}`));
|
|
115
|
+
console.log(chalk.dim(` Keys unchanged: ${result.keysSkipped}`));
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
console.log(chalk.green(` ✅ Successfully pushed ${languageCode}`));
|
|
119
|
+
console.log(chalk.green(` Keys added: ${result.keysAdded}`));
|
|
120
|
+
console.log(chalk.yellow(` Keys updated: ${result.keysUpdated}`));
|
|
121
|
+
console.log(chalk.dim(` Keys skipped: ${result.keysSkipped}`));
|
|
122
|
+
if (result.pendingChangesCreated) {
|
|
123
|
+
console.log(chalk.cyan(' 📝 Changes added to pending (publish required)'));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
totalAdded += result.keysAdded;
|
|
127
|
+
totalUpdated += result.keysUpdated;
|
|
128
|
+
totalSkipped += result.keysSkipped;
|
|
129
|
+
}
|
|
130
|
+
catch (error) {
|
|
131
|
+
console.error(chalk.red(` ❌ Failed to push ${languageCode}: ${error.message}`));
|
|
132
|
+
throw error;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
// Summary
|
|
136
|
+
console.log(chalk.bold('\n📊 Summary:'));
|
|
137
|
+
console.log(chalk.dim('─'.repeat(40)));
|
|
138
|
+
if (config.dryRun) {
|
|
139
|
+
console.log(chalk.cyan(' Dry run completed'));
|
|
140
|
+
console.log(chalk.green(` Total keys to add: ${totalAdded}`));
|
|
141
|
+
console.log(chalk.yellow(` Total keys to update: ${totalUpdated}`));
|
|
142
|
+
console.log(chalk.dim(` Total keys unchanged: ${totalSkipped}`));
|
|
143
|
+
console.log(chalk.dim('\n Run without --dry-run to apply changes.'));
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
console.log(chalk.green(` ✅ Push completed successfully`));
|
|
147
|
+
console.log(chalk.green(` Total keys added: ${totalAdded}`));
|
|
148
|
+
console.log(chalk.yellow(` Total keys updated: ${totalUpdated}`));
|
|
149
|
+
console.log(chalk.dim(` Total keys skipped: ${totalSkipped}`));
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
console.error(chalk.red('\n❌ Push failed. See errors above.'));
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
}
|
package/dist/commands/stats.js
CHANGED
|
@@ -3,6 +3,7 @@ import chalk from 'chalk';
|
|
|
3
3
|
import { promises as fs } from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
import { loadConfig } from '../core/config-loader.js';
|
|
6
|
+
import { createApiClient } from '../core/api-client.js';
|
|
6
7
|
import { flattenJson } from '../utils/flatten-json.js';
|
|
7
8
|
import { CONTENTSTORAGE_CONFIG } from '../utils/constants.js';
|
|
8
9
|
/**
|
|
@@ -171,6 +172,70 @@ function displayStats(result) {
|
|
|
171
172
|
console.log(overallColor(`Overall Completion: ${chalk.bold(result.overallCompletion.toFixed(1) + '%')}`));
|
|
172
173
|
console.log('');
|
|
173
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Display statistics from API response
|
|
177
|
+
*/
|
|
178
|
+
function displayApiStats(response) {
|
|
179
|
+
const { stats } = response;
|
|
180
|
+
console.log(chalk.bold('\n📊 Translation Statistics (from API)'));
|
|
181
|
+
console.log(chalk.dim('═'.repeat(70)));
|
|
182
|
+
console.log(chalk.cyan(`\nTotal unique content items: ${chalk.bold(stats.totalKeys)}`));
|
|
183
|
+
// Language statistics table header
|
|
184
|
+
console.log(chalk.bold('\n📋 Language Statistics:'));
|
|
185
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
186
|
+
// Table header
|
|
187
|
+
const headerFormat = (str, width) => str.padEnd(width, ' ');
|
|
188
|
+
console.log(chalk.bold(headerFormat('Language', 12) +
|
|
189
|
+
headerFormat('Completed', 12) +
|
|
190
|
+
headerFormat('Total', 10) +
|
|
191
|
+
'Complete'));
|
|
192
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
193
|
+
// Calculate overall completion
|
|
194
|
+
let totalCompleted = 0;
|
|
195
|
+
let totalPossible = 0;
|
|
196
|
+
// Language rows
|
|
197
|
+
for (const lang of stats.languages) {
|
|
198
|
+
const percentageColor = lang.percentage === 100
|
|
199
|
+
? chalk.green
|
|
200
|
+
: lang.percentage >= 80
|
|
201
|
+
? chalk.yellow
|
|
202
|
+
: chalk.red;
|
|
203
|
+
const percentage = percentageColor(lang.percentage + '%');
|
|
204
|
+
console.log(chalk.white(headerFormat(lang.code, 12)) +
|
|
205
|
+
chalk.white(headerFormat(lang.completed.toString(), 12)) +
|
|
206
|
+
chalk.white(headerFormat(stats.totalKeys.toString(), 10)) +
|
|
207
|
+
percentage);
|
|
208
|
+
totalCompleted += lang.completed;
|
|
209
|
+
totalPossible += stats.totalKeys;
|
|
210
|
+
}
|
|
211
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
212
|
+
// Pending changes info
|
|
213
|
+
if (stats.pendingChanges.total > 0) {
|
|
214
|
+
console.log(chalk.bold('\n📝 Pending Changes:'));
|
|
215
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
216
|
+
console.log(chalk.cyan(` Total: ${stats.pendingChanges.total}`));
|
|
217
|
+
if (stats.pendingChanges.added > 0) {
|
|
218
|
+
console.log(chalk.green(` Added: ${stats.pendingChanges.added}`));
|
|
219
|
+
}
|
|
220
|
+
if (stats.pendingChanges.edited > 0) {
|
|
221
|
+
console.log(chalk.yellow(` Edited: ${stats.pendingChanges.edited}`));
|
|
222
|
+
}
|
|
223
|
+
if (stats.pendingChanges.removed > 0) {
|
|
224
|
+
console.log(chalk.red(` Removed: ${stats.pendingChanges.removed}`));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
// Overall completion
|
|
228
|
+
console.log(chalk.bold('\n📈 Overall Summary:'));
|
|
229
|
+
console.log(chalk.dim('─'.repeat(70)));
|
|
230
|
+
const overallCompletion = totalPossible > 0 ? (totalCompleted / totalPossible) * 100 : 100;
|
|
231
|
+
const overallColor = overallCompletion === 100
|
|
232
|
+
? chalk.green
|
|
233
|
+
: overallCompletion >= 80
|
|
234
|
+
? chalk.yellow
|
|
235
|
+
: chalk.red;
|
|
236
|
+
console.log(overallColor(`Overall Completion: ${chalk.bold(overallCompletion.toFixed(1) + '%')}`));
|
|
237
|
+
console.log('');
|
|
238
|
+
}
|
|
174
239
|
/**
|
|
175
240
|
* Main stats command function
|
|
176
241
|
*/
|
|
@@ -196,6 +261,12 @@ export async function showStats() {
|
|
|
196
261
|
else if (key === 'content-dir') {
|
|
197
262
|
cliConfig.contentDir = value;
|
|
198
263
|
}
|
|
264
|
+
else if (key === 'api-key') {
|
|
265
|
+
cliConfig.apiKey = value;
|
|
266
|
+
}
|
|
267
|
+
else if (key === 'project-id') {
|
|
268
|
+
cliConfig.projectId = value;
|
|
269
|
+
}
|
|
199
270
|
i++; // Skip the value in next iteration
|
|
200
271
|
}
|
|
201
272
|
}
|
|
@@ -211,11 +282,31 @@ export async function showStats() {
|
|
|
211
282
|
}
|
|
212
283
|
// Merge configurations (CLI args override file config)
|
|
213
284
|
const config = { ...fileConfig, ...cliConfig };
|
|
214
|
-
//
|
|
215
|
-
|
|
216
|
-
|
|
285
|
+
// Check if using new API key authentication
|
|
286
|
+
const useApiClient = !!(config.apiKey && config.projectId);
|
|
287
|
+
// Validate required fields based on auth method
|
|
288
|
+
if (!useApiClient && !config.contentKey) {
|
|
289
|
+
console.error(chalk.red('\n❌ Error: Either "contentKey" or "apiKey" + "projectId" is required.\n' +
|
|
290
|
+
' Use --content-key for read-only access, or\n' +
|
|
291
|
+
' Use --api-key and --project-id for full API access.'));
|
|
217
292
|
process.exit(1);
|
|
218
293
|
}
|
|
294
|
+
// If using API client, fetch stats directly from API
|
|
295
|
+
if (useApiClient) {
|
|
296
|
+
const apiClient = createApiClient({
|
|
297
|
+
apiKey: config.apiKey,
|
|
298
|
+
projectId: config.projectId,
|
|
299
|
+
});
|
|
300
|
+
if (!apiClient) {
|
|
301
|
+
console.error(chalk.red('Failed to create API client'));
|
|
302
|
+
process.exit(1);
|
|
303
|
+
}
|
|
304
|
+
console.log(chalk.blue('Fetching statistics from API...\n'));
|
|
305
|
+
const apiStats = await apiClient.getStats();
|
|
306
|
+
displayApiStats(apiStats);
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
// contentKey path: local file analysis or fetch from CDN/API
|
|
219
310
|
if (!config.languageCodes || config.languageCodes.length === 0) {
|
|
220
311
|
console.error(chalk.red('\n❌ Error: At least one language code is required in configuration.'));
|
|
221
312
|
process.exit(1);
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs/promises';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import readline from 'readline';
|
|
5
|
+
import { exec, execSync } from 'child_process';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { loadConfig } from '../core/config-loader.js';
|
|
8
|
+
import { createApiClient } from '../core/api-client.js';
|
|
9
|
+
import { flattenJson } from '../utils/flatten-json.js';
|
|
10
|
+
function prompt(question) {
|
|
11
|
+
const rl = readline.createInterface({
|
|
12
|
+
input: process.stdin,
|
|
13
|
+
output: process.stdout,
|
|
14
|
+
});
|
|
15
|
+
return new Promise((resolve) => {
|
|
16
|
+
rl.question(question, (answer) => {
|
|
17
|
+
rl.close();
|
|
18
|
+
resolve(answer.trim());
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function getGitBranch() {
|
|
23
|
+
try {
|
|
24
|
+
return execSync('git rev-parse --abbrev-ref HEAD', {
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
27
|
+
}).trim();
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function openInBrowser(url) {
|
|
34
|
+
const platform = process.platform;
|
|
35
|
+
let command;
|
|
36
|
+
if (platform === 'darwin') {
|
|
37
|
+
command = `open "${url}"`;
|
|
38
|
+
}
|
|
39
|
+
else if (platform === 'win32') {
|
|
40
|
+
command = `start "" "${url}"`;
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
command = `xdg-open "${url}"`;
|
|
44
|
+
}
|
|
45
|
+
exec(command, (error) => {
|
|
46
|
+
if (error) {
|
|
47
|
+
console.error(chalk.red(`Failed to open browser: ${error.message}`));
|
|
48
|
+
console.log(chalk.yellow(`Please open this URL manually:\n ${url}`));
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export async function translateContent() {
|
|
53
|
+
// Parse CLI arguments
|
|
54
|
+
const args = process.argv.slice(2);
|
|
55
|
+
const cliConfig = {};
|
|
56
|
+
for (let i = 0; i < args.length; i++) {
|
|
57
|
+
const arg = args[i];
|
|
58
|
+
if (arg === '-y') {
|
|
59
|
+
cliConfig.skipConfirm = true;
|
|
60
|
+
}
|
|
61
|
+
else if (arg.startsWith('--')) {
|
|
62
|
+
const key = arg.substring(2);
|
|
63
|
+
const value = args[i + 1];
|
|
64
|
+
if (key === 'dry-run') {
|
|
65
|
+
cliConfig.dryRun = true;
|
|
66
|
+
}
|
|
67
|
+
else if (value && !value.startsWith('--') && value !== '-y') {
|
|
68
|
+
if (key === 'lang') {
|
|
69
|
+
cliConfig.languageCodes = [value.toUpperCase()];
|
|
70
|
+
}
|
|
71
|
+
else if (key === 'api-key') {
|
|
72
|
+
cliConfig.apiKey = value;
|
|
73
|
+
}
|
|
74
|
+
else if (key === 'project-id') {
|
|
75
|
+
cliConfig.projectId = value;
|
|
76
|
+
}
|
|
77
|
+
else if (key === 'content-dir') {
|
|
78
|
+
cliConfig.contentDir = value;
|
|
79
|
+
}
|
|
80
|
+
i++;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Load config
|
|
85
|
+
let fileConfig = {};
|
|
86
|
+
try {
|
|
87
|
+
fileConfig = await loadConfig();
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
console.log(chalk.yellow('Could not load configuration file. Using CLI arguments only.'));
|
|
91
|
+
}
|
|
92
|
+
const config = { ...fileConfig, ...cliConfig };
|
|
93
|
+
// Validate required fields
|
|
94
|
+
if (!config.apiKey) {
|
|
95
|
+
console.error(chalk.red('\n❌ Error: API key is required.\n' +
|
|
96
|
+
' Provide it via config file (apiKey) or --api-key argument.\n' +
|
|
97
|
+
' You can create an API key in Project Settings → API Keys.'));
|
|
98
|
+
process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
if (!config.projectId) {
|
|
101
|
+
console.error(chalk.red('\n❌ Error: Project ID is required.\n' +
|
|
102
|
+
' Provide it via config file (projectId) or --project-id argument.'));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
if (!config.contentDir) {
|
|
106
|
+
console.error(chalk.red('\n❌ Error: Content directory is required.\n' +
|
|
107
|
+
' Provide it via config file (contentDir) or --content-dir argument.'));
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
if (!config.languageCodes || config.languageCodes.length === 0) {
|
|
111
|
+
console.error(chalk.red('\n❌ Error: At least one language code is required.\n' +
|
|
112
|
+
' Provide it via config file (languageCodes) or --lang argument.'));
|
|
113
|
+
process.exit(1);
|
|
114
|
+
}
|
|
115
|
+
const sourceLanguage = config.languageCodes[0];
|
|
116
|
+
// Create API client
|
|
117
|
+
const apiClient = createApiClient({
|
|
118
|
+
apiKey: config.apiKey,
|
|
119
|
+
projectId: config.projectId,
|
|
120
|
+
});
|
|
121
|
+
if (!apiClient) {
|
|
122
|
+
console.error(chalk.red('Failed to create API client'));
|
|
123
|
+
process.exit(1);
|
|
124
|
+
}
|
|
125
|
+
// Step 1: Read local source file
|
|
126
|
+
const filePath = path.join(config.contentDir, `${sourceLanguage}.json`);
|
|
127
|
+
let localContent;
|
|
128
|
+
try {
|
|
129
|
+
const fileContent = await fs.readFile(filePath, 'utf-8');
|
|
130
|
+
localContent = JSON.parse(fileContent);
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
if (error.code === 'ENOENT') {
|
|
134
|
+
console.error(chalk.red(`\n❌ Source file not found: ${filePath}\n` +
|
|
135
|
+
` Check your contentDir setting and ensure the file exists.`));
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
console.error(chalk.red(`\n❌ Error reading source file: ${error.message}`));
|
|
139
|
+
}
|
|
140
|
+
process.exit(1);
|
|
141
|
+
}
|
|
142
|
+
const localFlat = flattenJson(localContent);
|
|
143
|
+
// Step 2: Fetch server content
|
|
144
|
+
console.log(chalk.blue('Comparing with server...'));
|
|
145
|
+
let serverFlat;
|
|
146
|
+
try {
|
|
147
|
+
const serverResponse = await apiClient.getContent({
|
|
148
|
+
languageCode: sourceLanguage,
|
|
149
|
+
format: 'flat',
|
|
150
|
+
draft: true,
|
|
151
|
+
});
|
|
152
|
+
serverFlat = (serverResponse.data[sourceLanguage] || {});
|
|
153
|
+
}
|
|
154
|
+
catch (error) {
|
|
155
|
+
console.error(chalk.red(`\n❌ Could not fetch server content: ${error.message}`));
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
// Step 3: Compute diff
|
|
159
|
+
const diff = { newKeys: [], modifiedKeys: [] };
|
|
160
|
+
for (const [key, value] of Object.entries(localFlat)) {
|
|
161
|
+
const serverValue = serverFlat[key];
|
|
162
|
+
if (serverValue === undefined) {
|
|
163
|
+
diff.newKeys.push({ key, value });
|
|
164
|
+
}
|
|
165
|
+
else if (serverValue !== value) {
|
|
166
|
+
diff.modifiedKeys.push({ key, value, oldValue: serverValue });
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
const totalChanges = diff.newKeys.length + diff.modifiedKeys.length;
|
|
170
|
+
if (totalChanges === 0) {
|
|
171
|
+
console.log(chalk.green('\n✓ All keys are up to date. Nothing to translate.'));
|
|
172
|
+
process.exit(0);
|
|
173
|
+
}
|
|
174
|
+
// Step 4: Display diff
|
|
175
|
+
const parts = [];
|
|
176
|
+
if (diff.newKeys.length > 0)
|
|
177
|
+
parts.push(`${diff.newKeys.length} new`);
|
|
178
|
+
if (diff.modifiedKeys.length > 0)
|
|
179
|
+
parts.push(`${diff.modifiedKeys.length} modified`);
|
|
180
|
+
console.log(chalk.bold(`\nFound ${parts.join(', ')}:\n`));
|
|
181
|
+
for (const { key, value } of diff.newKeys) {
|
|
182
|
+
const displayValue = value.length > 50 ? value.substring(0, 47) + '...' : value;
|
|
183
|
+
console.log(chalk.green(` + ${key.padEnd(35)} ${chalk.dim(`"${displayValue}"`)}`));
|
|
184
|
+
}
|
|
185
|
+
for (const { key, value, oldValue } of diff.modifiedKeys) {
|
|
186
|
+
const displayOld = oldValue.length > 25 ? oldValue.substring(0, 22) + '...' : oldValue;
|
|
187
|
+
const displayNew = value.length > 25 ? value.substring(0, 22) + '...' : value;
|
|
188
|
+
console.log(chalk.yellow(` ~ ${key.padEnd(35)} ${chalk.dim(`"${displayOld}" → "${displayNew}"`)}`));
|
|
189
|
+
}
|
|
190
|
+
// Dry run stops here
|
|
191
|
+
if (config.dryRun) {
|
|
192
|
+
console.log(chalk.dim('\n(dry run — nothing was pushed)'));
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
// Step 5: Confirm
|
|
196
|
+
if (!config.skipConfirm) {
|
|
197
|
+
// Get project name for display
|
|
198
|
+
let projectName = config.projectId;
|
|
199
|
+
try {
|
|
200
|
+
const projectInfo = await apiClient.getProject();
|
|
201
|
+
projectName = projectInfo.project.name;
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
// Use projectId as fallback
|
|
205
|
+
}
|
|
206
|
+
console.log(chalk.dim(`\nThese will be pushed to Contentstorage (project: ${projectName}).`));
|
|
207
|
+
const answer = await prompt('Proceed? (Y/n) ');
|
|
208
|
+
if (answer.toLowerCase() === 'n') {
|
|
209
|
+
console.log(chalk.dim('Cancelled.'));
|
|
210
|
+
process.exit(0);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Step 6: Push with session
|
|
214
|
+
const sessionKeys = [
|
|
215
|
+
...diff.newKeys.map(({ key, value }) => ({ key, value, status: 'new' })),
|
|
216
|
+
...diff.modifiedKeys.map(({ key, value }) => ({ key, value, status: 'modified' })),
|
|
217
|
+
];
|
|
218
|
+
const gitBranch = getGitBranch();
|
|
219
|
+
try {
|
|
220
|
+
const response = await apiClient.pushContent(sourceLanguage, localContent, {
|
|
221
|
+
session: {
|
|
222
|
+
keys: sessionKeys,
|
|
223
|
+
metadata: {
|
|
224
|
+
...(gitBranch ? { gitBranch } : {}),
|
|
225
|
+
pushedAt: new Date().toISOString(),
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
const { result } = response;
|
|
230
|
+
const pushed = result.keysAdded + result.keysUpdated;
|
|
231
|
+
console.log(chalk.green(`\n✓ ${pushed} keys pushed (${result.keysAdded} new, ${result.keysUpdated} modified)`) +
|
|
232
|
+
(response.session ? chalk.dim(` [session: ${response.session.id}]`) : ''));
|
|
233
|
+
// Step 7: Open browser
|
|
234
|
+
if (response.session) {
|
|
235
|
+
console.log(chalk.bold('\n→ Create task and add visual context:'));
|
|
236
|
+
console.log(chalk.cyan(` ${response.session.url}`));
|
|
237
|
+
openInBrowser(response.session.url);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
console.error(chalk.red(`\n❌ Failed to push keys: ${error.message}`));
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client for Contentstorage CLI API
|
|
3
|
+
* Uses API key authentication for project-level access
|
|
4
|
+
*/
|
|
5
|
+
export interface ContentResponse {
|
|
6
|
+
success: boolean;
|
|
7
|
+
data: Record<string, Record<string, any>>;
|
|
8
|
+
metadata: {
|
|
9
|
+
projectId: string;
|
|
10
|
+
languages: string[];
|
|
11
|
+
hasPendingChanges: boolean;
|
|
12
|
+
format: string;
|
|
13
|
+
source: 'draft' | 'published';
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
export interface PushResult {
|
|
17
|
+
keysAdded: number;
|
|
18
|
+
keysUpdated: number;
|
|
19
|
+
keysSkipped: number;
|
|
20
|
+
pendingChangesCreated: boolean;
|
|
21
|
+
uploadId?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface TranslateSessionData {
|
|
24
|
+
keys: Array<{
|
|
25
|
+
key: string;
|
|
26
|
+
value: string;
|
|
27
|
+
status: 'new' | 'modified';
|
|
28
|
+
}>;
|
|
29
|
+
metadata?: {
|
|
30
|
+
gitBranch?: string;
|
|
31
|
+
pushedAt?: string;
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export interface TranslateSessionResult {
|
|
35
|
+
id: string;
|
|
36
|
+
url: string;
|
|
37
|
+
}
|
|
38
|
+
export interface PushResponse {
|
|
39
|
+
success: boolean;
|
|
40
|
+
result: PushResult;
|
|
41
|
+
dryRun?: boolean;
|
|
42
|
+
session?: TranslateSessionResult;
|
|
43
|
+
}
|
|
44
|
+
export interface ProjectResponse {
|
|
45
|
+
success: boolean;
|
|
46
|
+
project: {
|
|
47
|
+
id: string;
|
|
48
|
+
name: string;
|
|
49
|
+
languages: string[];
|
|
50
|
+
changeTrackingEnabled: boolean;
|
|
51
|
+
createdAt: string;
|
|
52
|
+
updatedAt: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export interface StatsResponse {
|
|
56
|
+
success: boolean;
|
|
57
|
+
stats: {
|
|
58
|
+
totalKeys: number;
|
|
59
|
+
languages: Array<{
|
|
60
|
+
code: string;
|
|
61
|
+
completed: number;
|
|
62
|
+
percentage: number;
|
|
63
|
+
}>;
|
|
64
|
+
pendingChanges: {
|
|
65
|
+
total: number;
|
|
66
|
+
added: number;
|
|
67
|
+
edited: number;
|
|
68
|
+
removed: number;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
export interface ApiError {
|
|
73
|
+
success: false;
|
|
74
|
+
error: string;
|
|
75
|
+
message: string;
|
|
76
|
+
}
|
|
77
|
+
export declare class ApiClient {
|
|
78
|
+
private client;
|
|
79
|
+
private projectId;
|
|
80
|
+
constructor(apiKey: string, projectId: string);
|
|
81
|
+
/**
|
|
82
|
+
* Handle API errors consistently
|
|
83
|
+
*/
|
|
84
|
+
private handleError;
|
|
85
|
+
/**
|
|
86
|
+
* Pull content from the API
|
|
87
|
+
* @param options.languageCode - Optional specific language (returns all if omitted)
|
|
88
|
+
* @param options.format - 'nested' or 'flat' (default: nested)
|
|
89
|
+
* @param options.draft - true for draft content, false for published (default: true)
|
|
90
|
+
*/
|
|
91
|
+
getContent(options?: {
|
|
92
|
+
languageCode?: string;
|
|
93
|
+
format?: 'nested' | 'flat';
|
|
94
|
+
draft?: boolean;
|
|
95
|
+
}): Promise<ContentResponse>;
|
|
96
|
+
/**
|
|
97
|
+
* Push content to the API
|
|
98
|
+
* @param languageCode - The language code to push
|
|
99
|
+
* @param content - The content object (nested or flat)
|
|
100
|
+
* @param options.dryRun - If true, validate only without saving
|
|
101
|
+
*/
|
|
102
|
+
pushContent(languageCode: string, content: Record<string, any>, options?: {
|
|
103
|
+
dryRun?: boolean;
|
|
104
|
+
session?: TranslateSessionData;
|
|
105
|
+
}): Promise<PushResponse>;
|
|
106
|
+
/**
|
|
107
|
+
* Get project information
|
|
108
|
+
*/
|
|
109
|
+
getProject(): Promise<ProjectResponse>;
|
|
110
|
+
/**
|
|
111
|
+
* Get translation statistics
|
|
112
|
+
*/
|
|
113
|
+
getStats(): Promise<StatsResponse>;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Create an API client from config
|
|
117
|
+
* Returns null if API key is not configured
|
|
118
|
+
*/
|
|
119
|
+
export declare function createApiClient(config: {
|
|
120
|
+
apiKey?: string;
|
|
121
|
+
projectId?: string;
|
|
122
|
+
}): ApiClient | null;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Client for Contentstorage CLI API
|
|
3
|
+
* Uses API key authentication for project-level access
|
|
4
|
+
*/
|
|
5
|
+
import axios from 'axios';
|
|
6
|
+
import { CONTENTSTORAGE_CONFIG } from '../utils/constants.js';
|
|
7
|
+
export class ApiClient {
|
|
8
|
+
constructor(apiKey, projectId) {
|
|
9
|
+
this.projectId = projectId;
|
|
10
|
+
this.client = axios.create({
|
|
11
|
+
baseURL: `${CONTENTSTORAGE_CONFIG.API_URL}/api/v1/cli`,
|
|
12
|
+
headers: {
|
|
13
|
+
Authorization: `Bearer ${apiKey}`,
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
},
|
|
16
|
+
timeout: 30000,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Handle API errors consistently
|
|
21
|
+
*/
|
|
22
|
+
handleError(error) {
|
|
23
|
+
if (error.response?.data) {
|
|
24
|
+
const { error: errorCode, message } = error.response.data;
|
|
25
|
+
throw new Error(`${errorCode}: ${message}`);
|
|
26
|
+
}
|
|
27
|
+
if (error.response) {
|
|
28
|
+
throw new Error(`API error: ${error.response.status} ${error.response.statusText}`);
|
|
29
|
+
}
|
|
30
|
+
throw new Error(`Network error: ${error.message}`);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Pull content from the API
|
|
34
|
+
* @param options.languageCode - Optional specific language (returns all if omitted)
|
|
35
|
+
* @param options.format - 'nested' or 'flat' (default: nested)
|
|
36
|
+
* @param options.draft - true for draft content, false for published (default: true)
|
|
37
|
+
*/
|
|
38
|
+
async getContent(options = {}) {
|
|
39
|
+
try {
|
|
40
|
+
const params = new URLSearchParams();
|
|
41
|
+
params.append('projectId', this.projectId);
|
|
42
|
+
if (options.languageCode) {
|
|
43
|
+
params.append('languageCode', options.languageCode);
|
|
44
|
+
}
|
|
45
|
+
if (options.format) {
|
|
46
|
+
params.append('format', options.format);
|
|
47
|
+
}
|
|
48
|
+
if (options.draft !== undefined) {
|
|
49
|
+
params.append('draft', String(options.draft));
|
|
50
|
+
}
|
|
51
|
+
const response = await this.client.get(`/content?${params}`);
|
|
52
|
+
return response.data;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
this.handleError(error);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Push content to the API
|
|
60
|
+
* @param languageCode - The language code to push
|
|
61
|
+
* @param content - The content object (nested or flat)
|
|
62
|
+
* @param options.dryRun - If true, validate only without saving
|
|
63
|
+
*/
|
|
64
|
+
async pushContent(languageCode, content, options = {}) {
|
|
65
|
+
try {
|
|
66
|
+
const body = {
|
|
67
|
+
projectId: this.projectId,
|
|
68
|
+
languageCode,
|
|
69
|
+
content,
|
|
70
|
+
options: {
|
|
71
|
+
dryRun: options.dryRun || false,
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
if (options.session) {
|
|
75
|
+
body.session = options.session;
|
|
76
|
+
}
|
|
77
|
+
const response = await this.client.post('/content/push', body);
|
|
78
|
+
return response.data;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
this.handleError(error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Get project information
|
|
86
|
+
*/
|
|
87
|
+
async getProject() {
|
|
88
|
+
try {
|
|
89
|
+
const response = await this.client.get(`/projects/${this.projectId}`);
|
|
90
|
+
return response.data;
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
this.handleError(error);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get translation statistics
|
|
98
|
+
*/
|
|
99
|
+
async getStats() {
|
|
100
|
+
try {
|
|
101
|
+
const response = await this.client.get(`/projects/${this.projectId}/stats`);
|
|
102
|
+
return response.data;
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
this.handleError(error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Create an API client from config
|
|
111
|
+
* Returns null if API key is not configured
|
|
112
|
+
*/
|
|
113
|
+
export function createApiClient(config) {
|
|
114
|
+
if (!config.apiKey || !config.projectId) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
return new ApiClient(config.apiKey, config.projectId);
|
|
118
|
+
}
|
|
@@ -37,6 +37,9 @@ export async function loadConfig() {
|
|
|
37
37
|
contentKey: mergedConfig.contentKey || '',
|
|
38
38
|
contentDir: path.resolve(process.cwd(), mergedConfig.contentDir),
|
|
39
39
|
typesOutputFile: path.resolve(process.cwd(), mergedConfig.typesOutputFile),
|
|
40
|
+
// New API key authentication fields
|
|
41
|
+
apiKey: mergedConfig.apiKey,
|
|
42
|
+
projectId: mergedConfig.projectId,
|
|
40
43
|
};
|
|
41
44
|
return finalConfig;
|
|
42
45
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ export type AppConfig = {
|
|
|
5
5
|
typesOutputFile: string;
|
|
6
6
|
pendingChanges?: boolean;
|
|
7
7
|
flatten?: boolean;
|
|
8
|
+
apiKey?: string;
|
|
9
|
+
projectId?: string;
|
|
8
10
|
};
|
|
9
11
|
export type LanguageCode = 'SQ' | 'BE' | 'BS' | 'BG' | 'HR' | 'CS' | 'DA' | 'NL' | 'EN' | 'ET' | 'FI' | 'FR' | 'DE' | 'EL' | 'HU' | 'GA' | 'IT' | 'LV' | 'LT' | 'MK' | 'NO' | 'PL' | 'PT' | 'RO' | 'RU' | 'SR' | 'SK' | 'SL' | 'ES' | 'SV' | 'TR' | 'UK';
|
|
10
12
|
/**
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@contentstorage/core",
|
|
3
3
|
"author": "Kaido Hussar <kaido@contentstorage.app>",
|
|
4
4
|
"homepage": "https://contentstorage.app",
|
|
5
|
-
"version": "3.0
|
|
5
|
+
"version": "3.2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"description": "Contentstorage CLI for managing translations and generating TypeScript types",
|
|
8
8
|
"license": "MIT",
|