gtx-cli 2.4.15 → 2.5.0-alpha.1

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 (58) hide show
  1. package/dist/api/collectUserEditDiffs.d.ts +2 -7
  2. package/dist/api/collectUserEditDiffs.js +33 -78
  3. package/dist/api/downloadFileBatch.d.ts +11 -10
  4. package/dist/api/downloadFileBatch.js +120 -127
  5. package/dist/api/saveLocalEdits.js +18 -15
  6. package/dist/cli/base.js +1 -1
  7. package/dist/cli/commands/stage.d.ts +8 -2
  8. package/dist/cli/commands/stage.js +25 -7
  9. package/dist/cli/commands/translate.d.ts +4 -2
  10. package/dist/cli/commands/translate.js +5 -6
  11. package/dist/cli/flags.js +4 -1
  12. package/dist/config/generateSettings.js +10 -0
  13. package/dist/console/logging.d.ts +1 -1
  14. package/dist/console/logging.js +3 -4
  15. package/dist/formats/files/translate.d.ts +2 -2
  16. package/dist/formats/files/translate.js +12 -7
  17. package/dist/fs/config/downloadedVersions.d.ts +10 -3
  18. package/dist/fs/config/downloadedVersions.js +8 -0
  19. package/dist/fs/config/updateVersions.d.ts +2 -1
  20. package/dist/git/branches.d.ts +7 -0
  21. package/dist/git/branches.js +88 -0
  22. package/dist/types/branch.d.ts +14 -0
  23. package/dist/types/branch.js +1 -0
  24. package/dist/types/data.d.ts +1 -1
  25. package/dist/types/files.d.ts +7 -0
  26. package/dist/types/index.d.ts +7 -0
  27. package/dist/utils/SpinnerManager.d.ts +30 -0
  28. package/dist/utils/SpinnerManager.js +73 -0
  29. package/dist/utils/gitDiff.js +18 -16
  30. package/dist/workflow/BranchStep.d.ts +13 -0
  31. package/dist/workflow/BranchStep.js +131 -0
  32. package/dist/workflow/DownloadStep.d.ts +19 -0
  33. package/dist/workflow/DownloadStep.js +127 -0
  34. package/dist/workflow/EnqueueStep.d.ts +15 -0
  35. package/dist/workflow/EnqueueStep.js +33 -0
  36. package/dist/workflow/PollJobsStep.d.ts +31 -0
  37. package/dist/workflow/PollJobsStep.js +284 -0
  38. package/dist/workflow/SetupStep.d.ts +16 -0
  39. package/dist/workflow/SetupStep.js +71 -0
  40. package/dist/workflow/UploadStep.d.ts +21 -0
  41. package/dist/workflow/UploadStep.js +72 -0
  42. package/dist/workflow/UserEditDiffsStep.d.ts +11 -0
  43. package/dist/workflow/UserEditDiffsStep.js +30 -0
  44. package/dist/workflow/Workflow.d.ts +4 -0
  45. package/dist/workflow/Workflow.js +2 -0
  46. package/dist/workflow/download.d.ts +22 -0
  47. package/dist/workflow/download.js +104 -0
  48. package/dist/workflow/stage.d.ts +14 -0
  49. package/dist/workflow/stage.js +76 -0
  50. package/package.json +4 -5
  51. package/dist/api/checkFileTranslations.d.ts +0 -23
  52. package/dist/api/checkFileTranslations.js +0 -281
  53. package/dist/api/sendFiles.d.ts +0 -17
  54. package/dist/api/sendFiles.js +0 -127
  55. package/dist/api/sendUserEdits.d.ts +0 -19
  56. package/dist/api/sendUserEdits.js +0 -15
  57. package/dist/cli/commands/edits.d.ts +0 -8
  58. package/dist/cli/commands/edits.js +0 -32
@@ -1,281 +0,0 @@
1
- import chalk from 'chalk';
2
- import { createOraSpinner, logError } from '../console/logging.js';
3
- import { getLocaleProperties } from 'generaltranslation';
4
- import { downloadFileBatch } from './downloadFileBatch.js';
5
- import { gt } from '../utils/gt.js';
6
- import { TEMPLATE_FILE_NAME } from '../cli/commands/stage.js';
7
- import { clearLocaleDirs } from '../fs/clearLocaleDirs.js';
8
- import path from 'node:path';
9
- /**
10
- * Checks the status of translations for a given version ID
11
- * @param apiKey - The API key for the General Translation API
12
- * @param baseUrl - The base URL for the General Translation API
13
- * @param versionId - The version ID of the project
14
- * @param locales - The locales to wait for
15
- * @param startTime - The start time of the wait
16
- * @param timeoutDuration - The timeout duration for the wait in seconds
17
- * @returns True if all translations are deployed, false otherwise
18
- */
19
- export async function checkFileTranslations(data, locales, timeoutDuration, resolveOutputPath, options, forceRetranslation, forceDownload) {
20
- const startTime = Date.now();
21
- console.log();
22
- const spinner = await createOraSpinner();
23
- const spinnerMessage = forceRetranslation
24
- ? 'Waiting for retranslation...'
25
- : 'Waiting for translation...';
26
- spinner.start(spinnerMessage);
27
- // Initialize the query data
28
- const fileQueryData = prepareFileQueryData(data, locales);
29
- // Clear translated files before any downloads (if enabled)
30
- if (options.options?.experimentalClearLocaleDirs === true &&
31
- fileQueryData.length > 0) {
32
- const translatedFiles = new Set(fileQueryData
33
- .map((file) => {
34
- const outputPath = resolveOutputPath(file.fileName, file.locale);
35
- // Only clear if the output path is different from the source (i.e., there's a transform)
36
- return outputPath !== null && outputPath !== file.fileName
37
- ? outputPath
38
- : null;
39
- })
40
- .filter((path) => path !== null));
41
- // Derive cwd from config path
42
- const cwd = path.dirname(options.config);
43
- await clearLocaleDirs(translatedFiles, locales, options.options?.clearLocaleDirsExclude, cwd);
44
- }
45
- const downloadStatus = {
46
- downloaded: new Set(),
47
- failed: new Set(),
48
- skipped: new Set(),
49
- };
50
- // Do first check immediately, but skip if force retranslation is enabled
51
- if (!forceRetranslation) {
52
- const initialCheck = await checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options, forceDownload);
53
- if (initialCheck) {
54
- spinner.succeed(chalk.green('Files translated!'));
55
- return true;
56
- }
57
- }
58
- // Calculate time until next 5-second interval since startTime
59
- const msUntilNextInterval = Math.max(0, 5000 - ((Date.now() - startTime) % 5000));
60
- return new Promise((resolve) => {
61
- let intervalCheck;
62
- // Start the interval aligned with the original request time
63
- setTimeout(() => {
64
- intervalCheck = setInterval(async () => {
65
- const isDeployed = await checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options, forceDownload);
66
- const elapsed = Date.now() - startTime;
67
- if (isDeployed || elapsed >= timeoutDuration * 1000) {
68
- clearInterval(intervalCheck);
69
- if (isDeployed) {
70
- spinner.succeed(chalk.green('All files translated!'));
71
- resolve(true);
72
- }
73
- else {
74
- spinner.fail(chalk.red('Timed out waiting for translations'));
75
- resolve(false);
76
- }
77
- }
78
- }, 5000);
79
- }, msUntilNextInterval);
80
- });
81
- }
82
- /**
83
- * Prepares the file query data from input data and locales
84
- */
85
- function prepareFileQueryData(data, locales) {
86
- const fileQueryData = [];
87
- for (const file in data) {
88
- for (const locale of locales) {
89
- fileQueryData.push({
90
- versionId: data[file].versionId,
91
- fileName: data[file].fileName,
92
- locale,
93
- });
94
- }
95
- }
96
- return fileQueryData;
97
- }
98
- /**
99
- * Generates a formatted status text showing translation progress
100
- * @param downloadedFiles - Set of downloaded file+locale combinations
101
- * @param fileQueryData - Array of file query data objects
102
- * @returns Formatted status text
103
- */
104
- function generateStatusSuffixText(downloadStatus, fileQueryData) {
105
- // Simple progress indicator
106
- const progressText = chalk.green(`[${downloadStatus.downloaded.size +
107
- downloadStatus.failed.size +
108
- downloadStatus.skipped.size}/${fileQueryData.length}]`) + ` translations completed`;
109
- // Get terminal height to adapt our output
110
- const terminalHeight = process.stdout.rows || 24; // Default to 24 if undefined
111
- // If terminal is very small, just show the basic progress
112
- if (terminalHeight < 6) {
113
- return `${progressText}`;
114
- }
115
- const newSuffixText = [`${progressText}`];
116
- // Organize data by filename
117
- const fileStatus = new Map();
118
- // Initialize with all files and locales from fileQueryData
119
- for (const item of fileQueryData) {
120
- if (!fileStatus.has(item.fileName)) {
121
- fileStatus.set(item.fileName, {
122
- completed: new Set(),
123
- pending: new Set([item.locale]),
124
- failed: new Set(),
125
- skipped: new Set(),
126
- });
127
- }
128
- else {
129
- fileStatus.get(item.fileName)?.pending.add(item.locale);
130
- }
131
- }
132
- // Mark which ones are completed or failed
133
- for (const fileLocale of downloadStatus.downloaded) {
134
- const [fileName, locale] = fileLocale.split(':');
135
- const status = fileStatus.get(fileName);
136
- if (status) {
137
- status.pending.delete(locale);
138
- status.completed.add(locale);
139
- }
140
- }
141
- for (const fileLocale of downloadStatus.failed) {
142
- const [fileName, locale] = fileLocale.split(':');
143
- const status = fileStatus.get(fileName);
144
- if (status) {
145
- status.pending.delete(locale);
146
- status.failed.add(locale);
147
- }
148
- }
149
- for (const fileLocale of downloadStatus.skipped) {
150
- const [fileName, locale] = fileLocale.split(':');
151
- const status = fileStatus.get(fileName);
152
- if (status) {
153
- status.pending.delete(locale);
154
- status.skipped.add(locale);
155
- }
156
- }
157
- // Calculate how many files we can show based on terminal height
158
- const filesArray = Array.from(fileStatus.entries());
159
- const maxFilesToShow = Math.min(filesArray.length, terminalHeight - 3 // Header + progress + buffer
160
- );
161
- // Display each file with its status on a single line
162
- for (let i = 0; i < maxFilesToShow; i++) {
163
- const [fileName, status] = filesArray[i];
164
- // Create condensed locale status
165
- const localeStatuses = [];
166
- // Add completed locales
167
- if (status.completed.size > 0) {
168
- const completedCodes = Array.from(status.completed)
169
- .map((locale) => getLocaleProperties(locale).code)
170
- .join(', ');
171
- localeStatuses.push(chalk.green(`${completedCodes}`));
172
- }
173
- // Add (translated but not downloaded) skipped locales
174
- if (status.skipped.size > 0) {
175
- const skippedCodes = Array.from(status.skipped)
176
- .map((locale) => getLocaleProperties(locale).code)
177
- .join(', ');
178
- localeStatuses.push(chalk.green(`${skippedCodes}`));
179
- }
180
- // Add failed locales
181
- if (status.failed.size > 0) {
182
- const failedCodes = Array.from(status.failed)
183
- .map((locale) => getLocaleProperties(locale).code)
184
- .join(', ');
185
- localeStatuses.push(chalk.red(`${failedCodes}`));
186
- }
187
- // Add pending locales
188
- if (status.pending.size > 0) {
189
- const pendingCodes = Array.from(status.pending)
190
- .map((locale) => getLocaleProperties(locale).code)
191
- .join(', ');
192
- localeStatuses.push(chalk.yellow(`${pendingCodes}`));
193
- }
194
- // Format the line
195
- const prettyFileName = fileName === TEMPLATE_FILE_NAME ? '<React Elements>' : fileName;
196
- newSuffixText.push(`${chalk.bold(prettyFileName)} [${localeStatuses.join(', ')}]`);
197
- }
198
- // If we couldn't show all files, add an indicator
199
- if (filesArray.length > maxFilesToShow) {
200
- newSuffixText.push(`... and ${filesArray.length - maxFilesToShow} more files`);
201
- }
202
- return newSuffixText.join('\n');
203
- }
204
- /**
205
- * Checks translation status and downloads ready files
206
- */
207
- async function checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options, forceDownload) {
208
- try {
209
- // Only query for files that haven't been downloaded yet
210
- const currentQueryData = fileQueryData.filter((item) => !downloadStatus.downloaded.has(`${item.fileName}:${item.locale}`) &&
211
- !downloadStatus.failed.has(`${item.fileName}:${item.locale}`) &&
212
- !downloadStatus.skipped.has(`${item.fileName}:${item.locale}`));
213
- // If all files have been downloaded, we're done
214
- if (currentQueryData.length === 0) {
215
- return true;
216
- }
217
- // Check for translations
218
- const responseData = await gt.checkFileTranslations(currentQueryData);
219
- const translations = responseData.translations || [];
220
- // Filter for ready translations
221
- const readyTranslations = translations.filter((translation) => translation.isReady && translation.fileName);
222
- if (readyTranslations.length > 0) {
223
- // Build version map by fileName:locale for this batch
224
- const versionMap = new Map(fileQueryData.map((item) => [
225
- `${item.fileName}:${gt.resolveAliasLocale(item.locale)}`,
226
- item.versionId,
227
- ]));
228
- // Prepare batch download data
229
- const batchFiles = readyTranslations
230
- .map((translation) => {
231
- const locale = gt.resolveAliasLocale(translation.locale);
232
- const fileName = translation.fileName;
233
- const translationId = translation.id;
234
- const outputPath = resolveOutputPath(fileName, locale);
235
- // Skip downloading GTJSON files that are not in the files configuration
236
- if (outputPath === null) {
237
- downloadStatus.skipped.add(`${fileName}:${locale}`);
238
- return null;
239
- }
240
- return {
241
- translationId,
242
- inputPath: fileName,
243
- outputPath,
244
- locale,
245
- fileLocale: `${fileName}:${locale}`,
246
- fileId: translation.fileId,
247
- versionId: versionMap.get(`${fileName}:${locale}`),
248
- };
249
- })
250
- .filter((file) => file !== null);
251
- if (batchFiles.length > 0) {
252
- const batchResult = await downloadFileBatch(batchFiles, options, 3, 1000, Boolean(forceDownload));
253
- // Process results
254
- batchFiles.forEach((file) => {
255
- const { translationId, fileLocale } = file;
256
- if (batchResult.successful.includes(translationId)) {
257
- downloadStatus.downloaded.add(fileLocale);
258
- }
259
- else if (batchResult.failed.includes(translationId)) {
260
- downloadStatus.failed.add(fileLocale);
261
- }
262
- });
263
- }
264
- }
265
- // Force a refresh of the spinner display
266
- const statusText = generateStatusSuffixText(downloadStatus, fileQueryData);
267
- // Clear and reapply the suffix to force a refresh
268
- spinner.text = statusText;
269
- // If all files have been downloaded, we're done
270
- if (downloadStatus.downloaded.size +
271
- downloadStatus.failed.size +
272
- downloadStatus.skipped.size ===
273
- fileQueryData.length) {
274
- return true;
275
- }
276
- }
277
- catch (error) {
278
- logError(chalk.red('Error checking translation status: ') + error);
279
- }
280
- return false;
281
- }
@@ -1,17 +0,0 @@
1
- import { Settings, TranslateFlags } from '../types/index.js';
2
- import { CompletedFileTranslationData, FileToTranslate } from 'generaltranslation/types';
3
- export type SendFilesResult = {
4
- data: Record<string, {
5
- fileName: string;
6
- versionId: string;
7
- }>;
8
- locales: string[];
9
- translations: CompletedFileTranslationData[];
10
- };
11
- /**
12
- * Sends multiple files for translation to the API
13
- * @param files - Array of file objects to translate
14
- * @param options - The options for the API call
15
- * @returns The translated content or version ID
16
- */
17
- export declare function sendFiles(files: FileToTranslate[], options: TranslateFlags, settings: Settings): Promise<SendFilesResult>;
@@ -1,127 +0,0 @@
1
- import chalk from 'chalk';
2
- import { createSpinner, logErrorAndExit, logMessage, logSuccess, } from '../console/logging.js';
3
- import { gt } from '../utils/gt.js';
4
- import { TEMPLATE_FILE_NAME } from '../cli/commands/stage.js';
5
- import { collectAndSendUserEditDiffs } from './collectUserEditDiffs.js';
6
- /**
7
- * Sends multiple files for translation to the API
8
- * @param files - Array of file objects to translate
9
- * @param options - The options for the API call
10
- * @returns The translated content or version ID
11
- */
12
- export async function sendFiles(files, options, settings) {
13
- // Keep track of the most recent spinner so we can stop it on error
14
- let currentSpinner = null;
15
- logMessage(chalk.cyan('Files to translate:') +
16
- '\n' +
17
- files
18
- .map((file) => {
19
- if (file.fileName === TEMPLATE_FILE_NAME) {
20
- return `- <React Elements>`;
21
- }
22
- return `- ${file.fileName}`;
23
- })
24
- .join('\n'));
25
- try {
26
- // Step 1: Upload files (get references)
27
- const uploadSpinner = createSpinner('dots');
28
- currentSpinner = uploadSpinner;
29
- uploadSpinner.start(`Uploading ${files.length} file${files.length !== 1 ? 's' : ''} to General Translation API...`);
30
- const sourceLocale = settings.defaultLocale;
31
- if (!sourceLocale) {
32
- uploadSpinner.stop(chalk.red('Missing default source locale'));
33
- logErrorAndExit('sendFiles: settings.defaultLocale is required to upload source files');
34
- }
35
- // Convert FileToTranslate[] -> { source: FileUpload }[]
36
- const uploads = files.map(({ content, fileName, fileFormat, dataFormat }) => ({
37
- source: {
38
- content,
39
- fileName,
40
- fileFormat,
41
- dataFormat,
42
- locale: sourceLocale,
43
- },
44
- }));
45
- const upload = await gt.uploadSourceFiles(uploads, {
46
- sourceLocale,
47
- modelProvider: settings.modelProvider,
48
- });
49
- uploadSpinner.stop(chalk.green('Files uploaded successfully'));
50
- // Calculate timeout once for setup fetching
51
- // Accept number or numeric string, default to 600s
52
- const timeoutVal = options?.timeout !== undefined ? Number(options.timeout) : 600;
53
- const setupTimeoutMs = (Number.isFinite(timeoutVal) ? timeoutVal : 600) * 1000;
54
- const setupResult = await gt.setupProject(upload.uploadedFiles, {
55
- locales: settings.locales,
56
- });
57
- if (setupResult?.status === 'queued') {
58
- const { setupJobId } = setupResult;
59
- const setupSpinner = createSpinner('dots');
60
- currentSpinner = setupSpinner;
61
- setupSpinner.start('Setting up project...');
62
- const start = Date.now();
63
- const pollInterval = 2000;
64
- let setupCompleted = false;
65
- let setupFailedMessage = null;
66
- while (true) {
67
- const status = await gt.checkSetupStatus(setupJobId);
68
- if (status.status === 'completed') {
69
- setupCompleted = true;
70
- break;
71
- }
72
- if (status.status === 'failed') {
73
- setupFailedMessage = status.error?.message || 'Unknown error';
74
- break;
75
- }
76
- if (Date.now() - start > setupTimeoutMs) {
77
- setupFailedMessage = 'Timed out while waiting for setup generation';
78
- break;
79
- }
80
- await new Promise((r) => setTimeout(r, pollInterval));
81
- }
82
- if (setupCompleted) {
83
- setupSpinner.stop(chalk.green('Setup successfully completed'));
84
- }
85
- else {
86
- setupSpinner.stop(chalk.yellow(`Setup ${setupFailedMessage ? 'failed' : 'timed out'} — proceeding without setup${setupFailedMessage ? ` (${setupFailedMessage})` : ''}`));
87
- }
88
- }
89
- // Step 3 (optional): Prior to enqueue, detect and submit user edit diffs
90
- if (options?.saveLocal) {
91
- const prepSpinner = createSpinner('dots');
92
- currentSpinner = prepSpinner;
93
- prepSpinner.start('Updating translations...');
94
- try {
95
- await collectAndSendUserEditDiffs(upload.uploadedFiles, settings);
96
- }
97
- catch {
98
- // Non-fatal; keep going to enqueue
99
- }
100
- finally {
101
- prepSpinner.stop('Updated translations');
102
- }
103
- }
104
- // Step 4: Enqueue translations by reference
105
- const enqueueSpinner = createSpinner('dots');
106
- currentSpinner = enqueueSpinner;
107
- enqueueSpinner.start('Enqueuing translations...');
108
- const enqueueResult = await gt.enqueueFiles(upload.uploadedFiles, {
109
- sourceLocale: settings.defaultLocale,
110
- targetLocales: settings.locales,
111
- publish: settings.publish,
112
- requireApproval: settings.stageTranslations,
113
- modelProvider: settings.modelProvider,
114
- force: options?.force,
115
- });
116
- const { data, message, locales, translations } = enqueueResult;
117
- enqueueSpinner.stop(chalk.green('Files for translation uploaded successfully'));
118
- logSuccess(message);
119
- return { data, locales, translations };
120
- }
121
- catch {
122
- if (currentSpinner) {
123
- currentSpinner.stop();
124
- }
125
- logErrorAndExit('Failed to send files for translation');
126
- }
127
- }
@@ -1,19 +0,0 @@
1
- import { Settings } from '../types/index.js';
2
- export type UserEditDiff = {
3
- fileName: string;
4
- locale: string;
5
- diff: string;
6
- versionId?: string;
7
- fileId?: string;
8
- localContent?: string;
9
- };
10
- export type SendUserEditsPayload = {
11
- projectId?: string;
12
- diffs: UserEditDiff[];
13
- };
14
- /**
15
- * Sends user edit diffs to the API for persistence/rule extraction.
16
- * This function is intentionally decoupled from the translate pipeline
17
- * so it can be called as an independent action.
18
- */
19
- export declare function sendUserEditDiffs(diffs: UserEditDiff[], settings: Settings): Promise<void>;
@@ -1,15 +0,0 @@
1
- import { gt } from '../utils/gt.js';
2
- /**
3
- * Sends user edit diffs to the API for persistence/rule extraction.
4
- * This function is intentionally decoupled from the translate pipeline
5
- * so it can be called as an independent action.
6
- */
7
- export async function sendUserEditDiffs(diffs, settings) {
8
- if (!diffs.length)
9
- return;
10
- const payload = {
11
- projectId: settings.projectId,
12
- diffs,
13
- };
14
- await gt.submitUserEditDiffs(payload);
15
- }
@@ -1,8 +0,0 @@
1
- import { Settings } from '../../types/index.js';
2
- export type SendDiffsFlags = {
3
- fileName: string;
4
- locale: string;
5
- old: string;
6
- next: string;
7
- };
8
- export declare function handleSendDiffs(flags: SendDiffsFlags, settings: Settings): Promise<void>;
@@ -1,32 +0,0 @@
1
- import fs from 'node:fs';
2
- import { getGitUnifiedDiff } from '../../utils/gitDiff.js';
3
- import { sendUserEditDiffs } from '../../api/sendUserEdits.js';
4
- import { logErrorAndExit, logMessage } from '../../console/logging.js';
5
- export async function handleSendDiffs(flags, settings) {
6
- const { fileName, locale, old, next } = flags;
7
- if (!fs.existsSync(old)) {
8
- logErrorAndExit(`Old/original file not found: ${old}`);
9
- }
10
- if (!fs.existsSync(next)) {
11
- logErrorAndExit(`New/local file not found: ${next}`);
12
- }
13
- let diff;
14
- try {
15
- diff = await getGitUnifiedDiff(old, next);
16
- }
17
- catch (e) {
18
- logErrorAndExit('Git is required to compute diffs. Please install Git and ensure it is available on your PATH.');
19
- return; // unreachable
20
- }
21
- if (!diff || diff.trim().length === 0) {
22
- logMessage('No differences detected — nothing to send.');
23
- return;
24
- }
25
- await sendUserEditDiffs([
26
- {
27
- fileName,
28
- locale,
29
- diff,
30
- },
31
- ], settings);
32
- }