@localheroai/cli 0.0.5 → 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.
Files changed (116) hide show
  1. package/README +1 -1
  2. package/dist/api/auth.d.ts +2 -0
  3. package/dist/api/auth.js +28 -0
  4. package/dist/api/auth.js.map +1 -0
  5. package/dist/api/client.d.ts +3 -0
  6. package/dist/api/client.js +80 -0
  7. package/dist/api/client.js.map +1 -0
  8. package/dist/api/imports.d.ts +5 -0
  9. package/dist/api/imports.js +43 -0
  10. package/dist/api/imports.js.map +1 -0
  11. package/dist/api/projects.d.ts +2 -0
  12. package/dist/api/projects.js +42 -0
  13. package/dist/api/projects.js.map +1 -0
  14. package/dist/api/translations.d.ts +15 -0
  15. package/dist/api/translations.js +71 -0
  16. package/dist/api/translations.js.map +1 -0
  17. package/dist/cli.d.ts +2 -0
  18. package/dist/cli.js +79 -0
  19. package/dist/cli.js.map +1 -0
  20. package/dist/commands/_sync.js +22 -0
  21. package/dist/commands/_sync.js.map +1 -0
  22. package/dist/commands/_translate.js +3 -0
  23. package/dist/commands/_translate.js.map +1 -0
  24. package/dist/commands/init.d.ts +1 -0
  25. package/dist/commands/init.js +439 -0
  26. package/dist/commands/init.js.map +1 -0
  27. package/dist/commands/login.d.ts +16 -0
  28. package/dist/commands/login.js +58 -0
  29. package/dist/commands/login.js.map +1 -0
  30. package/dist/commands/pull.js +22 -0
  31. package/dist/commands/pull.js.map +1 -0
  32. package/dist/commands/push.js +56 -0
  33. package/dist/commands/push.js.map +1 -0
  34. package/dist/commands/sync.d.ts +20 -0
  35. package/dist/commands/sync.js +22 -0
  36. package/dist/commands/sync.js.map +1 -0
  37. package/dist/commands/translate.d.ts +14 -0
  38. package/dist/commands/translate.js +145 -0
  39. package/dist/commands/translate.js.map +1 -0
  40. package/dist/index.d.ts +5 -0
  41. package/dist/index.js +8 -0
  42. package/dist/index.js.map +1 -0
  43. package/dist/types/index.d.ts +75 -0
  44. package/dist/types/index.js +17 -0
  45. package/dist/types/index.js.map +1 -0
  46. package/dist/types/translate/index.js +2 -0
  47. package/dist/types/translate/index.js.map +1 -0
  48. package/dist/utils/auth.d.ts +2 -0
  49. package/dist/utils/auth.js +29 -0
  50. package/dist/utils/auth.js.map +1 -0
  51. package/dist/utils/common.js +9 -0
  52. package/dist/utils/common.js.map +1 -0
  53. package/dist/utils/config.d.ts +23 -0
  54. package/dist/utils/config.js +137 -0
  55. package/dist/utils/config.js.map +1 -0
  56. package/dist/utils/errors.js +37 -0
  57. package/dist/utils/errors.js.map +1 -0
  58. package/dist/utils/files.d.ts +32 -0
  59. package/dist/utils/files.js +347 -0
  60. package/dist/utils/files.js.map +1 -0
  61. package/dist/utils/git.d.ts +21 -0
  62. package/dist/utils/git.js +87 -0
  63. package/dist/utils/git.js.map +1 -0
  64. package/dist/utils/github.d.ts +241 -0
  65. package/dist/utils/github.js +161 -0
  66. package/dist/utils/github.js.map +1 -0
  67. package/dist/utils/import-service.d.ts +4 -0
  68. package/dist/utils/import-service.js +218 -0
  69. package/dist/utils/import-service.js.map +1 -0
  70. package/dist/utils/prompt-service.d.ts +44 -0
  71. package/dist/utils/prompt-service.js +104 -0
  72. package/dist/utils/prompt-service.js.map +1 -0
  73. package/dist/utils/sync-service.d.ts +58 -0
  74. package/dist/utils/sync-service.js +159 -0
  75. package/dist/utils/sync-service.js.map +1 -0
  76. package/dist/utils/translation-processor.js +197 -0
  77. package/dist/utils/translation-processor.js.map +1 -0
  78. package/dist/utils/translation-updater/common.d.ts +6 -0
  79. package/{src → dist}/utils/translation-updater/common.js +16 -10
  80. package/dist/utils/translation-updater/common.js.map +1 -0
  81. package/dist/utils/translation-updater/index.d.ts +5 -0
  82. package/{src → dist}/utils/translation-updater/index.js +21 -9
  83. package/dist/utils/translation-updater/index.js.map +1 -0
  84. package/dist/utils/translation-updater/json-handler.d.ts +5 -0
  85. package/dist/utils/translation-updater/json-handler.js +123 -0
  86. package/dist/utils/translation-updater/json-handler.js.map +1 -0
  87. package/dist/utils/translation-updater/yaml-handler.d.ts +5 -0
  88. package/dist/utils/translation-updater/yaml-handler.js +180 -0
  89. package/dist/utils/translation-updater/yaml-handler.js.map +1 -0
  90. package/dist/utils/translation-utils.d.ts +30 -0
  91. package/dist/utils/translation-utils.js +324 -0
  92. package/dist/utils/translation-utils.js.map +1 -0
  93. package/dist/utils/updater.js +38 -0
  94. package/dist/utils/updater.js.map +1 -0
  95. package/package.json +26 -26
  96. package/src/api/auth.js +0 -24
  97. package/src/api/client.js +0 -83
  98. package/src/api/imports.js +0 -22
  99. package/src/api/projects.js +0 -24
  100. package/src/api/translations.js +0 -58
  101. package/src/cli.js +0 -78
  102. package/src/commands/init.js +0 -485
  103. package/src/commands/login.js +0 -80
  104. package/src/commands/sync.js +0 -28
  105. package/src/commands/translate.js +0 -267
  106. package/src/utils/auth.js +0 -23
  107. package/src/utils/config.js +0 -125
  108. package/src/utils/files.js +0 -381
  109. package/src/utils/git.js +0 -72
  110. package/src/utils/github.js +0 -128
  111. package/src/utils/import-service.js +0 -128
  112. package/src/utils/prompt-service.js +0 -67
  113. package/src/utils/sync-service.js +0 -147
  114. package/src/utils/translation-updater/json-handler.js +0 -111
  115. package/src/utils/translation-updater/yaml-handler.js +0 -207
  116. package/src/utils/translation-utils.js +0 -278
@@ -1,58 +0,0 @@
1
- import { getApiKey } from '../utils/auth.js';
2
- import { apiRequest } from './client.js';
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
- };
32
- }
33
-
34
- export async function checkJobStatus(jobId, includeTranslations = false) {
35
- const apiKey = await getApiKey();
36
- const endpoint = `/api/v1/translation_jobs/${jobId}${includeTranslations ? '?include_translations=true' : ''}`;
37
- return apiRequest(endpoint, { apiKey });
38
- }
39
-
40
- export async function getTranslations(jobId) {
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 DELETED
@@ -1,78 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import chalk from 'chalk';
5
- import { readFileSync } from 'fs';
6
- import { login } from './commands/login.js';
7
- import { init } from './commands/init.js';
8
- import { translate } from './commands/translate.js';
9
- import { sync } from './commands/sync.js';
10
-
11
- const program = new Command();
12
-
13
- function getVersion() {
14
- const packageJson = JSON.parse(
15
- readFileSync(new URL('../package.json', import.meta.url))
16
- );
17
- return packageJson.version;
18
- }
19
-
20
- function handleApiError(error) {
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);
34
- }
35
-
36
- function wrapCommandAction(action) {
37
- return function (...args) {
38
- return Promise.resolve(action(...args)).catch(handleApiError);
39
- };
40
- }
41
-
42
- program
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()));
59
-
60
- program
61
- .command('init')
62
- .description('Initialize a new LocalHero.ai project')
63
- .action(wrapCommandAction(() => init()));
64
-
65
- program
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)));
71
-
72
- program
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)));
77
-
78
- program.parse();
@@ -1,485 +0,0 @@
1
- import { promises as fs } from 'fs';
2
- import path from 'path';
3
- import chalk from 'chalk';
4
- import { createPromptService } from '../utils/prompt-service.js';
5
- import { createProject, listProjects } from '../api/projects.js';
6
- import { configService } from '../utils/config.js';
7
- import { checkAuth } from '../utils/auth.js';
8
- import { login } from './login.js';
9
- import { importService } from '../utils/import-service.js';
10
- import { createGitHubActionFile } from '../utils/github.js';
11
- import { directoryExists, findFirstExistingPath, getDirectoryContents } from '../utils/files.js';
12
-
13
- const PROJECT_TYPES = {
14
- rails: {
15
- directIndicators: ['config/application.rb', 'Gemfile'],
16
- defaults: {
17
- translationPath: 'config/locales/',
18
- filePattern: '**/*.{yml,yaml}'
19
- },
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']
29
- },
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
- }
146
- };
147
-
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
- }
156
- }
157
-
158
- async function detectFramework(config) {
159
- for (const indicator of config.directIndicators || []) {
160
- try {
161
- const stats = await fs.stat(indicator);
162
- if (stats.isFile()) return true;
163
- } catch {
164
- continue;
165
- }
166
- }
167
-
168
- if (config.packageCheck) {
169
- const deps = await checkPackageJson();
170
- if (deps) {
171
- const { requires = [], oneOf = [] } = config.packageCheck;
172
-
173
- if (requires.length && !requires.every(pkg => deps[pkg])) {
174
- return false;
175
- }
176
-
177
- if (oneOf.length && !oneOf.some(pkg => deps[pkg])) {
178
- return false;
179
- }
180
-
181
- return true;
182
- }
183
- }
184
-
185
- return false;
186
- }
187
-
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
- }
204
- };
205
- }
206
- }
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
- };
223
- }
224
- }
225
-
226
- return {
227
- type: 'generic',
228
- defaults: PROJECT_TYPES.generic.defaults
229
- };
230
- }
231
-
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
- };
254
-
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
- }
273
-
274
- const commonPaths = projectDefaults.commonPaths || PROJECT_TYPES.generic.commonPaths;
275
- const existingDirs = [];
276
-
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}';
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
- }
323
-
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
- };
377
-
378
- await configUtils.saveProjectConfig(config, basePath);
379
- console.log(chalk.green('\n✓ Created localhero.json'));
380
-
381
- if (answers.newProject) {
382
- console.log(chalk.green(`✓ Project created, view it at: https://localhero.ai/projects/${answers.projectId}`));
383
- }
384
-
385
- console.log('Configuration:');
386
- console.log(JSON.stringify(config, null, 2));
387
- console.log(' ');
388
-
389
- const shouldSetupGitHubAction = await promptService.confirm({
390
- message: 'Would you like to set up GitHub Actions for automatic translations?',
391
- default: true
392
- });
393
-
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);
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
- }
465
-
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
- });
470
-
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
- }
477
-
478
- if (importResult.translations_url) {
479
- console.log(chalk.blue(`\nView your translations at: ${importResult.translations_url}`));
480
- }
481
- }
482
- }
483
-
484
- console.log('\n🚀 Done! Start translating with: npx @localheroai/cli translate');
485
- }
@@ -1,80 +0,0 @@
1
- import chalk from 'chalk';
2
- import { createPromptService } from '../utils/prompt-service.js';
3
- import { updateGitignore } from '../utils/git.js';
4
- import { verifyApiKey as defaultVerifyApiKey } from '../api/auth.js';
5
- import { configService } from '../utils/config.js';
6
-
7
- const API_KEY_PATTERN = /^tk_[a-zA-Z0-9]{48}$/;
8
-
9
- export async function login(deps = {}) {
10
- const {
11
- console = global.console,
12
- basePath = process.cwd(),
13
- promptService = createPromptService({ inquirer: await import('@inquirer/prompts') }),
14
- verifyApiKey = defaultVerifyApiKey,
15
- gitUtils = { updateGitignore },
16
- configUtils = configService,
17
- isCalledFromInit = false
18
- } = deps;
19
-
20
- const existingConfig = await configUtils.getAuthConfig(basePath);
21
-
22
- if (existingConfig?.api_key) {
23
- console.log(chalk.yellow('\n⚠️ Warning: This will replace your existing API key configuration'));
24
- }
25
-
26
- const apiKey = process.env.LOCALHERO_API_KEY || (
27
- console.log('\n→ Get your API key from: https://localhero.ai/api-keys'),
28
- console.log('→ New to LocalHero? Sign up at: https://localhero.ai/signup'),
29
- console.log(chalk.gray('The API key will be saved to .localhero_key, and automatically added to your .gitignore file.\n')),
30
- await promptService.getApiKey()
31
- );
32
-
33
- if (!apiKey) {
34
- throw new Error('User cancelled');
35
- }
36
-
37
- if (!API_KEY_PATTERN.test(apiKey)) {
38
- throw new Error('Invalid API key format');
39
- }
40
-
41
- const result = await verifyApiKey(apiKey);
42
-
43
- if (result.error) {
44
- if (result.error.code === 'invalid_api_key') {
45
- console.log(chalk.red('\n❌ ' + result.error.message));
46
- console.log(chalk.blue('\nℹ️ Get a new API key at https://localhero.ai/api-keys'));
47
- process.exit(1);
48
- }
49
- throw new Error(result.error.message);
50
- }
51
-
52
- const config = {
53
- api_key: apiKey,
54
- last_verified: new Date().toISOString()
55
- };
56
-
57
- await configUtils.saveAuthConfig(config, basePath);
58
- const gitignoreUpdated = await gitUtils.updateGitignore(basePath);
59
-
60
- console.log(chalk.green('\n✓ API key verified and saved to .localhero_key'));
61
- if (gitignoreUpdated) {
62
- console.log(chalk.green('✓ Added .localhero_key to .gitignore'));
63
- }
64
-
65
- console.log(chalk.blue(`💼️ Organization: ${result.organization.name}`));
66
- if (result.organization.projects.length > 0) {
67
- console.log(chalk.blue(`\n📚 Projects: ${result.organization.projects.map(p => p.name).join(', ')}`));
68
- }
69
-
70
- const projectConfig = await configUtils.getProjectConfig(basePath);
71
-
72
- if (!projectConfig && !isCalledFromInit) {
73
- console.log(chalk.yellow('\n⚠️ Almost there! You need to set up your project configuration.'));
74
- console.log(chalk.blue('Run this next:'));
75
- console.log(chalk.white('\n npx @localheroai/cli init\n'));
76
- } else if (!isCalledFromInit) {
77
- console.log('\nYou\'re ready to start translating!');
78
- console.log('Try running: npx @localheroai/cli translate');
79
- }
80
- }