@localheroai/cli 0.0.2 ā 0.0.3
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 +98 -0
- package/package.json +57 -57
- package/src/api/auth.js +20 -11
- package/src/api/client.js +70 -28
- package/src/api/imports.js +15 -13
- package/src/api/projects.js +17 -17
- package/src/api/translations.js +50 -29
- package/src/cli.js +49 -42
- package/src/commands/init.js +436 -236
- package/src/commands/login.js +59 -48
- package/src/commands/sync.js +28 -0
- package/src/commands/translate.js +227 -247
- package/src/utils/auth.js +15 -15
- package/src/utils/config.js +115 -86
- package/src/utils/files.js +338 -116
- package/src/utils/git.js +64 -8
- package/src/utils/github.js +75 -45
- package/src/utils/import-service.js +112 -129
- package/src/utils/prompt-service.js +66 -50
- package/src/utils/sync-service.js +147 -0
- package/src/utils/translation-updater/common.js +44 -0
- package/src/utils/translation-updater/index.js +36 -0
- package/src/utils/translation-updater/json-handler.js +112 -0
- package/src/utils/translation-updater/yaml-handler.js +181 -0
- package/src/utils/translation-utils.js +237 -0
- package/src/utils/defaults.js +0 -7
- package/src/utils/helpers.js +0 -3
- package/src/utils/project-service.js +0 -11
- package/src/utils/translation-updater.js +0 -154
package/README
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# LocalHero.ai CLI šāØ
|
|
2
|
+
|
|
3
|
+
> Automatic translations for teams that ship
|
|
4
|
+
|
|
5
|
+
LocalHero.ai is an AI-powered I18n translation service that seamlessly integrates with your development workflow. It automatically detects and translates I18n keys with missing translations, then saving any new translations directly to your repository. [Learn more at localhero.ai](https://localhero.ai/)
|
|
6
|
+
|
|
7
|
+
## Features š
|
|
8
|
+
|
|
9
|
+
- š¤ AI-powered translations that preserve your brand voice
|
|
10
|
+
- š Seamless integration with Rails, React and other frameworks coming soon
|
|
11
|
+
- š Automated workflow with GitHub Actions support
|
|
12
|
+
- š¦ Works with YAML and JSON translation files
|
|
13
|
+
|
|
14
|
+
## Getting Started š
|
|
15
|
+
|
|
16
|
+
1. Sign up for a free trial at [localhero.ai](https://localhero.ai/) (currently closed beta, get in touch if you are interested in trying it out)
|
|
17
|
+
2. Get your API key from [localhero.ai/api-keys](https://localhero.ai/api-keys)
|
|
18
|
+
3. Run the init command in your project:
|
|
19
|
+
```bash
|
|
20
|
+
npx @localheroai/cli init
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Commands š
|
|
24
|
+
|
|
25
|
+
### Initialize a Project
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
npx @localheroai/cli init
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The init command helps you set up your project with LocalHero.ai. It will:
|
|
32
|
+
- Detect your project type (Rails, React, or generic)
|
|
33
|
+
- Link to an existing LocalHero.ai project
|
|
34
|
+
- Configure translation paths and file patterns
|
|
35
|
+
- Set up GitHub Actions (optional)
|
|
36
|
+
- Import existing translations (optional)
|
|
37
|
+
|
|
38
|
+
This creates a `localhero.json` configuration file in your project root that stores your project settings:
|
|
39
|
+
- Project identifier
|
|
40
|
+
- Source and target languages for translation
|
|
41
|
+
- Translation file paths and patterns
|
|
42
|
+
- Ignore patterns for files to exclude
|
|
43
|
+
|
|
44
|
+
The configuration file is used by the tool to interact with your translations and the API.
|
|
45
|
+
|
|
46
|
+
### Login
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
npx @localheroai/cli login
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Authenticate with your LocalHero.ai account. Use this when:
|
|
53
|
+
- Setting up a new development environment
|
|
54
|
+
- Updating your API key
|
|
55
|
+
- Verifying your authentication status
|
|
56
|
+
|
|
57
|
+
### Translate
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npx @localheroai/cli translate
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Translating your missing keys:
|
|
64
|
+
- Automatically detects missing translations and sends them to the Localhero.ai translation API for translation
|
|
65
|
+
- Updates translation files with any new or update translations
|
|
66
|
+
- It's run manually or by GitHub Actions
|
|
67
|
+
|
|
68
|
+
## Environment Variables āļø
|
|
69
|
+
|
|
70
|
+
Configure the CLI behavior with these environment variables:
|
|
71
|
+
|
|
72
|
+
| Variable | Description | Default |
|
|
73
|
+
|----------|-------------|---------|
|
|
74
|
+
| `LOCALHERO_API_KEY` | Your LocalHero API key (get it at [localhero.ai/api-keys](https://localhero.ai/api-keys)) | Required |
|
|
75
|
+
| `LOCALHERO_API_HOST` | API host for LocalHero (you typically don't need to change this) | https://api.localhero.ai |
|
|
76
|
+
|
|
77
|
+
## GitHub Actions Integration š¤
|
|
78
|
+
|
|
79
|
+
LocalHero.ai automatically translate your I18n files when you push changes. During the `init` command, you'll be prompted to set up GitHub Actions.
|
|
80
|
+
|
|
81
|
+
1. Add your API key to your repository secrets:
|
|
82
|
+
- Go to Settings > Secrets > Actions
|
|
83
|
+
- Create a new secret named `LOCALHERO_API_KEY`
|
|
84
|
+
- Add your API key as the value
|
|
85
|
+
|
|
86
|
+
2. The workflow will:
|
|
87
|
+
- Run on push to your main branch
|
|
88
|
+
- Check for missing translations
|
|
89
|
+
- Create a pull request with new translations
|
|
90
|
+
|
|
91
|
+
## Support š¬
|
|
92
|
+
|
|
93
|
+
- Documentation: [localhero.ai/docs](https://docs.localhero.ai/docs)
|
|
94
|
+
- Email: support@localhero.ai
|
|
95
|
+
|
|
96
|
+
## License š
|
|
97
|
+
|
|
98
|
+
MIT License - see LICENSE file for details
|
package/package.json
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
2
|
+
"name": "@localheroai/cli",
|
|
3
|
+
"version": "0.0.3",
|
|
4
|
+
"description": "CLI tool for managing translations with LocalHero.ai",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"localheroai": "./src/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"start": "node src/cli.js",
|
|
12
|
+
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
13
|
+
"lint": "eslint .",
|
|
14
|
+
"postinstall": "chmod +x src/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"i18n",
|
|
18
|
+
"translation",
|
|
19
|
+
"cli",
|
|
20
|
+
"localization"
|
|
21
|
+
],
|
|
22
|
+
"author": "LocalHero.ai",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"dependencies": {
|
|
25
|
+
"chalk": "^5.3.0",
|
|
26
|
+
"commander": "^12.0.0",
|
|
27
|
+
"glob": "^10.3.10",
|
|
28
|
+
"inquirer": "^12.0.1",
|
|
29
|
+
"yaml": "^2.3.4"
|
|
30
|
+
},
|
|
31
|
+
"devDependencies": {
|
|
32
|
+
"@babel/preset-env": "^7.24.0",
|
|
33
|
+
"@inquirer/testing": "^2.1.36",
|
|
34
|
+
"@jest/globals": "^29.7.0",
|
|
35
|
+
"eslint": "^9.19.0",
|
|
36
|
+
"globals": "^15.14.0",
|
|
37
|
+
"jest": "^29.7.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=22.0.0"
|
|
41
|
+
},
|
|
42
|
+
"files": [
|
|
43
|
+
"src",
|
|
44
|
+
"README.md",
|
|
45
|
+
"LICENSE"
|
|
46
|
+
],
|
|
47
|
+
"jest": {
|
|
48
|
+
"testEnvironment": "node",
|
|
49
|
+
"transform": {},
|
|
50
|
+
"moduleNameMapper": {
|
|
51
|
+
"^(\\.{1,2}/.*)\\.js$": "$1"
|
|
9
52
|
},
|
|
10
|
-
"
|
|
11
|
-
|
|
12
|
-
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
13
|
-
"lint": "eslint .",
|
|
14
|
-
"postinstall": "chmod +x src/cli.js"
|
|
15
|
-
},
|
|
16
|
-
"keywords": [
|
|
17
|
-
"i18n",
|
|
18
|
-
"translation",
|
|
19
|
-
"cli",
|
|
20
|
-
"localization"
|
|
21
|
-
],
|
|
22
|
-
"author": "LocalHero.ai",
|
|
23
|
-
"license": "MIT",
|
|
24
|
-
"dependencies": {
|
|
25
|
-
"chalk": "^5.3.0",
|
|
26
|
-
"commander": "^12.0.0",
|
|
27
|
-
"dotenv": "^16.4.5",
|
|
28
|
-
"glob": "^10.3.10",
|
|
29
|
-
"inquirer": "^12.0.1",
|
|
30
|
-
"yaml": "^2.3.4"
|
|
31
|
-
},
|
|
32
|
-
"devDependencies": {
|
|
33
|
-
"@inquirer/testing": "^2.1.36",
|
|
34
|
-
"@jest/globals": "^29.7.0",
|
|
35
|
-
"eslint": "^9.14.0",
|
|
36
|
-
"jest": "^29.7.0",
|
|
37
|
-
"@babel/preset-env": "^7.24.0"
|
|
38
|
-
},
|
|
39
|
-
"engines": {
|
|
40
|
-
"node": ">=22.11.0"
|
|
41
|
-
},
|
|
42
|
-
"files": [
|
|
43
|
-
"src",
|
|
44
|
-
"README.md",
|
|
45
|
-
"LICENSE"
|
|
53
|
+
"testMatch": [
|
|
54
|
+
"**/tests/**/*.test.js"
|
|
46
55
|
],
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
"
|
|
50
|
-
|
|
51
|
-
"^(\\.{1,2}/.*)\\.js$": "$1"
|
|
52
|
-
},
|
|
53
|
-
"testMatch": [
|
|
54
|
-
"**/tests/**/*.test.js"
|
|
55
|
-
],
|
|
56
|
-
"testEnvironmentOptions": {
|
|
57
|
-
"extensionsToTreatAsEsm": [
|
|
58
|
-
".js"
|
|
59
|
-
]
|
|
60
|
-
}
|
|
56
|
+
"testEnvironmentOptions": {
|
|
57
|
+
"extensionsToTreatAsEsm": [
|
|
58
|
+
".js"
|
|
59
|
+
]
|
|
61
60
|
}
|
|
61
|
+
}
|
|
62
62
|
}
|
package/src/api/auth.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
import { apiRequest } from './client.js';
|
|
2
2
|
|
|
3
3
|
export async function verifyApiKey(apiKey) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
4
|
+
try {
|
|
5
|
+
return await apiRequest('/api/v1/auth/verify', {
|
|
6
|
+
apiKey
|
|
7
|
+
});
|
|
8
|
+
} catch (error) {
|
|
9
|
+
if (error.code === 'invalid_api_key') {
|
|
10
|
+
return {
|
|
11
|
+
error: {
|
|
12
|
+
code: 'invalid_api_key',
|
|
13
|
+
message: error.message
|
|
14
|
+
}
|
|
15
|
+
};
|
|
14
16
|
}
|
|
15
|
-
|
|
17
|
+
return {
|
|
18
|
+
error: {
|
|
19
|
+
code: 'verification_failed',
|
|
20
|
+
message: error.message || 'Failed to verify API key'
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
}
|
package/src/api/client.js
CHANGED
|
@@ -1,41 +1,83 @@
|
|
|
1
1
|
const DEFAULT_API_HOST = 'https://api.localhero.ai';
|
|
2
2
|
|
|
3
3
|
export function getApiHost() {
|
|
4
|
-
|
|
4
|
+
return process.env.LOCALHERO_API_HOST || DEFAULT_API_HOST;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function getNetworkErrorMessage(error) {
|
|
8
|
+
if (error.code === 'ECONNREFUSED') {
|
|
9
|
+
return `Unable to connect to ${getApiHost()}. Please check your internet connection and try again.`;
|
|
10
|
+
}
|
|
11
|
+
if (error.cause?.code === 'ENOTFOUND') {
|
|
12
|
+
return `Could not resolve host ${getApiHost()}. Please check your internet connection and try again.`;
|
|
13
|
+
}
|
|
14
|
+
if (error.cause?.code === 'ETIMEDOUT') {
|
|
15
|
+
return `Connection to ${getApiHost()} timed out. Please try again later.`;
|
|
16
|
+
}
|
|
17
|
+
return `Network error while connecting to ${getApiHost()}. Please check your internet connection and try again.`;
|
|
5
18
|
}
|
|
6
19
|
|
|
7
20
|
export async function apiRequest(endpoint, options = {}) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
21
|
+
const apiHost = getApiHost();
|
|
22
|
+
const url = `${apiHost}${endpoint}`;
|
|
23
|
+
const apiKey = process.env.LOCALHERO_API_KEY || options.apiKey;
|
|
11
24
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
25
|
+
const headers = {
|
|
26
|
+
'Content-Type': 'application/json',
|
|
27
|
+
...options.headers
|
|
28
|
+
};
|
|
16
29
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
30
|
+
if (apiKey) {
|
|
31
|
+
headers['Authorization'] = `Bearer ${apiKey}`;
|
|
32
|
+
}
|
|
20
33
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
const fetchOptions = {
|
|
35
|
+
method: options.method || 'GET',
|
|
36
|
+
headers,
|
|
37
|
+
};
|
|
25
38
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
39
|
+
if (options.body) {
|
|
40
|
+
fetchOptions.body = options.body;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let response;
|
|
44
|
+
try {
|
|
45
|
+
response = await fetch(url, fetchOptions);
|
|
46
|
+
} catch (error) {
|
|
47
|
+
const message = getNetworkErrorMessage(error);
|
|
48
|
+
error.message = message;
|
|
49
|
+
error.cliErrorMessage = message;
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let data;
|
|
54
|
+
try {
|
|
55
|
+
data = await response.json();
|
|
56
|
+
} catch (error) {
|
|
57
|
+
const message = 'Failed to parse API response';
|
|
58
|
+
error.message = message;
|
|
59
|
+
error.cliErrorMessage = message;
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
32
62
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
63
|
+
if (!response.ok) {
|
|
64
|
+
if (response.status === 401 && data?.error?.code === 'invalid_api_key') {
|
|
65
|
+
const message = 'Your API key is invalid or has been revoked. Please run `npx @localheroai/cli login` to update your API key.';
|
|
66
|
+
const error = new Error(message);
|
|
67
|
+
error.cliErrorMessage = message;
|
|
68
|
+
error.code = 'invalid_api_key';
|
|
69
|
+
error.data = data;
|
|
70
|
+
throw error;
|
|
38
71
|
}
|
|
72
|
+
const message = Array.isArray(data?.errors)
|
|
73
|
+
? data.errors.map(err => typeof err === 'string' ? err : err.message).join(', ')
|
|
74
|
+
: data?.error?.message || 'API request failed';
|
|
75
|
+
const error = new Error(message);
|
|
76
|
+
error.cliErrorMessage = message;
|
|
77
|
+
error.code = data?.error?.code || 'API_ERROR';
|
|
78
|
+
error.data = data;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
39
81
|
|
|
40
|
-
|
|
41
|
-
}
|
|
82
|
+
return data;
|
|
83
|
+
}
|
package/src/api/imports.js
CHANGED
|
@@ -2,19 +2,21 @@ import { getApiKey } from '../utils/auth.js';
|
|
|
2
2
|
import { apiRequest } from './client.js';
|
|
3
3
|
|
|
4
4
|
export async function createImport({ projectId, translations }) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
})
|
|
11
|
-
|
|
5
|
+
const apiKey = await getApiKey();
|
|
6
|
+
const response = await apiRequest(`/api/v1/projects/${projectId}/imports`, {
|
|
7
|
+
method: 'POST',
|
|
8
|
+
body: JSON.stringify({
|
|
9
|
+
translations
|
|
10
|
+
}),
|
|
11
|
+
apiKey
|
|
12
|
+
});
|
|
13
|
+
return response;
|
|
12
14
|
}
|
|
13
15
|
|
|
14
16
|
export async function checkImportStatus(projectId, importId) {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
17
|
+
const apiKey = await getApiKey();
|
|
18
|
+
const response = await apiRequest(`/api/v1/projects/${projectId}/imports/${importId}`, {
|
|
19
|
+
apiKey
|
|
20
|
+
});
|
|
21
|
+
return response.import;
|
|
22
|
+
}
|
package/src/api/projects.js
CHANGED
|
@@ -2,23 +2,23 @@ import { getApiKey } from '../utils/auth.js';
|
|
|
2
2
|
import { apiRequest } from './client.js';
|
|
3
3
|
|
|
4
4
|
export async function listProjects() {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
const apiKey = await getApiKey();
|
|
6
|
+
const response = await apiRequest('/api/v1/projects', { apiKey });
|
|
7
|
+
return response.projects;
|
|
8
8
|
}
|
|
9
9
|
|
|
10
10
|
export async function createProject(data) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
}
|
|
11
|
+
const apiKey = await getApiKey();
|
|
12
|
+
const response = await apiRequest('/api/v1/projects', {
|
|
13
|
+
method: 'POST',
|
|
14
|
+
body: JSON.stringify({
|
|
15
|
+
project: {
|
|
16
|
+
name: data.name,
|
|
17
|
+
source_language: data.sourceLocale,
|
|
18
|
+
target_languages: data.targetLocales
|
|
19
|
+
}
|
|
20
|
+
}),
|
|
21
|
+
apiKey
|
|
22
|
+
});
|
|
23
|
+
return response.project;
|
|
24
|
+
}
|
package/src/api/translations.js
CHANGED
|
@@ -1,37 +1,58 @@
|
|
|
1
1
|
import { getApiKey } from '../utils/auth.js';
|
|
2
2
|
import { apiRequest } from './client.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
3
|
+
import { getCurrentBranch } from '../utils/git.js';
|
|
4
|
+
|
|
5
|
+
export async function createTranslationJob({ sourceFiles, targetLocales, projectId, targetPaths }) {
|
|
6
|
+
const apiKey = await getApiKey();
|
|
7
|
+
const branch = await getCurrentBranch();
|
|
8
|
+
|
|
9
|
+
const response = await apiRequest(`/api/v1/projects/${projectId}/translation_jobs`, {
|
|
10
|
+
method: 'POST',
|
|
11
|
+
body: JSON.stringify({
|
|
12
|
+
target_languages: targetLocales,
|
|
13
|
+
files: sourceFiles.map(file => ({
|
|
14
|
+
path: file.path,
|
|
15
|
+
content: file.content,
|
|
16
|
+
format: file.format,
|
|
17
|
+
target_paths: targetPaths
|
|
18
|
+
})),
|
|
19
|
+
...(branch && { branch })
|
|
20
|
+
}),
|
|
21
|
+
apiKey
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
if (!response.jobs || !response.jobs.length) {
|
|
25
|
+
throw new Error('No translation jobs were created');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
jobs: response.jobs,
|
|
30
|
+
totalJobs: response.jobs.length
|
|
31
|
+
};
|
|
26
32
|
}
|
|
27
33
|
|
|
28
34
|
export async function checkJobStatus(jobId, includeTranslations = false) {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
const apiKey = await getApiKey();
|
|
36
|
+
const endpoint = `/api/v1/translation_jobs/${jobId}${includeTranslations ? '?include_translations=true' : ''}`;
|
|
37
|
+
return apiRequest(endpoint, { apiKey });
|
|
32
38
|
}
|
|
33
39
|
|
|
34
40
|
export async function getTranslations(jobId) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
}
|
|
41
|
+
const apiKey = await getApiKey();
|
|
42
|
+
return apiRequest(`/api/v1/translation_jobs/${jobId}/translations`, { apiKey });
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export async function getUpdates(projectId, { since, page = 1 }) {
|
|
46
|
+
const apiKey = await getApiKey();
|
|
47
|
+
|
|
48
|
+
if (!since) {
|
|
49
|
+
throw new Error('Missing required parameter: since (ISO 8601 timestamp)');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const queryParams = new URLSearchParams({
|
|
53
|
+
since,
|
|
54
|
+
page: page.toString()
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
return apiRequest(`/api/v1/projects/${projectId}/updates?${queryParams}`, { apiKey });
|
|
58
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -5,67 +5,74 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { readFileSync } from 'fs';
|
|
6
6
|
import { login } from './commands/login.js';
|
|
7
7
|
import { init } from './commands/init.js';
|
|
8
|
-
import { defaultDependencies } from './utils/defaults.js';
|
|
9
8
|
import { translate } from './commands/translate.js';
|
|
9
|
+
import { sync } from './commands/sync.js';
|
|
10
10
|
|
|
11
11
|
const program = new Command();
|
|
12
12
|
|
|
13
|
-
function displayBanner() {
|
|
14
|
-
console.log(chalk.blue(`
|
|
15
|
-
===============================================
|
|
16
|
-
|
|
17
|
-
LocalHero.ai CLI
|
|
18
|
-
|
|
19
|
-
===============================================
|
|
20
|
-
`));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
13
|
function getVersion() {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
const packageJson = JSON.parse(
|
|
15
|
+
readFileSync(new URL('../package.json', import.meta.url))
|
|
16
|
+
);
|
|
17
|
+
return packageJson.version;
|
|
28
18
|
}
|
|
29
19
|
|
|
30
20
|
function handleApiError(error) {
|
|
31
|
-
|
|
32
|
-
|
|
21
|
+
console.error(chalk.red(`ā ${error.cliErrorMessage || error.message}`));
|
|
22
|
+
|
|
23
|
+
if (program.opts().debug) {
|
|
24
|
+
console.error(chalk.dim(error.stack || error));
|
|
25
|
+
|
|
26
|
+
if (error.cause) {
|
|
27
|
+
console.error(chalk.dim(error.cause.stack || error.cause));
|
|
28
|
+
}
|
|
29
|
+
} else {
|
|
30
|
+
console.error(chalk.dim('\nRun with --debug for more information'));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
process.exit(1);
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
function wrapCommandAction(action) {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
return function (...args) {
|
|
38
|
+
return Promise.resolve(action(...args)).catch(handleApiError);
|
|
39
|
+
};
|
|
39
40
|
}
|
|
40
41
|
|
|
41
42
|
program
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
.name('localhero')
|
|
44
|
+
.description('CLI tool for automatic I18n translations with LocalHero.ai, more info at https://localhero.ai.')
|
|
45
|
+
.version(getVersion())
|
|
46
|
+
.option('--debug', 'Show debug information when errors occur')
|
|
47
|
+
.action(() => {
|
|
48
|
+
console.log('LocalHero.ai is automatic I18n translations service that easily integrates with your dev workflow.');
|
|
49
|
+
console.log(`\nVersion: ${getVersion()}`);
|
|
50
|
+
console.log('\nš Visit https://localhero.ai for more information');
|
|
51
|
+
console.log('š Set up your project with `npx @localheroai/cli init`');
|
|
52
|
+
console.log('š” Use --help to see available commands');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
program
|
|
56
|
+
.command('login')
|
|
57
|
+
.description('Authenticate with LocalHero.ai using an API key')
|
|
58
|
+
.action(wrapCommandAction(() => login()));
|
|
53
59
|
|
|
54
60
|
program
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
.command('init')
|
|
62
|
+
.description('Initialize a new LocalHero.ai project')
|
|
63
|
+
.action(wrapCommandAction(() => init()));
|
|
58
64
|
|
|
59
65
|
program
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
66
|
+
.command('translate')
|
|
67
|
+
.description('Translate missing keys in your i18n files')
|
|
68
|
+
.option('-v, --verbose', 'Show detailed progress information')
|
|
69
|
+
.option('-c, --commit', 'Automatically commit changes (useful for CI/CD)')
|
|
70
|
+
.action(wrapCommandAction((options) => translate(options)));
|
|
63
71
|
|
|
64
72
|
program
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
.action(wrapCommandAction((options) => translate(options)));
|
|
73
|
+
.command('sync')
|
|
74
|
+
.description('Sync updates from LocalHero.ai to your local files')
|
|
75
|
+
.option('-v, --verbose', 'Show detailed progress information')
|
|
76
|
+
.action(wrapCommandAction((options) => sync(options)));
|
|
70
77
|
|
|
71
78
|
program.parse();
|