@localheroai/cli 0.0.2 → 0.0.5
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 +63 -58
- 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 +232 -247
- package/src/utils/auth.js +15 -15
- package/src/utils/config.js +115 -86
- package/src/utils/files.js +358 -116
- package/src/utils/git.js +64 -8
- package/src/utils/github.js +83 -47
- package/src/utils/import-service.js +111 -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 +111 -0
- package/src/utils/translation-updater/yaml-handler.js +207 -0
- package/src/utils/translation-utils.js +278 -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
|
@@ -1,282 +1,267 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import { configService } from '../utils/config.js';
|
|
3
|
-
import { findTranslationFiles } from '../utils/files.js';
|
|
3
|
+
import { findTranslationFiles, parseFile, flattenTranslations } from '../utils/files.js';
|
|
4
4
|
import { createTranslationJob, checkJobStatus } from '../api/translations.js';
|
|
5
|
-
import { updateTranslationFile } from '../utils/translation-updater.js';
|
|
6
|
-
import path from 'path';
|
|
5
|
+
import { updateTranslationFile } from '../utils/translation-updater/index.js';
|
|
7
6
|
import { checkAuth } from '../utils/auth.js';
|
|
7
|
+
import { findMissingTranslations, batchKeysWithMissing, processLocaleTranslations } from '../utils/translation-utils.js';
|
|
8
|
+
import { syncService } from '../utils/sync-service.js';
|
|
8
9
|
import { autoCommitChanges } from '../utils/github.js';
|
|
9
10
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
const BATCH_SIZE = 50;
|
|
12
|
+
|
|
13
|
+
const defaultDeps = {
|
|
14
|
+
console,
|
|
15
|
+
configUtils: configService,
|
|
16
|
+
authUtils: { checkAuth },
|
|
17
|
+
fileUtils: { findTranslationFiles },
|
|
18
|
+
translationUtils: {
|
|
19
|
+
createTranslationJob,
|
|
20
|
+
checkJobStatus,
|
|
21
|
+
updateTranslationFile,
|
|
22
|
+
findMissingTranslations,
|
|
23
|
+
batchKeysWithMissing
|
|
24
|
+
},
|
|
25
|
+
syncService,
|
|
26
|
+
gitUtils: { autoCommitChanges }
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export async function translate(options = {}, deps = defaultDeps) {
|
|
30
|
+
const { console, configUtils, authUtils, fileUtils, translationUtils, syncService, gitUtils } = deps;
|
|
31
|
+
const { verbose } = options;
|
|
32
|
+
|
|
33
|
+
const isAuthenticated = await authUtils.checkAuth();
|
|
34
|
+
if (!isAuthenticated) {
|
|
35
|
+
console.error(chalk.red('\n✖ Your API key is invalid. Please run `npx @localheroai/cli login` to authenticate.\n'));
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const config = await configUtils.getProjectConfig();
|
|
40
|
+
if (!config) {
|
|
41
|
+
console.error(chalk.red('\n✖ No configuration found. Please run `npx @localheroai/cli init` first.\n'));
|
|
42
|
+
process.exit(1);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (!config.translationFiles?.paths) {
|
|
47
|
+
console.error(chalk.red('\n✖ Invalid configuration: missing translationFiles.paths. Please run `npx @localheroai/cli init` to set up your configuration.\n'));
|
|
48
|
+
process.exit(1);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const { hasUpdates, updates } = await syncService.checkForUpdates({ verbose });
|
|
53
|
+
if (hasUpdates) {
|
|
54
|
+
await syncService.applyUpdates(updates, { verbose });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (verbose) {
|
|
58
|
+
console.log(chalk.blue('\nℹ Using configuration:'));
|
|
59
|
+
console.log(chalk.gray(` Project ID: ${config.projectId}`));
|
|
60
|
+
console.log(chalk.gray(` Source locale: ${config.sourceLocale}`));
|
|
61
|
+
console.log(chalk.gray(` Output locales: ${config.outputLocales.join(', ')}`));
|
|
62
|
+
console.log(chalk.gray(` Translation files: ${config.translationFiles.paths.join(', ')}`));
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const result = await fileUtils.findTranslationFiles(config, { verbose, returnFullResult: true });
|
|
66
|
+
const { sourceFiles, targetFilesByLocale, allFiles } = result;
|
|
67
|
+
|
|
68
|
+
if (!allFiles || allFiles.length === 0) {
|
|
69
|
+
console.error(chalk.red('\n✖ No translation files found in the specified paths.\n'));
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (verbose) {
|
|
74
|
+
console.log(chalk.blue(`\nℹ Found ${allFiles.length} translation files`));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (sourceFiles.length === 0) {
|
|
78
|
+
console.error(chalk.red(`\n✖ No source files found for locale ${config.sourceLocale}\n`));
|
|
79
|
+
console.error(chalk.yellow(`This could be due to one of the following issues:`));
|
|
80
|
+
console.error(chalk.yellow(` 1. No translation files with the source locale "${config.sourceLocale}" exist in the configured paths`));
|
|
81
|
+
console.error(chalk.yellow(` 2. The locale identifiers in your filenames don't match the expected pattern`));
|
|
82
|
+
console.error(chalk.yellow(` 3. There was an error parsing one or more files (check for syntax errors in YAML or JSON)\n`));
|
|
83
|
+
console.error(chalk.yellow(`Try running with the --verbose flag for more detailed information.\n`));
|
|
84
|
+
process.exit(1);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (verbose) {
|
|
88
|
+
console.log(chalk.blue(`ℹ Found ${sourceFiles.length} source files for locale ${config.sourceLocale}`));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const missingByLocale = {};
|
|
92
|
+
|
|
93
|
+
for (const sourceFile of sourceFiles) {
|
|
94
|
+
const sourceContentRaw = Buffer.from(sourceFile.content, 'base64').toString();
|
|
95
|
+
const sourceContent = parseFile(sourceContentRaw, sourceFile.format);
|
|
96
|
+
const sourceKeys = flattenTranslations(sourceContent[config.sourceLocale] || sourceContent);
|
|
97
|
+
|
|
98
|
+
for (const targetLocale of config.outputLocales) {
|
|
99
|
+
const targetFiles = targetFilesByLocale[targetLocale] || [];
|
|
100
|
+
const result = processLocaleTranslations(sourceKeys, targetLocale, targetFiles, sourceFile, config.sourceLocale);
|
|
101
|
+
|
|
102
|
+
if (Object.keys(result.missingKeys).length > 0) {
|
|
103
|
+
if (!missingByLocale[targetLocale]) {
|
|
104
|
+
missingByLocale[targetLocale] = {
|
|
105
|
+
path: sourceFile.path,
|
|
106
|
+
targetPath: result.targetPath,
|
|
107
|
+
keys: {},
|
|
108
|
+
keyCount: 0
|
|
109
|
+
};
|
|
110
|
+
}
|
|
17
111
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
112
|
+
missingByLocale[targetLocale].keys = {
|
|
113
|
+
...missingByLocale[targetLocale].keys,
|
|
114
|
+
...result.missingKeys
|
|
115
|
+
};
|
|
116
|
+
missingByLocale[targetLocale].keyCount += Object.keys(result.missingKeys).length;
|
|
117
|
+
}
|
|
21
118
|
|
|
22
|
-
|
|
23
|
-
|
|
119
|
+
if (verbose && Object.keys(result.skippedKeys).length > 0) {
|
|
120
|
+
console.log(chalk.yellow(`\nℹ Skipped ${Object.keys(result.skippedKeys).length} keys marked as WIP in ${sourceFile.path}`));
|
|
121
|
+
}
|
|
24
122
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const missingLocales = Object.keys(missingByLocale);
|
|
126
|
+
if (missingLocales.length === 0) {
|
|
127
|
+
console.log(chalk.green('✓ All translations are up to date!'));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (verbose) {
|
|
132
|
+
console.log(chalk.blue('\nℹ Missing translations:'));
|
|
133
|
+
for (const [locale, data] of Object.entries(missingByLocale)) {
|
|
134
|
+
console.log(chalk.gray(` ${locale}: ${data.keyCount} keys`));
|
|
28
135
|
}
|
|
136
|
+
}
|
|
29
137
|
|
|
30
|
-
|
|
31
|
-
throw new Error('translationFiles.paths must be an array of paths');
|
|
32
|
-
}
|
|
33
|
-
}
|
|
138
|
+
const { batches, errors } = translationUtils.batchKeysWithMissing(sourceFiles, missingByLocale, BATCH_SIZE);
|
|
34
139
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
for (const
|
|
38
|
-
|
|
39
|
-
missing[key] = value;
|
|
40
|
-
}
|
|
140
|
+
if (errors.length > 0) {
|
|
141
|
+
console.error(chalk.red('\n✖ Errors occurred while preparing translation jobs:'));
|
|
142
|
+
for (const error of errors) {
|
|
143
|
+
console.error(chalk.red(` ${error.message}`));
|
|
41
144
|
}
|
|
42
|
-
|
|
43
|
-
}
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
let totalTranslated = 0;
|
|
149
|
+
let totalLanguages = 0;
|
|
150
|
+
const processedLocales = new Set();
|
|
151
|
+
const allJobIds = [];
|
|
152
|
+
let resultsBaseUrl = null;
|
|
153
|
+
|
|
154
|
+
for (const batch of batches) {
|
|
155
|
+
const targetPaths = Object.entries(missingByLocale).reduce((acc, [locale, data]) => {
|
|
156
|
+
acc[locale] = data.targetPath;
|
|
157
|
+
return acc;
|
|
158
|
+
}, {});
|
|
159
|
+
|
|
160
|
+
const jobRequest = {
|
|
161
|
+
projectId: config.projectId,
|
|
162
|
+
sourceFiles: batch.files,
|
|
163
|
+
targetLocales: batch.locales,
|
|
164
|
+
targetPaths
|
|
165
|
+
};
|
|
44
166
|
|
|
45
|
-
async function retryWithBackoff(operation, attempt = 1) {
|
|
46
167
|
try {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const backoffTime = Math.min(1000 * Math.pow(2, attempt), 30000);
|
|
52
|
-
const jitter = Math.random() * 1000;
|
|
53
|
-
const waitTime = backoffTime + jitter;
|
|
54
|
-
|
|
55
|
-
console.log(chalk.yellow(`⚠️ API error, retrying in ${Math.round(waitTime / 1000)}s (attempt ${attempt}/${MAX_RETRY_ATTEMPTS})`));
|
|
56
|
-
console.log(error);
|
|
57
|
-
await new Promise(resolve => setTimeout(resolve, waitTime));
|
|
58
|
-
|
|
59
|
-
return retryWithBackoff(operation, attempt + 1);
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function batchKeysWithMissing(sourceFiles, missingByLocale, batchSize = BATCH_SIZE) {
|
|
64
|
-
const batches = [];
|
|
65
|
-
const sourceFileEntries = new Map();
|
|
66
|
-
|
|
67
|
-
for (const [locale, localeData] of Object.entries(missingByLocale)) {
|
|
68
|
-
const sourceFile = sourceFiles.find(f => f.path === localeData.path);
|
|
69
|
-
if (!sourceFile) continue;
|
|
70
|
-
|
|
71
|
-
if (!sourceFileEntries.has(sourceFile.path)) {
|
|
72
|
-
sourceFileEntries.set(sourceFile.path, {
|
|
73
|
-
path: path.relative(process.cwd(), sourceFile.path),
|
|
74
|
-
keys: {},
|
|
75
|
-
locales: new Set()
|
|
76
|
-
});
|
|
77
|
-
}
|
|
168
|
+
const response = await translationUtils.createTranslationJob(jobRequest);
|
|
169
|
+
const { jobs } = response;
|
|
170
|
+
const batchJobIds = jobs.map(job => job.id);
|
|
78
171
|
|
|
79
|
-
|
|
80
|
-
entry.keys = { ...entry.keys, ...localeData.keys };
|
|
81
|
-
entry.locales.add(locale);
|
|
82
|
-
}
|
|
172
|
+
allJobIds.push(...batchJobIds);
|
|
83
173
|
|
|
84
|
-
|
|
85
|
-
const keys = entry.keys;
|
|
86
|
-
const keyEntries = Object.entries(keys);
|
|
87
|
-
for (let i = 0; i < keyEntries.length; i += batchSize) {
|
|
88
|
-
const batchKeys = Object.fromEntries(keyEntries.slice(i, i + batchSize));
|
|
89
|
-
batches.push([{
|
|
90
|
-
path: entry.path,
|
|
91
|
-
content: Buffer.from(JSON.stringify({ keys: batchKeys })).toString('base64')
|
|
92
|
-
}]);
|
|
93
|
-
}
|
|
94
|
-
}
|
|
174
|
+
const pendingJobs = new Set(batchJobIds);
|
|
95
175
|
|
|
96
|
-
|
|
97
|
-
|
|
176
|
+
while (pendingJobs.size > 0) {
|
|
177
|
+
const jobPromises = Array.from(pendingJobs).map(async jobId => {
|
|
178
|
+
if (verbose) {
|
|
179
|
+
console.log(chalk.blue(`\nℹ Checking job ${jobId}`));
|
|
180
|
+
}
|
|
98
181
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
182
|
+
let status;
|
|
183
|
+
let retries = 0;
|
|
184
|
+
const MAX_WAIT_MINUTES = 10;
|
|
185
|
+
const startTime = Date.now();
|
|
102
186
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const isAuthenticated = await checkAuth();
|
|
187
|
+
do {
|
|
188
|
+
status = await translationUtils.checkJobStatus(jobId, true);
|
|
106
189
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
console.log(chalk.blue('ℹ️ Loading configuration from localhero.json'));
|
|
112
|
-
|
|
113
|
-
const { translationFiles } = config;
|
|
114
|
-
const fileLocaleRegex = DEFAULT_LOCALE_REGEX;
|
|
115
|
-
let allFiles = [];
|
|
116
|
-
|
|
117
|
-
for (const translationPath of translationFiles.paths) {
|
|
118
|
-
const filesInPath = await findTranslationFiles(translationPath, fileLocaleRegex);
|
|
119
|
-
allFiles = allFiles.concat(filesInPath);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
if (!allFiles.length) {
|
|
123
|
-
console.error(chalk.red('❌ No translation files found'));
|
|
124
|
-
process.exit(1);
|
|
125
|
-
}
|
|
190
|
+
if (status.status === 'failed') {
|
|
191
|
+
throw new Error(`Translation job failed: ${status.error_details || 'Unknown error'}`);
|
|
192
|
+
}
|
|
126
193
|
|
|
127
|
-
|
|
128
|
-
|
|
194
|
+
if (status.status === 'pending' || status.status === 'processing') {
|
|
195
|
+
const elapsed = Math.floor((Date.now() - startTime) / 1000);
|
|
196
|
+
if (elapsed > MAX_WAIT_MINUTES * 60) {
|
|
197
|
+
throw new Error(`Translation timed out after ${MAX_WAIT_MINUTES} minutes`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
const waitSeconds = Math.min(2 ** retries, 30);
|
|
201
|
+
if (verbose) {
|
|
202
|
+
console.log(chalk.blue(` Job ${jobId} is ${status.status}, checking again in ${waitSeconds}s...`));
|
|
203
|
+
}
|
|
204
|
+
await new Promise(resolve => setTimeout(resolve, waitSeconds * 1000));
|
|
205
|
+
retries = Math.min(retries + 1, 5);
|
|
206
|
+
return { jobId, status: 'pending' };
|
|
207
|
+
}
|
|
129
208
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
209
|
+
if (status.status === 'completed' && status.translations?.data && status.language?.code) {
|
|
210
|
+
if (status.results_url && !resultsBaseUrl) {
|
|
211
|
+
resultsBaseUrl = status.results_url.split('?')[0];
|
|
212
|
+
}
|
|
134
213
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const sourceContent = JSON.parse(Buffer.from(sourceFile.content, 'base64').toString());
|
|
141
|
-
|
|
142
|
-
for (const targetLocale of config.outputLocales) {
|
|
143
|
-
const targetFile = targetFiles.find(f => f.locale === targetLocale);
|
|
144
|
-
const targetContent = targetFile ?
|
|
145
|
-
JSON.parse(Buffer.from(targetFile.content, 'base64').toString()) :
|
|
146
|
-
{ keys: {} };
|
|
147
|
-
|
|
148
|
-
const missing = findMissingTranslations(sourceContent.keys, targetContent.keys);
|
|
149
|
-
const missingCount = Object.keys(missing).length;
|
|
150
|
-
|
|
151
|
-
if (missingCount > 0) {
|
|
152
|
-
if (!missingByLocale[targetLocale]) {
|
|
153
|
-
missingByLocale[targetLocale] = {
|
|
154
|
-
keys: {},
|
|
155
|
-
path: sourceFile.path
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
missingByLocale[targetLocale].keys = {
|
|
159
|
-
...missingByLocale[targetLocale].keys,
|
|
160
|
-
...missing
|
|
161
|
-
};
|
|
162
|
-
console.log(chalk.blue(` ${targetLocale} (${path.basename(sourceFile.path)}): ${missingCount} missing keys`));
|
|
214
|
+
const languageCode = status.language.code;
|
|
215
|
+
if (!processedLocales.has(languageCode)) {
|
|
216
|
+
const targetPath = missingByLocale[languageCode].targetPath;
|
|
217
|
+
if (verbose) {
|
|
218
|
+
console.log(chalk.blue(` Updating translations for ${languageCode} in ${targetPath}`));
|
|
163
219
|
}
|
|
220
|
+
await translationUtils.updateTranslationFile(targetPath, status.translations.data, languageCode);
|
|
221
|
+
totalTranslated += missingByLocale[languageCode].keyCount;
|
|
222
|
+
totalLanguages++;
|
|
223
|
+
processedLocales.add(languageCode);
|
|
224
|
+
}
|
|
225
|
+
return { jobId, status: 'completed' };
|
|
164
226
|
}
|
|
165
|
-
}
|
|
166
227
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
228
|
+
return { jobId, status: status.status };
|
|
229
|
+
} while (status.status === 'pending' || status.status === 'processing');
|
|
230
|
+
});
|
|
171
231
|
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
for (const [batchIndex, batch] of batches.entries()) {
|
|
180
|
-
log(chalk.blue(`\nProcessing batch ${batchIndex + 1}/${batches.length}...`));
|
|
181
|
-
|
|
182
|
-
try {
|
|
183
|
-
const { jobs } = await retryWithBackoff(() =>
|
|
184
|
-
createTranslationJob({
|
|
185
|
-
sourceFiles: batch,
|
|
186
|
-
targetLocales: config.outputLocales,
|
|
187
|
-
projectId: config.projectId
|
|
188
|
-
})
|
|
189
|
-
);
|
|
190
|
-
const jobStatuses = new Map();
|
|
191
|
-
let allCompleted = false;
|
|
192
|
-
|
|
193
|
-
while (!allCompleted) {
|
|
194
|
-
allCompleted = true;
|
|
195
|
-
let totalProgress = 0;
|
|
196
|
-
|
|
197
|
-
for (const job of jobs) {
|
|
198
|
-
const status = await retryWithBackoff(() => checkJobStatus(job.id, true));
|
|
199
|
-
jobStatuses.set(job.id, status);
|
|
200
|
-
|
|
201
|
-
if (status.status === 'processing') {
|
|
202
|
-
allCompleted = false;
|
|
203
|
-
}
|
|
204
|
-
totalProgress += status.progress.percentage;
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
const averageProgress = Math.round(totalProgress / jobs.length);
|
|
208
|
-
const bar = '='.repeat(averageProgress / 2) + ' '.repeat(50 - averageProgress / 2);
|
|
209
|
-
|
|
210
|
-
process.stdout.write(`\r⧗ Translating... [${bar}] ${averageProgress}%`);
|
|
211
|
-
|
|
212
|
-
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
213
|
-
}
|
|
232
|
+
const results = await Promise.all(jobPromises);
|
|
233
|
+
results.forEach(result => {
|
|
234
|
+
if (result.status === 'completed') {
|
|
235
|
+
pendingJobs.delete(result.jobId);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
214
238
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
if (status.status === 'completed' && status.translations) {
|
|
218
|
-
if (!hasShownUpdateMessage) {
|
|
219
|
-
console.log(chalk.blue('\nℹ️ Updating translation files...'));
|
|
220
|
-
hasShownUpdateMessage = true;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const translations = status.translations.translations;
|
|
224
|
-
const targetLocale = status.language.code;
|
|
225
|
-
const uniqueKey = `${targetLocale}:${Object.keys(translations).sort().join(',')}`;
|
|
226
|
-
|
|
227
|
-
if (processedKeys.has(uniqueKey)) {
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
processedKeys.add(uniqueKey);
|
|
231
|
-
|
|
232
|
-
const targetFile = targetFiles.find(f => f.locale === targetLocale)?.path ||
|
|
233
|
-
path.join(config.translationPath, `${targetLocale}.yml`);
|
|
234
|
-
|
|
235
|
-
try {
|
|
236
|
-
const updatedKeys = await updateTranslationFile(
|
|
237
|
-
targetFile,
|
|
238
|
-
translations,
|
|
239
|
-
targetLocale
|
|
240
|
-
);
|
|
241
|
-
|
|
242
|
-
updatedFiles.add(targetFile);
|
|
243
|
-
totalKeysProcessed += updatedKeys.length;
|
|
244
|
-
|
|
245
|
-
if (verbose) {
|
|
246
|
-
console.log(chalk.blue(` Updated ${targetFile}`));
|
|
247
|
-
updatedKeys.forEach(key => console.log(chalk.gray(` - Added: ${key}`)));
|
|
248
|
-
} else {
|
|
249
|
-
console.log(chalk.blue(` Updated ${path.basename(targetFile)}`));
|
|
250
|
-
}
|
|
251
|
-
} catch (error) {
|
|
252
|
-
console.error(chalk.yellow(`⚠️ Failed to update ${targetFile}: ${error.message}`));
|
|
253
|
-
totalErrors++;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
|
-
} catch (error) {
|
|
258
|
-
totalErrors++;
|
|
259
|
-
console.error(chalk.red(`❌ Batch ${batchIndex + 1} failed: ${error.message}`));
|
|
260
|
-
console.log(error);
|
|
261
|
-
|
|
262
|
-
}
|
|
239
|
+
if (pendingJobs.size > 0) {
|
|
240
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
263
241
|
}
|
|
242
|
+
}
|
|
243
|
+
} catch (error) {
|
|
244
|
+
console.error(chalk.red(`\n✖ Error processing translation jobs: ${error.message}\n`));
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
264
248
|
|
|
265
|
-
|
|
266
|
-
console.error(chalk.red(`\n❌ Translation completed with ${totalErrors} failed batches`));
|
|
267
|
-
process.exit(1);
|
|
268
|
-
}
|
|
249
|
+
await configUtils.updateLastSyncedAt();
|
|
269
250
|
|
|
270
|
-
|
|
251
|
+
console.log(chalk.green('✓ Translations complete!'));
|
|
252
|
+
console.log(`Updated ${totalTranslated} keys in ${totalLanguages} languages`);
|
|
271
253
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
}
|
|
277
|
-
}
|
|
254
|
+
// Auto-commit changes if we're running in GitHub Actions
|
|
255
|
+
if (totalTranslated > 0) {
|
|
256
|
+
try {
|
|
257
|
+
gitUtils.autoCommitChanges(config.translationFiles.paths.join(' '));
|
|
278
258
|
} catch (error) {
|
|
279
|
-
|
|
280
|
-
process.exit(1);
|
|
259
|
+
console.warn(chalk.yellow(`\nℹ Could not auto-commit changes: ${error.message}`));
|
|
281
260
|
}
|
|
282
|
-
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (resultsBaseUrl && allJobIds.length > 0) {
|
|
264
|
+
const jobIdsParam = allJobIds.join(',');
|
|
265
|
+
console.log(`View job results at: ${resultsBaseUrl}?job_ids=${jobIdsParam}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
package/src/utils/auth.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
1
|
import { configService } from './config.js';
|
|
2
2
|
|
|
3
3
|
export async function getApiKey() {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
const envKey = process.env.LOCALHERO_API_KEY;
|
|
5
|
+
if (typeof envKey === 'string' && envKey.trim() !== '') {
|
|
6
|
+
return envKey;
|
|
7
|
+
}
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
const config = await configService.getAuthConfig();
|
|
10
|
+
return config?.api_key;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export async function checkAuth() {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
try {
|
|
15
|
+
const apiKey = await getApiKey();
|
|
16
|
+
const isValidFormat = typeof apiKey === 'string' &&
|
|
17
|
+
/^tk_[a-f0-9]+$/.test(apiKey);
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
19
|
+
return isValidFormat;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|