gtx-cli 2.3.9 → 2.3.11

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/CHANGELOG.md CHANGED
@@ -1,5 +1,17 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.3.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [#732](https://github.com/generaltranslation/gt/pull/732) [`bcd8272`](https://github.com/generaltranslation/gt/commit/bcd8272576ff02432e39cf1887a48b4f566eb752) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Added freezing when fetching translations for unmodified source files. This will keep any local changes until retranslation is triggered or --force-download is used
8
+
9
+ ## 2.3.10
10
+
11
+ ### Patch Changes
12
+
13
+ - [#731](https://github.com/generaltranslation/gt/pull/731) [`6896570`](https://github.com/generaltranslation/gt/commit/68965708f43f1bdd0315aa96ce69b6ef6d68260d) Thanks [@SamEggert](https://github.com/SamEggert)! - check for gt.config.json in the .locadex directory
14
+
3
15
  ## 2.3.9
4
16
 
5
17
  ### Patch Changes
@@ -20,4 +20,4 @@ export declare function checkFileTranslations(data: {
20
20
  versionId: string;
21
21
  fileName: string;
22
22
  };
23
- }, locales: string[], timeoutDuration: number, resolveOutputPath: (sourcePath: string, locale: string) => string | null, options: Settings, forceRetranslation?: boolean): Promise<boolean>;
23
+ }, locales: string[], timeoutDuration: number, resolveOutputPath: (sourcePath: string, locale: string) => string | null, options: Settings, forceRetranslation?: boolean, forceDownload?: boolean): Promise<boolean>;
@@ -16,7 +16,7 @@ import path from 'node:path';
16
16
  * @param timeoutDuration - The timeout duration for the wait in seconds
17
17
  * @returns True if all translations are deployed, false otherwise
18
18
  */
19
- export async function checkFileTranslations(data, locales, timeoutDuration, resolveOutputPath, options, forceRetranslation) {
19
+ export async function checkFileTranslations(data, locales, timeoutDuration, resolveOutputPath, options, forceRetranslation, forceDownload) {
20
20
  const startTime = Date.now();
21
21
  console.log();
22
22
  const spinner = await createOraSpinner();
@@ -49,7 +49,7 @@ export async function checkFileTranslations(data, locales, timeoutDuration, reso
49
49
  };
50
50
  // Do first check immediately, but skip if force retranslation is enabled
51
51
  if (!forceRetranslation) {
52
- const initialCheck = await checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options);
52
+ const initialCheck = await checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options, forceDownload);
53
53
  if (initialCheck) {
54
54
  spinner.succeed(chalk.green('Files translated!'));
55
55
  return true;
@@ -62,7 +62,7 @@ export async function checkFileTranslations(data, locales, timeoutDuration, reso
62
62
  // Start the interval aligned with the original request time
63
63
  setTimeout(() => {
64
64
  intervalCheck = setInterval(async () => {
65
- const isDeployed = await checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options);
65
+ const isDeployed = await checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options, forceDownload);
66
66
  const elapsed = Date.now() - startTime;
67
67
  if (isDeployed || elapsed >= timeoutDuration * 1000) {
68
68
  clearInterval(intervalCheck);
@@ -186,7 +186,7 @@ function generateStatusSuffixText(downloadStatus, fileQueryData) {
186
186
  /**
187
187
  * Checks translation status and downloads ready files
188
188
  */
189
- async function checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options) {
189
+ async function checkTranslationDeployment(fileQueryData, downloadStatus, spinner, resolveOutputPath, options, forceDownload) {
190
190
  try {
191
191
  // Only query for files that haven't been downloaded yet
192
192
  const currentQueryData = fileQueryData.filter((item) => !downloadStatus.downloaded.has(`${item.fileName}:${item.locale}`) &&
@@ -202,6 +202,11 @@ async function checkTranslationDeployment(fileQueryData, downloadStatus, spinner
202
202
  // Filter for ready translations
203
203
  const readyTranslations = translations.filter((translation) => translation.isReady && translation.fileName);
204
204
  if (readyTranslations.length > 0) {
205
+ // Build version map by fileName:locale for this batch
206
+ const versionMap = new Map(fileQueryData.map((item) => [
207
+ `${item.fileName}:${gt.resolveAliasLocale(item.locale)}`,
208
+ item.versionId,
209
+ ]));
205
210
  // Prepare batch download data
206
211
  const batchFiles = readyTranslations
207
212
  .map((translation) => {
@@ -220,10 +225,11 @@ async function checkTranslationDeployment(fileQueryData, downloadStatus, spinner
220
225
  outputPath,
221
226
  locale,
222
227
  fileLocale: `${fileName}:${locale}`,
228
+ versionId: versionMap.get(`${fileName}:${locale}`),
223
229
  };
224
230
  })
225
231
  .filter((file) => file !== null);
226
- const batchResult = await downloadFileBatch(batchFiles, options);
232
+ const batchResult = await downloadFileBatch(batchFiles, options, 3, 1000, Boolean(forceDownload));
227
233
  // Process results
228
234
  batchFiles.forEach((file) => {
229
235
  const { translationId, fileLocale } = file;
@@ -5,6 +5,8 @@ export type BatchedFiles = Array<{
5
5
  inputPath: string;
6
6
  locale: string;
7
7
  fileLocale: string;
8
+ fileId?: string;
9
+ versionId?: string;
8
10
  }>;
9
11
  export type DownloadFileBatchResult = {
10
12
  successful: string[];
@@ -17,4 +19,4 @@ export type DownloadFileBatchResult = {
17
19
  * @param retryDelay - Delay between retries in milliseconds
18
20
  * @returns Object containing successful and failed file IDs
19
21
  */
20
- export declare function downloadFileBatch(files: BatchedFiles, options: Settings, maxRetries?: number, retryDelay?: number): Promise<DownloadFileBatchResult>;
22
+ export declare function downloadFileBatch(files: BatchedFiles, options: Settings, maxRetries?: number, retryDelay?: number, forceDownload?: boolean): Promise<DownloadFileBatchResult>;
@@ -6,6 +6,7 @@ import { validateJsonSchema } from '../formats/json/utils.js';
6
6
  import { validateYamlSchema } from '../formats/yaml/utils.js';
7
7
  import { mergeJson } from '../formats/json/mergeJson.js';
8
8
  import mergeYaml from '../formats/yaml/mergeYaml.js';
9
+ import { getDownloadedVersions, saveDownloadedVersions, } from '../fs/config/downloadedVersions.js';
9
10
  /**
10
11
  * Downloads multiple translation files in a single batch request
11
12
  * @param files - Array of files to download with their output paths
@@ -13,17 +14,22 @@ import mergeYaml from '../formats/yaml/mergeYaml.js';
13
14
  * @param retryDelay - Delay between retries in milliseconds
14
15
  * @returns Object containing successful and failed file IDs
15
16
  */
16
- export async function downloadFileBatch(files, options, maxRetries = 3, retryDelay = 1000) {
17
+ export async function downloadFileBatch(files, options, maxRetries = 3, retryDelay = 1000, forceDownload = false) {
18
+ // Local record of what version was last downloaded for each fileName:locale
19
+ const downloadedVersions = getDownloadedVersions(options.configDirectory);
20
+ let didUpdateDownloadedLock = false;
17
21
  let retries = 0;
18
22
  const fileIds = files.map((file) => file.translationId);
19
23
  const result = { successful: [], failed: [] };
20
24
  // Create a map of translationId to outputPath for easier lookup
21
25
  const outputPathMap = new Map(files.map((file) => [file.translationId, file.outputPath]));
22
26
  const inputPathMap = new Map(files.map((file) => [file.translationId, file.inputPath]));
27
+ const fileIdMap = new Map(files.map((file) => [file.translationId, file.fileId]));
23
28
  const localeMap = new Map(files.map((file) => [
24
29
  file.translationId,
25
30
  gt.resolveAliasLocale(file.locale),
26
31
  ]));
32
+ const versionMap = new Map(files.map((file) => [file.translationId, file.versionId]));
27
33
  while (retries <= maxRetries) {
28
34
  try {
29
35
  // Download the files
@@ -36,6 +42,8 @@ export async function downloadFileBatch(files, options, maxRetries = 3, retryDel
36
42
  const outputPath = outputPathMap.get(translationId);
37
43
  const inputPath = inputPathMap.get(translationId);
38
44
  const locale = localeMap.get(translationId);
45
+ const fileId = fileIdMap.get(translationId);
46
+ const versionId = versionMap.get(translationId);
39
47
  if (!outputPath || !inputPath) {
40
48
  logWarning(`No input/output path found for file: ${translationId}`);
41
49
  result.failed.push(translationId);
@@ -46,6 +54,18 @@ export async function downloadFileBatch(files, options, maxRetries = 3, retryDel
46
54
  if (!fs.existsSync(dir)) {
47
55
  fs.mkdirSync(dir, { recursive: true });
48
56
  }
57
+ // If a local translation already exists for the same source version, skip overwrite
58
+ const keyId = fileId || inputPath;
59
+ const downloadedKey = `${keyId}:${locale}`;
60
+ const alreadyDownloadedVersion = downloadedVersions.entries[downloadedKey]?.versionId;
61
+ const fileExists = fs.existsSync(outputPath);
62
+ if (!forceDownload &&
63
+ fileExists &&
64
+ versionId &&
65
+ alreadyDownloadedVersion === versionId) {
66
+ result.successful.push(translationId);
67
+ continue;
68
+ }
49
69
  let data = file.data;
50
70
  if (options.options?.jsonSchema && locale) {
51
71
  const jsonSchema = validateJsonSchema(options.options, inputPath);
@@ -78,6 +98,15 @@ export async function downloadFileBatch(files, options, maxRetries = 3, retryDel
78
98
  // Write the file to disk
79
99
  await fs.promises.writeFile(outputPath, data);
80
100
  result.successful.push(translationId);
101
+ if (versionId) {
102
+ downloadedVersions.entries[downloadedKey] = {
103
+ versionId,
104
+ fileId: fileId || undefined,
105
+ fileName: inputPath,
106
+ updatedAt: new Date().toISOString(),
107
+ };
108
+ didUpdateDownloadedLock = true;
109
+ }
81
110
  }
82
111
  catch (error) {
83
112
  logError(`Error saving file ${file.id}: ` + error);
@@ -91,6 +120,11 @@ export async function downloadFileBatch(files, options, maxRetries = 3, retryDel
91
120
  result.failed.push(fileId);
92
121
  }
93
122
  }
123
+ // Persist any updates to the downloaded map at the end of a successful cycle
124
+ if (didUpdateDownloadedLock) {
125
+ saveDownloadedVersions(options.configDirectory, downloadedVersions);
126
+ didUpdateDownloadedLock = false;
127
+ }
94
128
  return result;
95
129
  }
96
130
  catch (error) {
@@ -100,6 +134,9 @@ export async function downloadFileBatch(files, options, maxRetries = 3, retryDel
100
134
  error);
101
135
  // Mark all files as failed
102
136
  result.failed = [...fileIds];
137
+ if (didUpdateDownloadedLock) {
138
+ saveDownloadedVersions(options.configDirectory, downloadedVersions);
139
+ }
103
140
  return result;
104
141
  }
105
142
  // Increment retry counter and wait before next attempt
@@ -109,5 +146,8 @@ export async function downloadFileBatch(files, options, maxRetries = 3, retryDel
109
146
  }
110
147
  // Mark all files as failed if we get here
111
148
  result.failed = [...fileIds];
149
+ if (didUpdateDownloadedLock) {
150
+ saveDownloadedVersions(options.configDirectory, downloadedVersions);
151
+ }
112
152
  return result;
113
153
  }
@@ -15,7 +15,7 @@ export async function handleTranslate(options, settings, filesTranslationRespons
15
15
  const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
16
16
  const { data } = filesTranslationResponse;
17
17
  // Check for remaining translations
18
- await checkFileTranslations(data, settings.locales, options.timeout, (sourcePath, locale) => fileMapping[locale][sourcePath] ?? null, settings, options.force);
18
+ await checkFileTranslations(data, settings.locales, options.timeout, (sourcePath, locale) => fileMapping[locale][sourcePath] ?? null, settings, options.force, options.forceDownload);
19
19
  }
20
20
  }
21
21
  // Downloads translations that were originally staged
@@ -33,8 +33,8 @@ export async function handleDownload(options, settings) {
33
33
  const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
34
34
  const stagedVersionData = await getStagedVersions(settings.configDirectory);
35
35
  // Check for remaining translations
36
- await checkFileTranslations(stagedVersionData, settings.locales, options.timeout, (sourcePath, locale) => fileMapping[locale][sourcePath] ?? null, settings, false // force is not applicable for downloading staged translations
37
- );
36
+ await checkFileTranslations(stagedVersionData, settings.locales, options.timeout, (sourcePath, locale) => fileMapping[locale][sourcePath] ?? null, settings, false, // force is not applicable for downloading staged translations
37
+ options.forceDownload);
38
38
  }
39
39
  export async function postProcessTranslations(settings) {
40
40
  // Localize static urls (/docs -> /[locale]/docs) and preserve anchor IDs for non-default locales
package/dist/cli/flags.js CHANGED
@@ -26,6 +26,7 @@ export function attachTranslateFlags(command) {
26
26
  .option('--experimental-flatten-json-files', 'Triggering this will flatten the json files into a single file. This is useful for projects that have a lot of json files.', false)
27
27
  .option('--experimental-localize-static-imports', 'Triggering this will run a script after the cli tool that localizes all static imports in content files. Currently only supported for md and mdx files.', false)
28
28
  .option('--force', 'Force a retranslation, invalidating all existing cached translations if they exist.', false)
29
+ .option('--force-download', 'Force download and overwrite local files, bypassing downloaded-versions checks.', false)
29
30
  .option('--experimental-clear-locale-dirs', 'Clear locale directories before downloading new translations', false);
30
31
  return command;
31
32
  }
@@ -15,5 +15,11 @@ export function resolveConfig(cwd) {
15
15
  config: loadConfig(path.join(cwd, 'src/gt.config.json')),
16
16
  };
17
17
  }
18
+ if (fs.existsSync(path.join(cwd, '.locadex/gt.config.json'))) {
19
+ return {
20
+ path: path.join(cwd, '.locadex/gt.config.json'),
21
+ config: loadConfig(path.join(cwd, '.locadex/gt.config.json')),
22
+ };
23
+ }
18
24
  return null;
19
25
  }
@@ -0,0 +1,12 @@
1
+ export type DownloadedVersionEntry = {
2
+ versionId: string;
3
+ fileId?: string;
4
+ fileName?: string;
5
+ updatedAt?: string;
6
+ };
7
+ export type DownloadedVersions = {
8
+ version: number;
9
+ entries: Record<string, DownloadedVersionEntry>;
10
+ };
11
+ export declare function getDownloadedVersions(configDirectory: string): DownloadedVersions;
12
+ export declare function saveDownloadedVersions(configDirectory: string, lock: DownloadedVersions): void;
@@ -0,0 +1,30 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { logError } from '../../console/logging.js';
4
+ const DOWNLOADED_VERSIONS_FILE = 'downloaded-versions.json';
5
+ export function getDownloadedVersions(configDirectory) {
6
+ try {
7
+ const filepath = path.join(configDirectory, DOWNLOADED_VERSIONS_FILE);
8
+ if (!fs.existsSync(filepath))
9
+ return { version: 1, entries: {} };
10
+ const raw = JSON.parse(fs.readFileSync(filepath, 'utf8'));
11
+ if (raw && typeof raw === 'object' && raw.version && raw.entries) {
12
+ return raw;
13
+ }
14
+ return { version: 1, entries: {} };
15
+ }
16
+ catch (error) {
17
+ logError(`An error occurred while getting downloaded versions: ${error}`);
18
+ return { version: 1, entries: {} };
19
+ }
20
+ }
21
+ export function saveDownloadedVersions(configDirectory, lock) {
22
+ try {
23
+ const filepath = path.join(configDirectory, DOWNLOADED_VERSIONS_FILE);
24
+ fs.mkdirSync(configDirectory, { recursive: true });
25
+ fs.writeFileSync(filepath, JSON.stringify(lock, null, 2));
26
+ }
27
+ catch (error) {
28
+ logError(`An error occurred while updating ${DOWNLOADED_VERSIONS_FILE}: ${error}`);
29
+ }
30
+ }
@@ -1,7 +1,7 @@
1
1
  import chalk from 'chalk';
2
2
  import path from 'node:path';
3
3
  import fs from 'node:fs';
4
- import { logError } from '../console/logging.js';
4
+ import { logWarning, logError } from '../console/logging.js';
5
5
  export function determineLibrary() {
6
6
  let library = 'base';
7
7
  const additionalModules = [];
@@ -11,7 +11,7 @@ export function determineLibrary() {
11
11
  const packageJsonPath = path.join(cwd, 'package.json');
12
12
  // Check if package.json exists
13
13
  if (!fs.existsSync(packageJsonPath)) {
14
- logError(chalk.red('No package.json found in the current directory. Please run this command from the root of your project.'));
14
+ logWarning(chalk.yellow('No package.json found in the current directory. Please run this command from the root of your project.'));
15
15
  return { library: 'base', additionalModules: [] };
16
16
  }
17
17
  // Read and parse package.json
@@ -41,6 +41,7 @@ export type TranslateFlags = {
41
41
  stageTranslations?: boolean;
42
42
  publish?: boolean;
43
43
  force?: boolean;
44
+ forceDownload?: boolean;
44
45
  experimentalLocalizeStaticUrls?: boolean;
45
46
  experimentalHideDefaultLocale?: boolean;
46
47
  experimentalFlattenJsonFiles?: boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "2.3.9",
3
+ "version": "2.3.11",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [