@localheroai/cli 0.0.1 → 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 +58 -58
- package/src/api/auth.js +20 -11
- package/src/api/client.js +71 -30
- 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 +80 -23
- 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/src/commands/init.js
CHANGED
|
@@ -2,284 +2,484 @@ import { promises as fs } from 'fs';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import { createPromptService } from '../utils/prompt-service.js';
|
|
5
|
-
import {
|
|
5
|
+
import { createProject, listProjects } from '../api/projects.js';
|
|
6
6
|
import { configService } from '../utils/config.js';
|
|
7
7
|
import { checkAuth } from '../utils/auth.js';
|
|
8
8
|
import { login } from './login.js';
|
|
9
9
|
import { importService } from '../utils/import-service.js';
|
|
10
10
|
import { createGitHubActionFile } from '../utils/github.js';
|
|
11
|
+
import { directoryExists, findFirstExistingPath, getDirectoryContents } from '../utils/files.js';
|
|
11
12
|
|
|
12
13
|
const PROJECT_TYPES = {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}
|
|
14
|
+
rails: {
|
|
15
|
+
directIndicators: ['config/application.rb', 'Gemfile'],
|
|
16
|
+
defaults: {
|
|
17
|
+
translationPath: 'config/locales/',
|
|
18
|
+
filePattern: '**/*.{yml,yaml}'
|
|
19
19
|
},
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
20
|
+
commonPaths: [
|
|
21
|
+
'config/locales'
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
nextjs: {
|
|
25
|
+
directIndicators: ['next.config.js', 'next.config.mjs'],
|
|
26
|
+
packageCheck: {
|
|
27
|
+
requires: ['next'],
|
|
28
|
+
oneOf: ['next-i18next', 'next-translate']
|
|
26
29
|
},
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
30
|
+
defaults: {
|
|
31
|
+
translationPath: 'public/locales/',
|
|
32
|
+
filePattern: '**/*.json'
|
|
33
|
+
},
|
|
34
|
+
commonPaths: [
|
|
35
|
+
'public/locales',
|
|
36
|
+
'src/locales',
|
|
37
|
+
'locales'
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
vueI18n: {
|
|
41
|
+
directIndicators: ['vue.config.js'],
|
|
42
|
+
packageCheck: {
|
|
43
|
+
oneOf: ['vue-i18n', '@nuxtjs/i18n']
|
|
44
|
+
},
|
|
45
|
+
defaults: {
|
|
46
|
+
translationPath: 'src/locales/',
|
|
47
|
+
filePattern: '**/*.json'
|
|
48
|
+
},
|
|
49
|
+
commonPaths: [
|
|
50
|
+
'src/locales',
|
|
51
|
+
'src/i18n',
|
|
52
|
+
'locales',
|
|
53
|
+
'i18n'
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
i18next: {
|
|
57
|
+
directIndicators: ['i18next.config.js', 'i18n.js', 'i18n/index.js'],
|
|
58
|
+
packageCheck: {
|
|
59
|
+
requires: ['i18next']
|
|
60
|
+
},
|
|
61
|
+
defaults: {
|
|
62
|
+
translationPath: 'public/locales/',
|
|
63
|
+
filePattern: '**/*.json'
|
|
64
|
+
},
|
|
65
|
+
commonPaths: [
|
|
66
|
+
'public/locales',
|
|
67
|
+
'src/locales',
|
|
68
|
+
'locales',
|
|
69
|
+
'src/i18n',
|
|
70
|
+
'i18n'
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
reactIntl: {
|
|
74
|
+
directIndicators: ['.babelrc'],
|
|
75
|
+
packageCheck: {
|
|
76
|
+
requires: ['react-intl']
|
|
77
|
+
},
|
|
78
|
+
defaults: {
|
|
79
|
+
translationPath: 'src/translations/',
|
|
80
|
+
filePattern: '**/*.json'
|
|
81
|
+
},
|
|
82
|
+
commonPaths: [
|
|
83
|
+
'src/i18n',
|
|
84
|
+
'src/translations',
|
|
85
|
+
'src/lang',
|
|
86
|
+
'src/locales',
|
|
87
|
+
'translations',
|
|
88
|
+
'locales'
|
|
89
|
+
]
|
|
90
|
+
},
|
|
91
|
+
gatsbyReact: {
|
|
92
|
+
directIndicators: ['gatsby-config.js'],
|
|
93
|
+
packageCheck: {
|
|
94
|
+
requires: ['gatsby'],
|
|
95
|
+
oneOf: ['gatsby-plugin-intl', 'gatsby-plugin-i18n']
|
|
96
|
+
},
|
|
97
|
+
defaults: {
|
|
98
|
+
translationPath: 'src/data/i18n/',
|
|
99
|
+
filePattern: '**/*.json'
|
|
100
|
+
},
|
|
101
|
+
commonPaths: [
|
|
102
|
+
'src/data/i18n',
|
|
103
|
+
'src/i18n',
|
|
104
|
+
'src/locales',
|
|
105
|
+
'locales'
|
|
106
|
+
]
|
|
107
|
+
},
|
|
108
|
+
react: {
|
|
109
|
+
directIndicators: ['src/App.js', 'src/App.jsx', 'src/index.js', 'src/index.jsx'],
|
|
110
|
+
packageCheck: {
|
|
111
|
+
requires: ['react']
|
|
112
|
+
},
|
|
113
|
+
defaults: {
|
|
114
|
+
translationPath: 'src/locales/',
|
|
115
|
+
filePattern: '**/*.{json,yml}'
|
|
116
|
+
},
|
|
117
|
+
commonPaths: [
|
|
118
|
+
'src/locales',
|
|
119
|
+
'public/locales',
|
|
120
|
+
'src/i18n',
|
|
121
|
+
'src/translations',
|
|
122
|
+
'src/lang',
|
|
123
|
+
'assets/i18n',
|
|
124
|
+
'locales'
|
|
125
|
+
]
|
|
126
|
+
},
|
|
127
|
+
generic: {
|
|
128
|
+
directIndicators: [],
|
|
129
|
+
defaults: {
|
|
130
|
+
translationPath: 'locales/',
|
|
131
|
+
filePattern: '**/*.{json,yml,yaml}'
|
|
132
|
+
},
|
|
133
|
+
commonPaths: [
|
|
134
|
+
'locales',
|
|
135
|
+
'src/locales',
|
|
136
|
+
'public/locales',
|
|
137
|
+
'src/i18n',
|
|
138
|
+
'src/translations',
|
|
139
|
+
'src/lang',
|
|
140
|
+
'assets/i18n',
|
|
141
|
+
'i18n',
|
|
142
|
+
'translations',
|
|
143
|
+
'lang'
|
|
144
|
+
]
|
|
145
|
+
}
|
|
34
146
|
};
|
|
35
147
|
|
|
36
|
-
async function
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
continue;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
return {
|
|
48
|
-
type: 'generic',
|
|
49
|
-
defaults: PROJECT_TYPES.generic.defaults
|
|
50
|
-
};
|
|
148
|
+
async function checkPackageJson() {
|
|
149
|
+
try {
|
|
150
|
+
const content = await fs.readFile('package.json', 'utf8');
|
|
151
|
+
const pkg = JSON.parse(content);
|
|
152
|
+
return { ...pkg.dependencies, ...pkg.devDependencies };
|
|
153
|
+
} catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
51
156
|
}
|
|
52
157
|
|
|
53
|
-
async function
|
|
158
|
+
async function detectFramework(config) {
|
|
159
|
+
for (const indicator of config.directIndicators || []) {
|
|
54
160
|
try {
|
|
55
|
-
|
|
56
|
-
|
|
161
|
+
const stats = await fs.stat(indicator);
|
|
162
|
+
if (stats.isFile()) return true;
|
|
57
163
|
} catch {
|
|
58
|
-
|
|
164
|
+
continue;
|
|
59
165
|
}
|
|
60
|
-
}
|
|
166
|
+
}
|
|
61
167
|
|
|
62
|
-
|
|
63
|
-
const
|
|
168
|
+
if (config.packageCheck) {
|
|
169
|
+
const deps = await checkPackageJson();
|
|
170
|
+
if (deps) {
|
|
171
|
+
const { requires = [], oneOf = [] } = config.packageCheck;
|
|
64
172
|
|
|
65
|
-
|
|
66
|
-
return
|
|
67
|
-
|
|
173
|
+
if (requires.length && !requires.every(pkg => deps[pkg])) {
|
|
174
|
+
return false;
|
|
175
|
+
}
|
|
68
176
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
...projects.map(p => ({
|
|
73
|
-
name: p.name,
|
|
74
|
-
value: p.id
|
|
75
|
-
}))
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
const projectChoice = await promptService.select({
|
|
79
|
-
message: 'Would you like to use an existing project or create a new one?',
|
|
80
|
-
choices
|
|
81
|
-
});
|
|
177
|
+
if (oneOf.length && !oneOf.some(pkg => deps[pkg])) {
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
82
180
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
};
|
|
87
|
-
}
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
88
184
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
let projectId = projectChoice;
|
|
92
|
-
let newProject = false;
|
|
93
|
-
let config = await promptService.getProjectSetup();
|
|
94
|
-
|
|
95
|
-
if (!existingProject) {
|
|
96
|
-
config = {
|
|
97
|
-
projectName: await promptService.input({
|
|
98
|
-
message: 'Project name:',
|
|
99
|
-
default: path.basename(process.cwd()),
|
|
100
|
-
}),
|
|
101
|
-
sourceLocale: await promptService.input({
|
|
102
|
-
message: 'Source language code:',
|
|
103
|
-
default: 'en'
|
|
104
|
-
}),
|
|
105
|
-
outputLocales: (await promptService.input({
|
|
106
|
-
message: 'Target languages (comma-separated):',
|
|
107
|
-
})).split(',').map(lang => lang.trim()).filter(Boolean)
|
|
108
|
-
};
|
|
185
|
+
return false;
|
|
186
|
+
}
|
|
109
187
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
188
|
+
async function detectProjectType() {
|
|
189
|
+
for (const [type, config] of Object.entries(PROJECT_TYPES)) {
|
|
190
|
+
if (!config.directIndicators?.length && !config.packageCheck) continue;
|
|
191
|
+
|
|
192
|
+
const isFramework = await detectFramework(config);
|
|
193
|
+
if (!isFramework) continue;
|
|
194
|
+
|
|
195
|
+
if (config.commonPaths) {
|
|
196
|
+
const translationPath = await findFirstExistingPath(config.commonPaths);
|
|
197
|
+
if (translationPath) {
|
|
198
|
+
return {
|
|
199
|
+
type,
|
|
200
|
+
defaults: {
|
|
201
|
+
...config.defaults,
|
|
202
|
+
translationPath: `${translationPath}/`
|
|
203
|
+
}
|
|
121
204
|
};
|
|
205
|
+
}
|
|
122
206
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
207
|
+
return { type, defaults: config.defaults };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const translationPath = await findFirstExistingPath(PROJECT_TYPES.generic.commonPaths);
|
|
211
|
+
if (translationPath) {
|
|
212
|
+
const contents = await getDirectoryContents(translationPath);
|
|
213
|
+
if (contents) {
|
|
214
|
+
return {
|
|
215
|
+
type: 'detected',
|
|
216
|
+
defaults: {
|
|
217
|
+
translationPath: `${translationPath}/`,
|
|
218
|
+
filePattern: contents.jsonFiles.length > 0 && contents.yamlFiles.length === 0
|
|
219
|
+
? '**/*.json'
|
|
220
|
+
: '**/*.{json,yml,yaml}'
|
|
221
|
+
}
|
|
222
|
+
};
|
|
135
223
|
}
|
|
224
|
+
}
|
|
136
225
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
ignorePaths: ignorePaths.split(',').map(p => p.trim()).filter(Boolean)
|
|
142
|
-
};
|
|
226
|
+
return {
|
|
227
|
+
type: 'generic',
|
|
228
|
+
defaults: PROJECT_TYPES.generic.defaults
|
|
229
|
+
};
|
|
143
230
|
}
|
|
144
231
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
232
|
+
async function promptForConfig(projectDefaults, projectService, promptService, console = global.console) {
|
|
233
|
+
const { choice: projectChoice, project: existingProject } = await promptService.selectProject(projectService);
|
|
234
|
+
let projectId = projectChoice;
|
|
235
|
+
let newProject = false;
|
|
236
|
+
let config = await promptService.getProjectSetup();
|
|
237
|
+
|
|
238
|
+
if (!existingProject) {
|
|
239
|
+
config = {
|
|
240
|
+
projectName: await promptService.input({
|
|
241
|
+
message: 'Project name:',
|
|
242
|
+
default: path.basename(process.cwd()),
|
|
243
|
+
}),
|
|
244
|
+
sourceLocale: await promptService.input({
|
|
245
|
+
message: 'Source language - the language that we will translate from:',
|
|
246
|
+
default: 'en',
|
|
247
|
+
hint: 'Examples: "en" for en.json/en.yml, "en-US" for en-US.json, or directory name like "en" in /locales/en/common.json'
|
|
248
|
+
}),
|
|
249
|
+
outputLocales: (await promptService.input({
|
|
250
|
+
message: 'Target languages (comma-separated):',
|
|
251
|
+
hint: 'Must match your file names or directory names exactly. Examples: en.json → "en", fr-CA.json → "fr-CA", /locales/de/ → "de"'
|
|
252
|
+
})).split(',').map(lang => lang.trim()).filter(Boolean)
|
|
253
|
+
};
|
|
161
254
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
255
|
+
try {
|
|
256
|
+
newProject = await projectService.createProject({
|
|
257
|
+
name: config.projectName,
|
|
258
|
+
sourceLocale: config.sourceLocale,
|
|
259
|
+
targetLocales: config.outputLocales
|
|
260
|
+
});
|
|
261
|
+
projectId = newProject.id;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.log(chalk.red(`\n✗ Failed to create project: ${error.message}`));
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
} else {
|
|
267
|
+
config = {
|
|
268
|
+
projectName: existingProject.name,
|
|
269
|
+
sourceLocale: existingProject.source_language,
|
|
270
|
+
outputLocales: existingProject.target_languages
|
|
271
|
+
};
|
|
272
|
+
}
|
|
166
273
|
|
|
167
|
-
|
|
274
|
+
const commonPaths = projectDefaults.commonPaths || PROJECT_TYPES.generic.commonPaths;
|
|
275
|
+
const existingDirs = [];
|
|
168
276
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
277
|
+
for (const dir of commonPaths) {
|
|
278
|
+
if (await directoryExists(dir)) {
|
|
279
|
+
existingDirs.push(dir);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
let dirHint = `Directory containing your translation files for ${projectDefaults.type || 'your'} project`;
|
|
284
|
+
if (existingDirs.length > 0) {
|
|
285
|
+
dirHint += `. Found existing directories: ${existingDirs.map(d => `"${d}/"`).join(', ')}`;
|
|
286
|
+
} else {
|
|
287
|
+
dirHint += `. Common paths: ${commonPaths.slice(0, 3).map(d => `"${d}/"`).join(', ')}`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const translationPath = await promptService.input({
|
|
291
|
+
message: 'Translation files path:',
|
|
292
|
+
default: projectDefaults.defaults.translationPath,
|
|
293
|
+
hint: dirHint
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
let filePattern = projectDefaults.defaults.filePattern;
|
|
297
|
+
const contents = await getDirectoryContents(translationPath);
|
|
298
|
+
|
|
299
|
+
if (contents) {
|
|
300
|
+
if (contents.jsonFiles.length > 0 && contents.yamlFiles.length === 0) {
|
|
301
|
+
filePattern = '**/*.json';
|
|
302
|
+
} else if (contents.jsonFiles.length === 0 && contents.yamlFiles.length > 0) {
|
|
303
|
+
filePattern = '**/*.{yml,yaml}';
|
|
304
|
+
} else if (contents.jsonFiles.length > 0 && contents.yamlFiles.length > 0) {
|
|
305
|
+
filePattern = '**/*.{json,yml,yaml}';
|
|
175
306
|
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const ignorePaths = await promptService.input({
|
|
310
|
+
message: 'Paths to ignore (comma-separated, leave empty for none):',
|
|
311
|
+
hint: 'Example: "locales/ignored,locales/temp"'
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
...config,
|
|
316
|
+
projectId,
|
|
317
|
+
translationPath,
|
|
318
|
+
filePattern,
|
|
319
|
+
ignorePaths: ignorePaths.split(',').map(p => p.trim()).filter(Boolean),
|
|
320
|
+
newProject
|
|
321
|
+
};
|
|
322
|
+
}
|
|
176
323
|
|
|
177
|
-
|
|
178
|
-
|
|
324
|
+
export async function init(deps = {}) {
|
|
325
|
+
const {
|
|
326
|
+
console = global.console,
|
|
327
|
+
basePath = process.cwd(),
|
|
328
|
+
promptService = createPromptService({ inquirer: await import('@inquirer/prompts') }),
|
|
329
|
+
configUtils = configService,
|
|
330
|
+
authUtils = { checkAuth },
|
|
331
|
+
importUtils = importService,
|
|
332
|
+
projectApi = { createProject, listProjects },
|
|
333
|
+
login: loginFn = login
|
|
334
|
+
} = deps;
|
|
335
|
+
|
|
336
|
+
const existingConfig = await configUtils.getProjectConfig(basePath);
|
|
337
|
+
if (existingConfig) {
|
|
338
|
+
console.log(chalk.yellow('Existing configuration found in localhero.json. Skipping initialization.'));
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const isAuthenticated = await authUtils.checkAuth();
|
|
343
|
+
if (!isAuthenticated) {
|
|
344
|
+
console.log('LocalHero.ai - Automate your i18n translations\n');
|
|
345
|
+
console.log(chalk.yellow('No API key found. Let\'s get you authenticated.'));
|
|
346
|
+
|
|
347
|
+
await loginFn({
|
|
348
|
+
console,
|
|
349
|
+
basePath,
|
|
350
|
+
promptService,
|
|
351
|
+
configUtils,
|
|
352
|
+
verifyApiKey: authUtils.verifyApiKey,
|
|
353
|
+
isCalledFromInit: true
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
console.log('\nLet\'s set up configuration for your project.\n');
|
|
358
|
+
|
|
359
|
+
const projectDefaults = await detectProjectType();
|
|
360
|
+
const answers = await promptForConfig(projectDefaults, projectApi, promptService, console);
|
|
361
|
+
|
|
362
|
+
if (!answers) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const config = {
|
|
367
|
+
schemaVersion: '1.0',
|
|
368
|
+
projectId: answers.projectId,
|
|
369
|
+
sourceLocale: answers.sourceLocale,
|
|
370
|
+
outputLocales: answers.outputLocales,
|
|
371
|
+
translationFiles: {
|
|
372
|
+
paths: [answers.translationPath],
|
|
373
|
+
pattern: answers.filePattern,
|
|
374
|
+
ignore: answers.ignorePaths
|
|
375
|
+
}
|
|
376
|
+
};
|
|
179
377
|
|
|
180
|
-
|
|
181
|
-
|
|
378
|
+
await configUtils.saveProjectConfig(config, basePath);
|
|
379
|
+
console.log(chalk.green('\n✓ Created localhero.json'));
|
|
182
380
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
sourceLocale: answers.sourceLocale,
|
|
187
|
-
outputLocales: answers.outputLocales,
|
|
188
|
-
translationFiles: {
|
|
189
|
-
paths: [answers.translationPath],
|
|
190
|
-
ignore: answers.ignorePaths
|
|
191
|
-
}
|
|
192
|
-
};
|
|
381
|
+
if (answers.newProject) {
|
|
382
|
+
console.log(chalk.green(`✓ Project created, view it at: https://localhero.ai/projects/${answers.projectId}`));
|
|
383
|
+
}
|
|
193
384
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
console.log(JSON.stringify(config, null, 2));
|
|
198
|
-
console.log(' ');
|
|
385
|
+
console.log('Configuration:');
|
|
386
|
+
console.log(JSON.stringify(config, null, 2));
|
|
387
|
+
console.log(' ');
|
|
199
388
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
389
|
+
const shouldSetupGitHubAction = await promptService.confirm({
|
|
390
|
+
message: 'Would you like to set up GitHub Actions for automatic translations?',
|
|
391
|
+
default: true
|
|
392
|
+
});
|
|
204
393
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
394
|
+
if (shouldSetupGitHubAction) {
|
|
395
|
+
try {
|
|
396
|
+
const workflowFile = await createGitHubActionFile(basePath, config.translationFiles.paths);
|
|
397
|
+
console.log(chalk.green(`\n✓ Created GitHub Action workflow at ${workflowFile}`));
|
|
398
|
+
console.log('\nNext steps:');
|
|
399
|
+
console.log('1. Add your API key to your repository\'s secrets:');
|
|
400
|
+
console.log(' - Go to Settings > Secrets > Actions > New repository secret');
|
|
401
|
+
console.log(' - Name: LOCALHERO_API_KEY');
|
|
402
|
+
console.log(' - Value: [Your API Key] (find this at https://localhero.ai/api-keys or in your local .localhero_key file)');
|
|
403
|
+
console.log('\n2. Commit and push the workflow file to enable automatic translations\n');
|
|
404
|
+
} catch (error) {
|
|
405
|
+
console.log(chalk.yellow('\nFailed to create GitHub Action workflow:'), error.message);
|
|
218
406
|
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const shouldImport = await promptService.confirm({
|
|
410
|
+
message: 'Would you like to import existing translation files? (recommended)',
|
|
411
|
+
default: true
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
if (shouldImport) {
|
|
415
|
+
console.log('\nSearching for translation files...');
|
|
416
|
+
console.log(`Looking in: ${config.translationFiles.paths.join(', ')}`);
|
|
417
|
+
|
|
418
|
+
const importResult = await importUtils.importTranslations(config, basePath);
|
|
419
|
+
|
|
420
|
+
if (importResult.status === 'no_files') {
|
|
421
|
+
console.log(chalk.yellow('\nNo translation files found.'));
|
|
422
|
+
console.log('Make sure your translation files:');
|
|
423
|
+
console.log('1. Are in the specified path(s)');
|
|
424
|
+
console.log('2. Have the correct file extensions (.json, .yml, or .yaml)');
|
|
425
|
+
console.log('3. Follow the naming convention: [language-code].[extension] or are in language-specific directories');
|
|
426
|
+
console.log(`4. Include source language files (${config.sourceLocale}.[extension])`);
|
|
427
|
+
console.log('\nSupported JSON formats:');
|
|
428
|
+
console.log('- Nested format: { "navbar": { "home": "Home" } }');
|
|
429
|
+
console.log('- Flat format: { "navbar.home": "Home" }');
|
|
430
|
+
console.log('- With language wrapper: { "en": { "navbar": { "home": "Home" } } }');
|
|
431
|
+
console.log('\nSupported directory structures:');
|
|
432
|
+
console.log('- /locales/en.json, /locales/fr.json');
|
|
433
|
+
console.log('- /locales/en/common.json, /locales/fr/common.json');
|
|
434
|
+
console.log('- /locales/common.en.json, /locales/common.fr.json');
|
|
435
|
+
} else if (importResult.status === 'failed') {
|
|
436
|
+
console.log(chalk.red('\n✗ Failed to import translations'));
|
|
437
|
+
if (importResult.error) {
|
|
438
|
+
console.log(chalk.red(`Error: ${importResult.error}`));
|
|
439
|
+
}
|
|
440
|
+
return;
|
|
441
|
+
} else if (importResult.status === 'completed') {
|
|
442
|
+
console.log(chalk.green('\n✓ Successfully imported translations'));
|
|
443
|
+
await configUtils.updateLastSyncedAt();
|
|
444
|
+
|
|
445
|
+
if (importResult.files) {
|
|
446
|
+
console.log('\nImported files:');
|
|
447
|
+
[...importResult.files.source, ...importResult.files.target]
|
|
448
|
+
.sort((a, b) => a.path.localeCompare(b.path))
|
|
449
|
+
.forEach(file => {
|
|
450
|
+
const isSource = importResult.files.source.includes(file);
|
|
451
|
+
console.log(`- ${file.path}${isSource ? ' [source]' : ''}`);
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
if (importResult.sourceImport) {
|
|
456
|
+
console.log(`\nImported ${importResult.sourceImport.statistics.total_keys} source language keys`);
|
|
457
|
+
|
|
458
|
+
if (importResult.sourceImport.warnings?.length) {
|
|
459
|
+
console.log(chalk.yellow('\nWarnings:'));
|
|
460
|
+
importResult.sourceImport.warnings.forEach(warning => {
|
|
461
|
+
console.log(`- ${warning.message} (${warning.language})`);
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
}
|
|
219
465
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
466
|
+
console.log('\nTarget Languages:');
|
|
467
|
+
importResult.statistics.languages.forEach(lang => {
|
|
468
|
+
console.log(`${lang.code.toUpperCase()}: ${lang.translated}/${importResult.statistics.total_keys} translated`);
|
|
469
|
+
});
|
|
224
470
|
|
|
225
|
-
|
|
226
|
-
console.log('\
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
471
|
+
if (importResult.warnings?.length) {
|
|
472
|
+
console.log(chalk.yellow('\nWarnings:'));
|
|
473
|
+
importResult.warnings.forEach(warning => {
|
|
474
|
+
console.log(`- ${warning.message} (${warning.language})`);
|
|
475
|
+
});
|
|
476
|
+
}
|
|
231
477
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
console.log(chalk.yellow('\nNo translation files found.'));
|
|
236
|
-
console.log('Make sure your translation files:');
|
|
237
|
-
console.log('1. Are in the specified path(s)');
|
|
238
|
-
console.log('2. Have the correct file extensions (.json, .yml, or .yaml)');
|
|
239
|
-
console.log('3. Follow the naming convention: [language-code].[extension]');
|
|
240
|
-
console.log(`4. Include source language files (${config.sourceLocale}.[extension])`);
|
|
241
|
-
} else if (importResult.status === 'failed') {
|
|
242
|
-
console.log(chalk.red('\n✗ Failed to import translations'));
|
|
243
|
-
if (importResult.error) {
|
|
244
|
-
console.log(`Error: ${importResult.error}`);
|
|
245
|
-
}
|
|
246
|
-
} else if (importResult.status === 'completed') {
|
|
247
|
-
console.log(chalk.green('\n✓ Successfully imported translations'));
|
|
248
|
-
|
|
249
|
-
if (importResult.files) {
|
|
250
|
-
console.log('\nImported files:');
|
|
251
|
-
[...importResult.files.source, ...importResult.files.target]
|
|
252
|
-
.sort((a, b) => a.path.localeCompare(b.path))
|
|
253
|
-
.forEach(file => {
|
|
254
|
-
const isSource = importResult.files.source.includes(file);
|
|
255
|
-
console.log(`- ${file.path}${isSource ? ' [source]' : ''}`);
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
if (importResult.sourceImport) {
|
|
260
|
-
console.log(`\nImported ${importResult.sourceImport.statistics.total_keys} source language keys`);
|
|
261
|
-
|
|
262
|
-
if (importResult.sourceImport.warnings?.length) {
|
|
263
|
-
console.log(chalk.yellow('\nWarnings:'));
|
|
264
|
-
importResult.sourceImport.warnings.forEach(warning => {
|
|
265
|
-
console.log(`- ${warning.message} (${warning.language})`);
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
console.log('\nTarget Languages:');
|
|
271
|
-
importResult.statistics.languages.forEach(lang => {
|
|
272
|
-
console.log(`${lang.code.toUpperCase()}: ${lang.translated}/${importResult.statistics.total_keys} translated`);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
if (importResult.warnings?.length) {
|
|
276
|
-
console.log(chalk.yellow('\nWarnings:'));
|
|
277
|
-
importResult.warnings.forEach(warning => {
|
|
278
|
-
console.log(`- ${warning.message} (${warning.language})`);
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
478
|
+
if (importResult.translations_url) {
|
|
479
|
+
console.log(chalk.blue(`\nView your translations at: ${importResult.translations_url}`));
|
|
480
|
+
}
|
|
282
481
|
}
|
|
482
|
+
}
|
|
283
483
|
|
|
284
|
-
|
|
484
|
+
console.log('\n🚀 Done! Start translating with: npx @localheroai/cli translate');
|
|
285
485
|
}
|