gtx-cli 2.6.23 → 2.6.24

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,11 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.6.24
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1036](https://github.com/generaltranslation/gt/pull/1036) [`5fc08d0`](https://github.com/generaltranslation/gt/commit/5fc08d0028b7936b5916a048786bedc3b13e0042) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Fix: Update composite JSONs when non-translatable content changes, fix bug where `save-local` update composite key index
8
+
3
9
  ## 2.6.23
4
10
 
5
11
  ### Patch Changes
@@ -9,6 +9,7 @@ import mergeYaml from '../formats/yaml/mergeYaml.js';
9
9
  import { getDownloadedVersions, saveDownloadedVersions, ensureNestedObject, } from '../fs/config/downloadedVersions.js';
10
10
  import { recordDownloaded } from '../state/recentDownloads.js';
11
11
  import { recordWarning } from '../state/translateWarnings.js';
12
+ import { hashStringSync } from '../utils/hash.js';
12
13
  import stringify from 'fast-json-stable-stringify';
13
14
  /**
14
15
  * Downloads multiple translation files in a single batch request
@@ -70,16 +71,32 @@ export async function downloadFileBatch(fileTracker, files, options, forceDownlo
70
71
  // If a local translation already exists for the same source version, skip overwrite
71
72
  const downloadedVersion = downloadedVersions.entries[branchId]?.[fileId]?.[versionId]?.[locale];
72
73
  const fileExists = fs.existsSync(outputPath);
73
- if (!forceDownload && fileExists && downloadedVersion) {
74
+ let sourceChanged = false;
75
+ if (downloadedVersion?.sourceHash) {
76
+ try {
77
+ const currentSourceContent = fs.readFileSync(inputPath, 'utf8');
78
+ const currentSourceHash = hashStringSync(currentSourceContent);
79
+ sourceChanged = currentSourceHash !== downloadedVersion.sourceHash;
80
+ }
81
+ catch {
82
+ sourceChanged = true;
83
+ }
84
+ }
85
+ if (!forceDownload &&
86
+ !sourceChanged &&
87
+ fileExists &&
88
+ downloadedVersion) {
74
89
  result.skipped.push(requestedFile);
75
90
  continue;
76
91
  }
77
92
  let data = file.data;
93
+ let sourceContentHash;
78
94
  if (options.options?.jsonSchema && locale) {
79
95
  const jsonSchema = validateJsonSchema(options.options, inputPath);
80
96
  if (jsonSchema) {
81
97
  const originalContent = fs.readFileSync(inputPath, 'utf8');
82
98
  if (originalContent) {
99
+ sourceContentHash = hashStringSync(originalContent);
83
100
  data = mergeJson(originalContent, inputPath, options.options, [
84
101
  {
85
102
  translatedContent: file.data,
@@ -94,6 +111,7 @@ export async function downloadFileBatch(fileTracker, files, options, forceDownlo
94
111
  if (yamlSchema) {
95
112
  const originalContent = fs.readFileSync(inputPath, 'utf8');
96
113
  if (originalContent) {
114
+ sourceContentHash = hashStringSync(originalContent);
97
115
  data = mergeYaml(originalContent, inputPath, options.options, [
98
116
  {
99
117
  translatedContent: file.data,
@@ -135,6 +153,7 @@ export async function downloadFileBatch(fileTracker, files, options, forceDownlo
135
153
  ]);
136
154
  downloadedVersions.entries[branchId][fileId][versionId][locale] = {
137
155
  updatedAt: new Date().toISOString(),
156
+ ...(sourceContentHash ? { sourceHash: sourceContentHash } : {}),
138
157
  };
139
158
  didUpdateDownloadedLock = true;
140
159
  }
@@ -33,6 +33,9 @@ export function extractJson(localContent, inputPath, options, targetLocale, defa
33
33
  const canonicalTargetLocale = useCanonicalLocaleKeys
34
34
  ? gt.resolveCanonicalLocale(targetLocale)
35
35
  : targetLocale;
36
+ const canonicalDefaultLocale = useCanonicalLocaleKeys
37
+ ? gt.resolveCanonicalLocale(defaultLocale)
38
+ : defaultLocale;
36
39
  // Handle include-style schemas (simple path-based extraction)
37
40
  if (jsonSchema.include) {
38
41
  const extracted = flattenJsonWithStringFilter(localJson, jsonSchema.include);
@@ -58,16 +61,22 @@ export function extractJson(localContent, inputPath, options, targetLocale, defa
58
61
  logger.warn(`No matching items found for locale ${targetLocale} at path: ${sourceObjectPointer}`);
59
62
  continue;
60
63
  }
64
+ // Also find default locale items
65
+ const matchingDefaultLocaleItems = findMatchingItemArray(canonicalDefaultLocale, sourceObjectOptions, sourceObjectPointer, sourceObjectValue);
66
+ const defaultKeys = Object.keys(matchingDefaultLocaleItems);
67
+ const targetEntries = Object.entries(matchingTargetLocaleItems);
61
68
  // Initialize the nested structure for this source object pointer
62
69
  if (!compositeResult[sourceObjectPointer]) {
63
70
  compositeResult[sourceObjectPointer] = {};
64
71
  }
65
- // For each matching item, extract the included values
66
- for (const [itemPointer, { sourceItem }] of Object.entries(matchingTargetLocaleItems)) {
72
+ // For each target item, use the default locale's key position
73
+ for (let i = 0; i < targetEntries.length; i++) {
74
+ const [, { sourceItem }] = targetEntries[i];
67
75
  // Extract values at the include paths
68
76
  const extractedValues = flattenJsonWithStringFilter(sourceItem, sourceObjectOptions.include);
69
- // Store under the item pointer (e.g., "/0")
70
- compositeResult[sourceObjectPointer][itemPointer] = extractedValues;
77
+ // Use default locale key
78
+ const outputKey = i < defaultKeys.length ? defaultKeys[i] : targetEntries[i][0];
79
+ compositeResult[sourceObjectPointer][outputKey] = extractedValues;
71
80
  }
72
81
  }
73
82
  else {
@@ -100,10 +100,25 @@ export function mergeJson(originalContent, inputPath, options, targets, defaultL
100
100
  ? gt.resolveCanonicalLocale(target.targetLocale)
101
101
  : target.targetLocale, sourceObjectOptions, sourceObjectPointer, sourceObjectValue);
102
102
  Object.values(targetItemsToRemove).forEach(({ index }) => indiciesToRemove.add(index));
103
- // 3. Merge matchingDefaultLocaleItems and targetItems
103
+ // Remap mismatched positional keys to current source positions
104
+ const sourceKeys = [...matchingDefaultLocaleItemKeys];
105
+ const remappedTargetItems = {};
106
+ for (const [key, value] of Object.entries(targetItems)) {
107
+ if (matchingDefaultLocaleItemKeys.has(key)) {
108
+ remappedTargetItems[key] = value;
109
+ }
110
+ else if (sourceKeys.length === 1 &&
111
+ !(sourceKeys[0] in remappedTargetItems)) {
112
+ remappedTargetItems[sourceKeys[0]] = value;
113
+ }
114
+ else {
115
+ logger.warn(`Skipping translated item at ${key}: cannot map to source item at path ${sourceObjectPointer}`);
116
+ }
117
+ }
118
+ // Merge matchingDefaultLocaleItems and remapped targetItems
104
119
  const mergedItems = {
105
120
  ...(sourceObjectOptions.transform ? matchingDefaultLocaleItems : {}),
106
- ...targetItems,
121
+ ...remappedTargetItems,
107
122
  };
108
123
  // 4. Validate that the mergedItems is not empty
109
124
  if (Object.keys(mergedItems).length === 0) {
@@ -113,8 +128,8 @@ export function mergeJson(originalContent, inputPath, options, targets, defaultL
113
128
  for (const [sourceItemPointer, targetItem] of Object.entries(mergedItems)) {
114
129
  // 5. Validate that all the array indecies are still present in the source json
115
130
  if (!matchingDefaultLocaleItemKeys.has(sourceItemPointer)) {
116
- logger.error(`Array index ${sourceItemPointer} is not present in the source json. It is possible that the source json has been modified since the translation was generated.`);
117
- return exitSync(1);
131
+ logger.warn(`Skipping translated item at ${sourceItemPointer}: not present in source json at path ${sourceObjectPointer}`);
132
+ continue;
118
133
  }
119
134
  // 6. Override the source item with the translated values
120
135
  const defaultLocaleSourceItem = matchingDefaultLocaleItems[sourceItemPointer].sourceItem;
@@ -145,10 +160,9 @@ export function mergeJson(originalContent, inputPath, options, targets, defaultL
145
160
  itemsToAdd.push(mutatedSourceItem);
146
161
  }
147
162
  }
148
- // 8. Check that items to add is >= items to remove (if this happens, something is very wrong)
163
+ // 8. Check that items to add is >= items to remove
149
164
  if (itemsToAdd.length < indiciesToRemove.size) {
150
- logger.error(`Items to add is less than items to remove at path: ${sourceObjectPointer}. Please check your JSON schema key field.`);
151
- return exitSync(1);
165
+ logger.warn(`Items to add (${itemsToAdd.length}) is less than items to remove (${indiciesToRemove.size}) at path: ${sourceObjectPointer}. Some translated items may have been skipped.`);
152
166
  }
153
167
  // 9. Remove all items for the target locale (they can be identified by the key)
154
168
  const filteredSourceObjectValue = sourceObjectValue.filter((_, index) => !indiciesToRemove.has(index));
@@ -2,6 +2,7 @@ export type DownloadedVersionEntry = {
2
2
  fileName?: string;
3
3
  updatedAt?: string;
4
4
  postProcessHash?: string;
5
+ sourceHash?: string;
5
6
  };
6
7
  export type DownloadedVersions = {
7
8
  version: number;
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.6.23";
1
+ export declare const PACKAGE_VERSION = "2.6.24";
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const PACKAGE_VERSION = '2.6.23';
2
+ export const PACKAGE_VERSION = '2.6.24';
@@ -57,6 +57,7 @@ export async function downloadTranslations(fileVersionData, jobData, branchData,
57
57
  skipped: new Map(),
58
58
  };
59
59
  // Step 1: Poll translation jobs if jobData exists
60
+ let pollTimedOut = false;
60
61
  if (jobData) {
61
62
  const pollStep = new PollTranslationJobsStep(gt);
62
63
  const pollResult = await pollStep.run({
@@ -75,8 +76,15 @@ export async function downloadTranslations(fileVersionData, jobData, branchData,
75
76
  recordWarning('failed_translation', value.fileName, `Failed to translate for locale ${value.locale}`);
76
77
  }
77
78
  }
79
+ // Even if polling timed out, still download whatever completed successfully
78
80
  if (!pollResult.success) {
79
- return false;
81
+ pollTimedOut = true;
82
+ if (pollResult.fileTracker.completed.size > 0) {
83
+ logger.warn(chalk.yellow(`Timed out, but ${pollResult.fileTracker.completed.size} translation(s) completed successfully. Downloading completed files...`));
84
+ }
85
+ else {
86
+ return false;
87
+ }
80
88
  }
81
89
  }
82
90
  else {
@@ -93,6 +101,10 @@ export async function downloadTranslations(fileVersionData, jobData, branchData,
93
101
  forceDownload,
94
102
  });
95
103
  await downloadStep.wait();
104
+ // If polling timed out, report failure even though we downloaded what we could
105
+ if (pollTimedOut) {
106
+ return false;
107
+ }
96
108
  return downloadResult;
97
109
  }
98
110
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "2.6.23",
3
+ "version": "2.6.24",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [