@localheroai/cli 0.0.3 → 0.0.6
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 +1 -1
- package/dist/api/auth.d.ts +2 -0
- package/dist/api/auth.js +28 -0
- package/dist/api/auth.js.map +1 -0
- package/dist/api/client.d.ts +3 -0
- package/dist/api/client.js +80 -0
- package/dist/api/client.js.map +1 -0
- package/dist/api/imports.d.ts +5 -0
- package/dist/api/imports.js +43 -0
- package/dist/api/imports.js.map +1 -0
- package/dist/api/projects.d.ts +2 -0
- package/dist/api/projects.js +42 -0
- package/dist/api/projects.js.map +1 -0
- package/dist/api/translations.d.ts +15 -0
- package/dist/api/translations.js +71 -0
- package/dist/api/translations.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +79 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/_sync.js +22 -0
- package/dist/commands/_sync.js.map +1 -0
- package/dist/commands/_translate.js +3 -0
- package/dist/commands/_translate.js.map +1 -0
- package/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +439 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/login.d.ts +16 -0
- package/dist/commands/login.js +58 -0
- package/dist/commands/login.js.map +1 -0
- package/dist/commands/pull.js +22 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/push.js +56 -0
- package/dist/commands/push.js.map +1 -0
- package/dist/commands/sync.d.ts +20 -0
- package/dist/commands/sync.js +22 -0
- package/dist/commands/sync.js.map +1 -0
- package/dist/commands/translate.d.ts +14 -0
- package/dist/commands/translate.js +145 -0
- package/dist/commands/translate.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +8 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +75 -0
- package/dist/types/index.js +17 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/translate/index.js +2 -0
- package/dist/types/translate/index.js.map +1 -0
- package/dist/utils/auth.d.ts +2 -0
- package/dist/utils/auth.js +29 -0
- package/dist/utils/auth.js.map +1 -0
- package/dist/utils/common.js +9 -0
- package/dist/utils/common.js.map +1 -0
- package/dist/utils/config.d.ts +23 -0
- package/dist/utils/config.js +137 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/errors.js +37 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/files.d.ts +32 -0
- package/dist/utils/files.js +347 -0
- package/dist/utils/files.js.map +1 -0
- package/dist/utils/git.d.ts +21 -0
- package/dist/utils/git.js +87 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/github.d.ts +241 -0
- package/dist/utils/github.js +161 -0
- package/dist/utils/github.js.map +1 -0
- package/dist/utils/import-service.d.ts +4 -0
- package/dist/utils/import-service.js +218 -0
- package/dist/utils/import-service.js.map +1 -0
- package/dist/utils/prompt-service.d.ts +44 -0
- package/dist/utils/prompt-service.js +104 -0
- package/dist/utils/prompt-service.js.map +1 -0
- package/dist/utils/sync-service.d.ts +58 -0
- package/dist/utils/sync-service.js +159 -0
- package/dist/utils/sync-service.js.map +1 -0
- package/dist/utils/translation-processor.js +197 -0
- package/dist/utils/translation-processor.js.map +1 -0
- package/dist/utils/translation-updater/common.d.ts +6 -0
- package/{src → dist}/utils/translation-updater/common.js +16 -10
- package/dist/utils/translation-updater/common.js.map +1 -0
- package/dist/utils/translation-updater/index.d.ts +5 -0
- package/{src → dist}/utils/translation-updater/index.js +21 -9
- package/dist/utils/translation-updater/index.js.map +1 -0
- package/dist/utils/translation-updater/json-handler.d.ts +5 -0
- package/{src → dist}/utils/translation-updater/json-handler.js +42 -31
- package/dist/utils/translation-updater/json-handler.js.map +1 -0
- package/dist/utils/translation-updater/yaml-handler.d.ts +5 -0
- package/{src → dist}/utils/translation-updater/yaml-handler.js +40 -41
- package/dist/utils/translation-updater/yaml-handler.js.map +1 -0
- package/dist/utils/translation-utils.d.ts +30 -0
- package/dist/utils/translation-utils.js +324 -0
- package/dist/utils/translation-utils.js.map +1 -0
- package/dist/utils/updater.js +38 -0
- package/dist/utils/updater.js.map +1 -0
- package/package.json +33 -28
- package/src/api/auth.js +0 -24
- package/src/api/client.js +0 -83
- package/src/api/imports.js +0 -22
- package/src/api/projects.js +0 -24
- package/src/api/translations.js +0 -58
- package/src/cli.js +0 -78
- package/src/commands/init.js +0 -485
- package/src/commands/login.js +0 -80
- package/src/commands/sync.js +0 -28
- package/src/commands/translate.js +0 -262
- package/src/utils/auth.js +0 -23
- package/src/utils/config.js +0 -125
- package/src/utils/files.js +0 -361
- package/src/utils/git.js +0 -72
- package/src/utils/github.js +0 -122
- package/src/utils/import-service.js +0 -129
- package/src/utils/prompt-service.js +0 -67
- package/src/utils/sync-service.js +0 -147
- package/src/utils/translation-utils.js +0 -237
package/src/utils/files.js
DELETED
|
@@ -1,361 +0,0 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { glob } from 'glob';
|
|
3
|
-
import { readFile } from 'fs/promises';
|
|
4
|
-
import path from 'path';
|
|
5
|
-
import yaml from 'yaml';
|
|
6
|
-
import { promises as fs } from 'fs';
|
|
7
|
-
|
|
8
|
-
export function parseFile(content, format) {
|
|
9
|
-
try {
|
|
10
|
-
if (format === 'json') {
|
|
11
|
-
return JSON.parse(content);
|
|
12
|
-
}
|
|
13
|
-
return yaml.parse(content);
|
|
14
|
-
} catch (error) {
|
|
15
|
-
throw new Error(`Failed to parse ${format} file: ${error.message}`);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function extractLocaleFromPath(filePath, localeRegex, knownLocales = []) {
|
|
20
|
-
if (knownLocales && knownLocales.length > 0) {
|
|
21
|
-
const basename = path.basename(filePath, path.extname(filePath));
|
|
22
|
-
const foundLocaleInFilename = knownLocales.find(locale =>
|
|
23
|
-
locale && basename.toLowerCase() === locale.toLowerCase()
|
|
24
|
-
);
|
|
25
|
-
if (foundLocaleInFilename) {
|
|
26
|
-
return foundLocaleInFilename.toLowerCase();
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
// Then try to match in the path
|
|
30
|
-
const pathParts = filePath.toLowerCase().split(path.sep);
|
|
31
|
-
const foundLocaleInPath = knownLocales.find(locale =>
|
|
32
|
-
locale && pathParts.includes(locale.toLowerCase())
|
|
33
|
-
);
|
|
34
|
-
if (foundLocaleInPath) {
|
|
35
|
-
return foundLocaleInPath.toLowerCase();
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const dirName = path.basename(path.dirname(filePath));
|
|
40
|
-
if (dirName && isValidLocale(dirName)) {
|
|
41
|
-
return dirName.toLowerCase();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
if (localeRegex) {
|
|
45
|
-
const filename = path.basename(filePath);
|
|
46
|
-
const regexPattern = new RegExp(localeRegex);
|
|
47
|
-
const regexMatch = filename.match(regexPattern);
|
|
48
|
-
if (regexMatch && regexMatch[1]) {
|
|
49
|
-
const locale = regexMatch[1].toLowerCase();
|
|
50
|
-
if (isValidLocale(locale)) {
|
|
51
|
-
return locale;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
throw new Error(`Could not extract locale from path: ${filePath}`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function isValidLocale(locale) {
|
|
60
|
-
// Basic validation for language code (2 letters) or language-region code (e.g., en-US)
|
|
61
|
-
return /^[a-z]{2}(?:-[A-Z]{2})?$/.test(locale);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export function flattenTranslations(obj, parentKey = '') {
|
|
65
|
-
const result = {};
|
|
66
|
-
|
|
67
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
68
|
-
const newKey = parentKey ? `${parentKey}.${key}` : key;
|
|
69
|
-
|
|
70
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
71
|
-
Object.assign(result, flattenTranslations(value, newKey));
|
|
72
|
-
} else {
|
|
73
|
-
result[newKey] = value;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
return result;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function detectJsonFormat(obj) {
|
|
81
|
-
let hasNested = false;
|
|
82
|
-
let hasDotNotation = false;
|
|
83
|
-
|
|
84
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
85
|
-
if (key.includes('.')) {
|
|
86
|
-
hasDotNotation = true;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
90
|
-
hasNested = true;
|
|
91
|
-
|
|
92
|
-
for (const [, nestedValue] of Object.entries(value)) {
|
|
93
|
-
if (nestedValue && typeof nestedValue === 'object' && !Array.isArray(nestedValue)) {
|
|
94
|
-
return 'nested';
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (hasNested && hasDotNotation) {
|
|
101
|
-
return 'mixed';
|
|
102
|
-
} else if (hasNested) {
|
|
103
|
-
return 'nested';
|
|
104
|
-
} else if (hasDotNotation) {
|
|
105
|
-
return 'flat';
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
return 'flat';
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function unflattenTranslations(flatObj) {
|
|
112
|
-
const result = {};
|
|
113
|
-
|
|
114
|
-
for (const [key, value] of Object.entries(flatObj)) {
|
|
115
|
-
const keys = key.split('.');
|
|
116
|
-
let current = result;
|
|
117
|
-
|
|
118
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
119
|
-
const k = keys[i];
|
|
120
|
-
current[k] = current[k] || {};
|
|
121
|
-
current = current[k];
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
current[keys[keys.length - 1]] = value;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
return result;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function preserveJsonStructure(originalObj, newTranslations, format) {
|
|
131
|
-
if (format === 'flat') {
|
|
132
|
-
return { ...originalObj, ...newTranslations };
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
if (format === 'nested') {
|
|
136
|
-
const merged = { ...originalObj };
|
|
137
|
-
const unflattenedNew = unflattenTranslations(newTranslations);
|
|
138
|
-
return deepMerge(merged, unflattenedNew);
|
|
139
|
-
}
|
|
140
|
-
const result = { ...originalObj };
|
|
141
|
-
|
|
142
|
-
for (const [key, value] of Object.entries(newTranslations)) {
|
|
143
|
-
if (key.includes('.')) {
|
|
144
|
-
const keys = key.split('.');
|
|
145
|
-
if (originalObj[key] !== undefined) {
|
|
146
|
-
result[key] = value;
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
let current = result;
|
|
151
|
-
|
|
152
|
-
for (let i = 0; i < keys.length - 1; i++) {
|
|
153
|
-
const k = keys[i];
|
|
154
|
-
current[k] = current[k] || {};
|
|
155
|
-
if (typeof current[k] !== 'object' || Array.isArray(current[k])) {
|
|
156
|
-
current[k] = {};
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
current = current[k];
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
current[keys[keys.length - 1]] = value;
|
|
163
|
-
} else {
|
|
164
|
-
result[key] = value;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
return result;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function deepMerge(target, source) {
|
|
172
|
-
const result = { ...target };
|
|
173
|
-
|
|
174
|
-
for (const [key, value] of Object.entries(source)) {
|
|
175
|
-
if (value && typeof value === 'object' &&
|
|
176
|
-
result[key] && typeof result[key] === 'object' &&
|
|
177
|
-
!Array.isArray(value) && !Array.isArray(result[key])) {
|
|
178
|
-
result[key] = deepMerge(result[key], value);
|
|
179
|
-
} else {
|
|
180
|
-
result[key] = value;
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
return result;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function extractNamespace(filePath) {
|
|
188
|
-
const fileName = path.basename(filePath, path.extname(filePath));
|
|
189
|
-
const dirName = path.basename(path.dirname(filePath));
|
|
190
|
-
|
|
191
|
-
// Pattern 1: /path/to/en/common.json -> namespace = common
|
|
192
|
-
if (/^[a-z]{2}(-[A-Z]{2})?$/.test(dirName)) {
|
|
193
|
-
return fileName;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// Pattern 2: /path/to/messages.en.json -> namespace = messages
|
|
197
|
-
const dotMatch = fileName.match(/^(.+)\.([a-z]{2}(?:-[A-Z]{2})?)$/);
|
|
198
|
-
if (dotMatch) {
|
|
199
|
-
return dotMatch[1];
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// Pattern 3: /path/to/common-en.json -> namespace = common
|
|
203
|
-
const dashMatch = fileName.match(/^(.+)-([a-z]{2}(?:-[A-Z]{2})?)$/);
|
|
204
|
-
if (dashMatch) {
|
|
205
|
-
return dashMatch[1];
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
return '';
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
export async function findTranslationFiles(config, options = {}) {
|
|
212
|
-
const {
|
|
213
|
-
parseContent = true,
|
|
214
|
-
includeContent = true,
|
|
215
|
-
extractKeys = true,
|
|
216
|
-
basePath = process.cwd(),
|
|
217
|
-
sourceLocale = config.sourceLocale,
|
|
218
|
-
targetLocales = config.outputLocales || [],
|
|
219
|
-
includeNamespace = false,
|
|
220
|
-
verbose = false,
|
|
221
|
-
returnFullResult = false
|
|
222
|
-
} = options;
|
|
223
|
-
const knownLocales = [sourceLocale, ...targetLocales];
|
|
224
|
-
const { translationFiles } = config;
|
|
225
|
-
const {
|
|
226
|
-
paths = [],
|
|
227
|
-
pattern = '**/*.{json,yml,yaml}',
|
|
228
|
-
ignore = [],
|
|
229
|
-
localeRegex = '.*?([a-z]{2}(?:-[A-Z]{2})?)\\.(?:yml|yaml|json)$'
|
|
230
|
-
} = translationFiles || {};
|
|
231
|
-
|
|
232
|
-
const processedFiles = [];
|
|
233
|
-
|
|
234
|
-
for (const translationPath of paths) {
|
|
235
|
-
const fullPath = path.join(basePath, translationPath);
|
|
236
|
-
const globPattern = path.join(fullPath, pattern);
|
|
237
|
-
|
|
238
|
-
if (verbose) {
|
|
239
|
-
console.log(chalk.blue(`Searching for translation files in ${globPattern}`));
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
let files;
|
|
243
|
-
try {
|
|
244
|
-
files = await glob(globPattern, {
|
|
245
|
-
ignore: ignore.map(i => path.join(basePath, i)),
|
|
246
|
-
absolute: false
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
if (verbose) {
|
|
250
|
-
console.log(chalk.blue(`Found ${files.length} files in ${translationPath}`));
|
|
251
|
-
}
|
|
252
|
-
} catch (error) {
|
|
253
|
-
if (verbose) {
|
|
254
|
-
console.error(chalk.red(`Error searching for files in ${translationPath}: ${error.message}`));
|
|
255
|
-
}
|
|
256
|
-
files = [];
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
for (const file of files) {
|
|
260
|
-
try {
|
|
261
|
-
const filePath = file;
|
|
262
|
-
const format = path.extname(file).slice(1);
|
|
263
|
-
const locale = extractLocaleFromPath(file, localeRegex, knownLocales);
|
|
264
|
-
|
|
265
|
-
const result = {
|
|
266
|
-
path: filePath,
|
|
267
|
-
format,
|
|
268
|
-
locale
|
|
269
|
-
};
|
|
270
|
-
|
|
271
|
-
if (parseContent) {
|
|
272
|
-
const content = await readFile(filePath, 'utf8');
|
|
273
|
-
const parsedContent = parseFile(content, format);
|
|
274
|
-
|
|
275
|
-
if (includeContent) {
|
|
276
|
-
result.content = Buffer.from(content).toString('base64');
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (extractKeys) {
|
|
280
|
-
const hasLanguageWrapper = parsedContent[locale] !== undefined;
|
|
281
|
-
result.hasLanguageWrapper = hasLanguageWrapper;
|
|
282
|
-
const translationData = hasLanguageWrapper ? parsedContent[locale] : parsedContent;
|
|
283
|
-
result.translations = translationData;
|
|
284
|
-
result.keys = flattenTranslations(translationData);
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
if (includeNamespace) {
|
|
289
|
-
result.namespace = extractNamespace(filePath);
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
processedFiles.push(result);
|
|
293
|
-
} catch (error) {
|
|
294
|
-
if (verbose) {
|
|
295
|
-
console.warn(chalk.yellow(`Warning: ${error.message}`));
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (!returnFullResult) {
|
|
302
|
-
return processedFiles;
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
const allFiles = processedFiles;
|
|
306
|
-
const sourceFiles = allFiles.filter(file => file.locale === sourceLocale);
|
|
307
|
-
const targetFilesByLocale = {};
|
|
308
|
-
|
|
309
|
-
for (const locale of targetLocales) {
|
|
310
|
-
targetFilesByLocale[locale] = allFiles.filter(file => file.locale === locale);
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
return {
|
|
314
|
-
allFiles,
|
|
315
|
-
sourceFiles,
|
|
316
|
-
targetFilesByLocale
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
export {
|
|
321
|
-
unflattenTranslations,
|
|
322
|
-
detectJsonFormat,
|
|
323
|
-
preserveJsonStructure,
|
|
324
|
-
directoryExists,
|
|
325
|
-
findFirstExistingPath,
|
|
326
|
-
getDirectoryContents
|
|
327
|
-
};
|
|
328
|
-
|
|
329
|
-
async function directoryExists(path, fsModule = fs) {
|
|
330
|
-
try {
|
|
331
|
-
const stats = await fsModule.stat(path);
|
|
332
|
-
return stats.isDirectory();
|
|
333
|
-
} catch (error) {
|
|
334
|
-
if (error.code === 'ENOENT') {
|
|
335
|
-
return false;
|
|
336
|
-
}
|
|
337
|
-
throw error;
|
|
338
|
-
}
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
async function findFirstExistingPath(paths, fsModule = fs) {
|
|
342
|
-
for (const path of paths) {
|
|
343
|
-
if (await directoryExists(path, fsModule)) {
|
|
344
|
-
return path;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
return null;
|
|
348
|
-
}
|
|
349
|
-
|
|
350
|
-
async function getDirectoryContents(dir, fsModule = fs) {
|
|
351
|
-
try {
|
|
352
|
-
const files = await fsModule.readdir(dir);
|
|
353
|
-
return {
|
|
354
|
-
files,
|
|
355
|
-
jsonFiles: files.filter(f => f.endsWith('.json')),
|
|
356
|
-
yamlFiles: files.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
357
|
-
};
|
|
358
|
-
} catch {
|
|
359
|
-
return null;
|
|
360
|
-
}
|
|
361
|
-
}
|
package/src/utils/git.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { promisify } from 'util';
|
|
4
|
-
import { execFile } from 'child_process';
|
|
5
|
-
|
|
6
|
-
const execFileAsync = promisify(execFile);
|
|
7
|
-
|
|
8
|
-
const defaultDeps = {
|
|
9
|
-
fs,
|
|
10
|
-
path,
|
|
11
|
-
exec: execFileAsync,
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
export const gitService = {
|
|
15
|
-
deps: { ...defaultDeps },
|
|
16
|
-
|
|
17
|
-
setDependencies(customDeps = {}) {
|
|
18
|
-
this.deps = { ...defaultDeps, ...customDeps };
|
|
19
|
-
return this;
|
|
20
|
-
},
|
|
21
|
-
|
|
22
|
-
async updateGitignore(basePath) {
|
|
23
|
-
const { fs, path } = this.deps;
|
|
24
|
-
const gitignorePath = path.join(basePath, '.gitignore');
|
|
25
|
-
let content = '';
|
|
26
|
-
|
|
27
|
-
try {
|
|
28
|
-
content = await fs.readFile(gitignorePath, 'utf8');
|
|
29
|
-
} catch (error) {
|
|
30
|
-
if (error.code !== 'ENOENT') {
|
|
31
|
-
return false;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
if (content.includes('.localhero_key')) {
|
|
36
|
-
return false;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
try {
|
|
40
|
-
await fs.appendFile(gitignorePath, '\n.localhero_key\n');
|
|
41
|
-
return true;
|
|
42
|
-
} catch (error) {
|
|
43
|
-
if (error.code === 'ENOENT') {
|
|
44
|
-
try {
|
|
45
|
-
await fs.writeFile(gitignorePath, '.localhero_key\n');
|
|
46
|
-
return true;
|
|
47
|
-
} catch {
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
},
|
|
54
|
-
|
|
55
|
-
async getCurrentBranch() {
|
|
56
|
-
try {
|
|
57
|
-
const { exec } = this.deps;
|
|
58
|
-
const { stdout } = await exec('git', ['rev-parse', '--abbrev-ref', 'HEAD']);
|
|
59
|
-
return stdout.trim();
|
|
60
|
-
} catch {
|
|
61
|
-
return null;
|
|
62
|
-
}
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
export async function updateGitignore(basePath) {
|
|
67
|
-
return gitService.updateGitignore(basePath);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export async function getCurrentBranch() {
|
|
71
|
-
return gitService.getCurrentBranch();
|
|
72
|
-
}
|
package/src/utils/github.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
import { execSync } from 'child_process';
|
|
2
|
-
import { promises as fs } from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
const defaultDependencies = {
|
|
6
|
-
exec: (cmd, options) => execSync(cmd, options),
|
|
7
|
-
fs,
|
|
8
|
-
path,
|
|
9
|
-
env: process.env
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const githubService = {
|
|
13
|
-
deps: { ...defaultDependencies },
|
|
14
|
-
|
|
15
|
-
// For testing - reset or inject custom dependencies
|
|
16
|
-
setDependencies(customDeps = {}) {
|
|
17
|
-
this.deps = { ...defaultDependencies, ...customDeps };
|
|
18
|
-
return this;
|
|
19
|
-
},
|
|
20
|
-
|
|
21
|
-
isGitHubAction() {
|
|
22
|
-
return this.deps.env.GITHUB_ACTIONS === 'true';
|
|
23
|
-
},
|
|
24
|
-
|
|
25
|
-
async createGitHubActionFile(basePath, translationPaths) {
|
|
26
|
-
const { fs, path } = this.deps;
|
|
27
|
-
const workflowDir = path.join(basePath, '.github', 'workflows');
|
|
28
|
-
const workflowFile = path.join(workflowDir, 'localhero-translate.yml');
|
|
29
|
-
|
|
30
|
-
await fs.mkdir(workflowDir, { recursive: true });
|
|
31
|
-
|
|
32
|
-
const actionContent = `name: Localhero.ai - I18n translation
|
|
33
|
-
|
|
34
|
-
on:
|
|
35
|
-
pull_request:
|
|
36
|
-
paths:
|
|
37
|
-
${translationPaths.map(p => `- "${p}"`).join('\n ')}
|
|
38
|
-
|
|
39
|
-
jobs:
|
|
40
|
-
translate:
|
|
41
|
-
runs-on: ubuntu-latest
|
|
42
|
-
permissions:
|
|
43
|
-
contents: write
|
|
44
|
-
pull-requests: write
|
|
45
|
-
|
|
46
|
-
steps:
|
|
47
|
-
- name: Checkout code
|
|
48
|
-
uses: actions/checkout@v4
|
|
49
|
-
with:
|
|
50
|
-
ref: \${{ github.head_ref }}
|
|
51
|
-
fetch-depth: 0
|
|
52
|
-
|
|
53
|
-
- name: Set up Node.js
|
|
54
|
-
uses: actions/setup-node@v4
|
|
55
|
-
with:
|
|
56
|
-
node-version: 18
|
|
57
|
-
|
|
58
|
-
- name: Run LocalHero CLI
|
|
59
|
-
env:
|
|
60
|
-
LOCALHERO_API_KEY: \${{ secrets.LOCALHERO_API_KEY }}
|
|
61
|
-
GITHUB_TOKEN: \${{ secrets.GITHUB_TOKEN }}
|
|
62
|
-
run: npx -y @localheroai/cli translate`;
|
|
63
|
-
|
|
64
|
-
await fs.writeFile(workflowFile, actionContent);
|
|
65
|
-
return workflowFile;
|
|
66
|
-
},
|
|
67
|
-
|
|
68
|
-
autoCommitChanges(filesPath) {
|
|
69
|
-
const { exec, env } = this.deps;
|
|
70
|
-
|
|
71
|
-
if (!this.isGitHubAction()) return;
|
|
72
|
-
|
|
73
|
-
console.log("Running in GitHub Actions. Committing changes...");
|
|
74
|
-
try {
|
|
75
|
-
exec('git config --global user.name "LocalHero Bot"', { stdio: "inherit" });
|
|
76
|
-
exec('git config --global user.email "hi@localhero.ai"', { stdio: "inherit" });
|
|
77
|
-
|
|
78
|
-
const branchName = env.GITHUB_HEAD_REF;
|
|
79
|
-
if (!branchName) {
|
|
80
|
-
throw new Error('Could not determine branch name from GITHUB_HEAD_REF');
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
exec(`git add ${filesPath}`, { stdio: "inherit" });
|
|
84
|
-
|
|
85
|
-
const status = exec('git status --porcelain').toString();
|
|
86
|
-
if (!status) {
|
|
87
|
-
console.log("No changes to commit.");
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
exec('git commit -m "Update translations"', { stdio: "inherit" });
|
|
92
|
-
|
|
93
|
-
const token = env.GITHUB_TOKEN;
|
|
94
|
-
if (!token) {
|
|
95
|
-
throw new Error('GITHUB_TOKEN is not set');
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const repository = env.GITHUB_REPOSITORY;
|
|
99
|
-
if (!repository) {
|
|
100
|
-
throw new Error('GITHUB_REPOSITORY is not set');
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const remoteUrl = `https://x-access-token:${token}@github.com/${repository}.git`;
|
|
104
|
-
|
|
105
|
-
exec(`git remote set-url origin ${remoteUrl}`, { stdio: "inherit" });
|
|
106
|
-
exec(`git push origin HEAD:${branchName}`, { stdio: "inherit" });
|
|
107
|
-
console.log("Changes committed and pushed successfully.");
|
|
108
|
-
} catch (error) {
|
|
109
|
-
console.error("Auto-commit failed:", error.message);
|
|
110
|
-
throw error;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
};
|
|
114
|
-
|
|
115
|
-
// Only export the functions needed externally
|
|
116
|
-
export function createGitHubActionFile(basePath, translationPaths) {
|
|
117
|
-
return githubService.createGitHubActionFile(basePath, translationPaths);
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function autoCommitChanges(filesPath) {
|
|
121
|
-
return githubService.autoCommitChanges(filesPath);
|
|
122
|
-
}
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { promises as fs } from 'fs';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { createImport, checkImportStatus } from '../api/imports.js';
|
|
4
|
-
import { findTranslationFiles, flattenTranslations } from './files.js';
|
|
5
|
-
|
|
6
|
-
function getFileFormat(filePath) {
|
|
7
|
-
const ext = path.extname(filePath).toLowerCase();
|
|
8
|
-
if (ext === '.json') return 'json';
|
|
9
|
-
if (ext === '.yml' || ext === '.yaml') return 'yaml';
|
|
10
|
-
return null;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
async function readFileContent(filePath) {
|
|
14
|
-
const content = await fs.readFile(filePath, 'utf8');
|
|
15
|
-
const format = getFileFormat(filePath);
|
|
16
|
-
|
|
17
|
-
if (format === 'json') {
|
|
18
|
-
try {
|
|
19
|
-
const jsonContent = JSON.parse(content);
|
|
20
|
-
const flattened = flattenTranslations(jsonContent);
|
|
21
|
-
|
|
22
|
-
return Buffer.from(JSON.stringify(flattened)).toString('base64');
|
|
23
|
-
} catch {
|
|
24
|
-
return Buffer.from(content).toString('base64');
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return Buffer.from(content).toString('base64');
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export const importService = {
|
|
32
|
-
async findTranslationFiles(config, basePath = process.cwd()) {
|
|
33
|
-
const files = await findTranslationFiles(config, {
|
|
34
|
-
basePath,
|
|
35
|
-
parseContent: false,
|
|
36
|
-
includeContent: false,
|
|
37
|
-
extractKeys: false,
|
|
38
|
-
includeNamespace: true
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
return files.map(file => ({
|
|
42
|
-
path: path.isAbsolute(file.path) ? path.relative(basePath, file.path) : file.path,
|
|
43
|
-
language: file.locale,
|
|
44
|
-
format: file.format === 'yml' ? 'yaml' : file.format,
|
|
45
|
-
namespace: file.namespace || ''
|
|
46
|
-
}));
|
|
47
|
-
},
|
|
48
|
-
|
|
49
|
-
async importTranslations(config, basePath = process.cwd()) {
|
|
50
|
-
const files = await this.findTranslationFiles(config, basePath);
|
|
51
|
-
|
|
52
|
-
if (!files.length) {
|
|
53
|
-
return { status: 'no_files' };
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const sourceFiles = files.filter(file => file.language === config.sourceLocale);
|
|
57
|
-
const targetFiles = files.filter(file => file.language !== config.sourceLocale);
|
|
58
|
-
const importedFiles = {
|
|
59
|
-
source: sourceFiles,
|
|
60
|
-
target: targetFiles
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
if (!sourceFiles.length) {
|
|
64
|
-
return {
|
|
65
|
-
status: 'failed',
|
|
66
|
-
error: 'No source language files found. Source language files must be included in the first import.',
|
|
67
|
-
files: importedFiles
|
|
68
|
-
};
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const allTranslations = [];
|
|
72
|
-
|
|
73
|
-
for (const file of sourceFiles) {
|
|
74
|
-
const fullPath = path.join(basePath, file.path);
|
|
75
|
-
allTranslations.push({
|
|
76
|
-
language: file.language,
|
|
77
|
-
format: file.format === 'yml' ? 'yaml' : file.format,
|
|
78
|
-
filename: file.path,
|
|
79
|
-
content: await readFileContent(fullPath)
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
for (const file of targetFiles) {
|
|
84
|
-
const fullPath = path.join(basePath, file.path);
|
|
85
|
-
allTranslations.push({
|
|
86
|
-
language: file.language,
|
|
87
|
-
format: file.format === 'yml' ? 'yaml' : file.format,
|
|
88
|
-
filename: file.path,
|
|
89
|
-
content: await readFileContent(fullPath)
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const importResult = await createImport({
|
|
94
|
-
projectId: config.projectId,
|
|
95
|
-
translations: allTranslations
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
if (importResult.status === 'failed') {
|
|
99
|
-
return {
|
|
100
|
-
...importResult,
|
|
101
|
-
files: importedFiles
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
let finalImportResult = importResult;
|
|
106
|
-
while (finalImportResult.status === 'processing') {
|
|
107
|
-
await new Promise(resolve => setTimeout(resolve, finalImportResult.poll_interval * 1000));
|
|
108
|
-
finalImportResult = await checkImportStatus(config.projectId, finalImportResult.id);
|
|
109
|
-
|
|
110
|
-
if (finalImportResult.status === 'failed') {
|
|
111
|
-
return {
|
|
112
|
-
...finalImportResult,
|
|
113
|
-
files: importedFiles
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Ensure we pass through all relevant fields from the API response
|
|
119
|
-
const { status, statistics, warnings, translations_url, sourceImport } = finalImportResult;
|
|
120
|
-
return {
|
|
121
|
-
status,
|
|
122
|
-
statistics,
|
|
123
|
-
warnings,
|
|
124
|
-
translations_url,
|
|
125
|
-
sourceImport,
|
|
126
|
-
files: importedFiles
|
|
127
|
-
};
|
|
128
|
-
}
|
|
129
|
-
};
|