gt 2.10.4 → 2.10.5

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.10.5
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1116](https://github.com/generaltranslation/gt/pull/1116) [`31d7229`](https://github.com/generaltranslation/gt/commit/31d7229e3893b712e2007369e8b3d219bcc9bde8) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Adding v2 of `gt-lock.json`
8
+
3
9
  ## 2.10.4
4
10
 
5
11
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  import * as fs from 'node:fs';
2
2
  import * as path from 'node:path';
3
- import { getDownloadedVersions } from '../fs/config/downloadedVersions.js';
3
+ import { readLockfile, } from '../fs/config/downloadedVersions.js';
4
4
  import { createFileMapping } from '../formats/files/fileMapping.js';
5
5
  import { getGitUnifiedDiff } from '../utils/gitDiff.js';
6
6
  import { gt } from '../utils/gt.js';
@@ -8,26 +8,15 @@ import os from 'node:os';
8
8
  import { randomUUID } from 'node:crypto';
9
9
  import { hashStringSync } from '../utils/hash.js';
10
10
  import { extractJson } from '../formats/json/extractJson.js';
11
- const findLatestDownloadedVersion = (downloadedVersions, branchId, fileId, locale) => {
12
- const versionsForFile = downloadedVersions.entries?.[branchId]?.[fileId] ?? undefined;
13
- if (!versionsForFile)
11
+ import { extractYaml } from '../formats/yaml/extractYaml.js';
12
+ const findLatestDownloadedVersion = (entryMap, fileId, locale) => {
13
+ const entry = entryMap.get(fileId);
14
+ if (!entry)
14
15
  return null;
15
- let latest = null;
16
- for (const [versionId, locales] of Object.entries(versionsForFile)) {
17
- const entry = locales?.[locale];
18
- if (!entry)
19
- continue;
20
- const updatedAt = entry.updatedAt
21
- ? Date.parse(entry.updatedAt)
22
- : Number.NEGATIVE_INFINITY;
23
- const latestUpdatedAt = latest?.entry.updatedAt
24
- ? Date.parse(latest.entry.updatedAt)
25
- : Number.NEGATIVE_INFINITY;
26
- if (!latest || updatedAt > latestUpdatedAt) {
27
- latest = { versionId, entry };
28
- }
29
- }
30
- return latest;
16
+ const translation = entry.translations[locale];
17
+ if (!translation)
18
+ return null;
19
+ return { versionId: entry.versionId, entry: translation };
31
20
  };
32
21
  /**
33
22
  * Collects local user edits by diffing the latest downloaded server translation version
@@ -40,7 +29,7 @@ export async function collectAndSendUserEditDiffs(files, settings) {
40
29
  return;
41
30
  const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
42
31
  const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
43
- const downloadedVersions = getDownloadedVersions(settings.configDirectory);
32
+ const { entryMap } = readLockfile(settings);
44
33
  const tempDir = path.join(os.tmpdir(), randomUUID());
45
34
  if (!fs.existsSync(tempDir))
46
35
  fs.mkdirSync(tempDir, { recursive: true });
@@ -52,7 +41,7 @@ export async function collectAndSendUserEditDiffs(files, settings) {
52
41
  continue;
53
42
  if (!fs.existsSync(outputPath))
54
43
  continue;
55
- const latestDownloaded = findLatestDownloadedVersion(downloadedVersions, uploadedFile.branchId, uploadedFile.fileId, locale);
44
+ const latestDownloaded = findLatestDownloadedVersion(entryMap, uploadedFile.fileId, locale);
56
45
  if (!latestDownloaded)
57
46
  continue;
58
47
  const downloadedVersion = latestDownloaded.entry;
@@ -137,6 +126,14 @@ export async function collectAndSendUserEditDiffs(files, settings) {
137
126
  localContent = extractedContent;
138
127
  }
139
128
  }
129
+ else if ((c.fileName.endsWith('.yaml') || c.fileName.endsWith('.yml')) &&
130
+ settings.options?.yamlSchema &&
131
+ c.locale !== settings.defaultLocale) {
132
+ const extractedContent = extractYaml(rawLocalContent, c.fileName, settings.options);
133
+ if (extractedContent) {
134
+ localContent = extractedContent;
135
+ }
136
+ }
140
137
  collectedDiffs.push({
141
138
  fileName: c.fileName,
142
139
  locale: c.locale,
@@ -5,12 +5,37 @@ import { gt } from '../utils/gt.js';
5
5
  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
+ import { extractJson } from '../formats/json/extractJson.js';
8
9
  import mergeYaml from '../formats/yaml/mergeYaml.js';
9
- import { getDownloadedVersions, saveDownloadedVersions, ensureNestedObject, } from '../fs/config/downloadedVersions.js';
10
+ import { extractYaml } from '../formats/yaml/extractYaml.js';
11
+ import { readLockfile, writeLockfile, findOrCreateEntry, } from '../fs/config/downloadedVersions.js';
10
12
  import { recordDownloaded } from '../state/recentDownloads.js';
11
13
  import { recordWarning } from '../state/translateWarnings.js';
12
- import { hashStringSync } from '../utils/hash.js';
13
14
  import stringify from 'fast-json-stable-stringify';
15
+ /**
16
+ * Merges translated content with the current source file for schema-based formats.
17
+ */
18
+ function mergeWithSource(translatedContent, locale, inputPath, options) {
19
+ if (!options.options)
20
+ return translatedContent;
21
+ const jsonSchema = options.options.jsonSchema
22
+ ? validateJsonSchema(options.options, inputPath)
23
+ : null;
24
+ const yamlSchema = !jsonSchema && options.options.yamlSchema
25
+ ? validateYamlSchema(options.options, inputPath)
26
+ : null;
27
+ if (!jsonSchema && !yamlSchema)
28
+ return translatedContent;
29
+ const sourceContent = fs.readFileSync(inputPath, 'utf8');
30
+ if (!sourceContent)
31
+ return translatedContent;
32
+ if (jsonSchema) {
33
+ return mergeJson(sourceContent, inputPath, options.options, [{ translatedContent, targetLocale: locale }], options.defaultLocale, options.locales)[0];
34
+ }
35
+ else {
36
+ return mergeYaml(sourceContent, inputPath, options.options, [{ translatedContent, targetLocale: locale }], options.defaultLocale)[0];
37
+ }
38
+ }
14
39
  /**
15
40
  * Downloads multiple translation files in a single batch request
16
41
  * @param files - Array of files to download with their output paths
@@ -20,7 +45,7 @@ import stringify from 'fast-json-stable-stringify';
20
45
  */
21
46
  export async function downloadFileBatch(fileTracker, files, options, forceDownload = false) {
22
47
  // Local record of what version was last downloaded for each fileName:locale
23
- const downloadedVersions = getDownloadedVersions(options.configDirectory);
48
+ const { data: downloadedVersions, entryMap, originalV1, } = readLockfile(options);
24
49
  let didUpdateDownloadedLock = false;
25
50
  // Create a map of requested file keys to the file object
26
51
  const requestedFileMap = new Map(files.map((file) => [
@@ -69,58 +94,37 @@ export async function downloadFileBatch(fileTracker, files, options, forceDownlo
69
94
  fs.mkdirSync(dir, { recursive: true });
70
95
  }
71
96
  // If a local translation already exists for the same source version, skip overwrite
72
- const downloadedVersion = downloadedVersions.entries[branchId]?.[fileId]?.[versionId]?.[locale];
97
+ const existingEntry = entryMap.get(fileId);
98
+ const downloadedTranslation = existingEntry?.versionId === versionId
99
+ ? existingEntry.translations[locale]
100
+ : undefined;
73
101
  const fileExists = fs.existsSync(outputPath);
74
- let sourceChanged = false;
75
- if (downloadedVersion?.sourceHash) {
102
+ if (!forceDownload && fileExists && downloadedTranslation) {
103
+ // For schema-based files, re-merge with current source in case
104
+ // non-translatable fields changed (skip the API download, not the merge)
76
105
  try {
77
- const currentSourceContent = fs.readFileSync(inputPath, 'utf8');
78
- const currentSourceHash = hashStringSync(currentSourceContent);
79
- sourceChanged = currentSourceHash !== downloadedVersion.sourceHash;
106
+ const existingContent = fs.readFileSync(outputPath, 'utf8');
107
+ const jsonExtracted = options.options?.jsonSchema
108
+ ? extractJson(existingContent, inputPath, options.options, locale, options.defaultLocale)
109
+ : null;
110
+ const extracted = jsonExtracted ??
111
+ (options.options?.yamlSchema
112
+ ? extractYaml(existingContent, inputPath, options.options)
113
+ : null);
114
+ if (extracted) {
115
+ const remerged = mergeWithSource(extracted, locale, inputPath, options);
116
+ if (remerged !== existingContent) {
117
+ await fs.promises.writeFile(outputPath, remerged);
118
+ }
119
+ }
80
120
  }
81
121
  catch {
82
- sourceChanged = true;
122
+ // If re-merge fails, still count as skipped — not worth failing the download
83
123
  }
84
- }
85
- if (!forceDownload &&
86
- !sourceChanged &&
87
- fileExists &&
88
- downloadedVersion) {
89
124
  result.skipped.push(requestedFile);
90
125
  continue;
91
126
  }
92
- let data = file.data;
93
- let sourceContentHash;
94
- if (options.options?.jsonSchema && locale) {
95
- const jsonSchema = validateJsonSchema(options.options, inputPath);
96
- if (jsonSchema) {
97
- const originalContent = fs.readFileSync(inputPath, 'utf8');
98
- if (originalContent) {
99
- sourceContentHash = hashStringSync(originalContent);
100
- data = mergeJson(originalContent, inputPath, options.options, [
101
- {
102
- translatedContent: file.data,
103
- targetLocale: locale,
104
- },
105
- ], options.defaultLocale, options.locales)[0];
106
- }
107
- }
108
- }
109
- if (options.options?.yamlSchema && locale) {
110
- const yamlSchema = validateYamlSchema(options.options, inputPath);
111
- if (yamlSchema) {
112
- const originalContent = fs.readFileSync(inputPath, 'utf8');
113
- if (originalContent) {
114
- sourceContentHash = hashStringSync(originalContent);
115
- data = mergeYaml(originalContent, inputPath, options.options, [
116
- {
117
- translatedContent: file.data,
118
- targetLocale: locale,
119
- },
120
- ], options.defaultLocale)[0];
121
- }
122
- }
123
- }
127
+ let data = mergeWithSource(file.data, locale, inputPath, options);
124
128
  // If the file is a GTJSON file, stable sort the order and format the data
125
129
  if (file.fileFormat === 'GTJSON') {
126
130
  try {
@@ -144,16 +148,12 @@ export async function downloadFileBatch(fileTracker, files, options, forceDownlo
144
148
  inputPath,
145
149
  });
146
150
  result.successful.push(requestedFile);
147
- if (branchId && fileId && versionId && locale) {
148
- ensureNestedObject(downloadedVersions.entries, [
149
- branchId,
150
- fileId,
151
- versionId,
152
- locale,
153
- ]);
154
- downloadedVersions.entries[branchId][fileId][versionId][locale] = {
151
+ if (fileId && versionId && locale) {
152
+ const entry = findOrCreateEntry(entryMap, downloadedVersions.entries, fileId, versionId);
153
+ entry.fileName = inputPath;
154
+ entry.translations[locale] = {
155
155
  updatedAt: new Date().toISOString(),
156
- ...(sourceContentHash ? { sourceHash: sourceContentHash } : {}),
156
+ fileName: outputPath,
157
157
  };
158
158
  didUpdateDownloadedLock = true;
159
159
  }
@@ -173,7 +173,7 @@ export async function downloadFileBatch(fileTracker, files, options, forceDownlo
173
173
  }
174
174
  // Persist any updates to the downloaded map at the end of a successful cycle
175
175
  if (didUpdateDownloadedLock) {
176
- saveDownloadedVersions(options.configDirectory, downloadedVersions);
176
+ writeLockfile(downloadedVersions, originalV1);
177
177
  didUpdateDownloadedLock = false;
178
178
  }
179
179
  return result;
@@ -184,7 +184,7 @@ export async function downloadFileBatch(fileTracker, files, options, forceDownlo
184
184
  // Mark all files as failed if we get here
185
185
  result.failed = [...requestedFileMap.values()];
186
186
  if (didUpdateDownloadedLock) {
187
- saveDownloadedVersions(options.configDirectory, downloadedVersions);
187
+ writeLockfile(downloadedVersions, originalV1);
188
188
  }
189
189
  return result;
190
190
  }
@@ -0,0 +1,13 @@
1
+ import { AdditionalOptions } from '../../types/index.js';
2
+ /**
3
+ * Extracts translated values from a fully merged YAML file back into the
4
+ * flattened JSON pointer format that mergeYaml expects.
5
+ * This is the inverse of mergeYaml — it takes a merged YAML document and
6
+ * extracts only the values at the schema's include paths.
7
+ *
8
+ * @param localContent - The full YAML content from the user's local file
9
+ * @param inputPath - The path to the file (used for matching yamlSchema)
10
+ * @param options - Additional options containing yamlSchema config
11
+ * @returns The flattened JSON string of translatable values, or null if no extraction needed
12
+ */
13
+ export declare function extractYaml(localContent: string, inputPath: string, options: AdditionalOptions): string | null;
@@ -0,0 +1,33 @@
1
+ import { logger } from '../../console/logger.js';
2
+ import { validateYamlSchema } from './utils.js';
3
+ import { flattenJsonWithStringFilter } from '../json/flattenJson.js';
4
+ import YAML from 'yaml';
5
+ /**
6
+ * Extracts translated values from a fully merged YAML file back into the
7
+ * flattened JSON pointer format that mergeYaml expects.
8
+ * This is the inverse of mergeYaml — it takes a merged YAML document and
9
+ * extracts only the values at the schema's include paths.
10
+ *
11
+ * @param localContent - The full YAML content from the user's local file
12
+ * @param inputPath - The path to the file (used for matching yamlSchema)
13
+ * @param options - Additional options containing yamlSchema config
14
+ * @returns The flattened JSON string of translatable values, or null if no extraction needed
15
+ */
16
+ export function extractYaml(localContent, inputPath, options) {
17
+ const yamlSchema = validateYamlSchema(options, inputPath);
18
+ if (!yamlSchema || !yamlSchema.include) {
19
+ return null;
20
+ }
21
+ let yaml;
22
+ try {
23
+ yaml = YAML.parse(localContent);
24
+ }
25
+ catch {
26
+ logger.error(`Invalid YAML file: ${inputPath}`);
27
+ return null;
28
+ }
29
+ const extracted = flattenJsonWithStringFilter(yaml, yamlSchema.include);
30
+ if (!Object.keys(extracted).length)
31
+ return null;
32
+ return JSON.stringify(extracted);
33
+ }
@@ -1,21 +1,60 @@
1
+ import type { Settings } from '../../types/index.js';
2
+ export type DownloadedTranslation = {
3
+ updatedAt?: string;
4
+ postProcessHash?: string;
5
+ fileName?: string;
6
+ };
1
7
  export type DownloadedVersionEntry = {
8
+ fileId: string;
9
+ versionId: string;
10
+ fileName?: string;
11
+ translations: {
12
+ [locale: string]: DownloadedTranslation;
13
+ };
14
+ };
15
+ export type DownloadedVersions = {
16
+ version: 2;
17
+ branchId: string;
18
+ entries: DownloadedVersionEntry[];
19
+ };
20
+ export type DownloadedVersionEntryV1 = {
2
21
  fileName?: string;
3
22
  updatedAt?: string;
4
23
  postProcessHash?: string;
5
24
  sourceHash?: string;
6
25
  };
7
- export type DownloadedVersions = {
26
+ export type DownloadedVersionsV1 = {
8
27
  version: number;
9
28
  entries: {
10
29
  [branchId: string]: {
11
30
  [fileId: string]: {
12
31
  [versionId: string]: {
13
- [locale: string]: DownloadedVersionEntry;
32
+ [locale: string]: DownloadedVersionEntryV1;
14
33
  };
15
34
  };
16
35
  };
17
36
  };
18
37
  };
19
- export declare function getDownloadedVersions(configDirectory: string): DownloadedVersions;
20
- export declare function saveDownloadedVersions(configDirectory: string, lock: DownloadedVersions): void;
21
- export declare function ensureNestedObject(obj: any, path: string[]): any;
38
+ /**
39
+ * Reads the lockfile and returns v2 data regardless of the on-disk format.
40
+ * If the file is v1, `originalV1` contains the full v1 data so that
41
+ * `writeLockfile` can merge changes back without losing other branches.
42
+ */
43
+ export declare function readLockfile(settings: Settings): {
44
+ data: DownloadedVersions;
45
+ entryMap: EntryMap;
46
+ originalV1: DownloadedVersionsV1 | null;
47
+ };
48
+ /**
49
+ * Writes the lockfile. If `originalV1` is provided, merges the current
50
+ * branch's data back into the v1 structure (preserving other branches)
51
+ * and writes v1 format. Otherwise writes v2.
52
+ */
53
+ export declare function writeLockfile(data: DownloadedVersions, originalV1: DownloadedVersionsV1 | null): void;
54
+ export type EntryMap = Map<string, DownloadedVersionEntry>;
55
+ export declare function buildEntryMap(entries: DownloadedVersionEntry[]): EntryMap;
56
+ /**
57
+ * Finds or creates an entry, keeping the map and backing array in sync.
58
+ * If the fileId exists but versionId changed, replaces it in-place.
59
+ */
60
+ export declare function findOrCreateEntry(entryMap: EntryMap, entries: DownloadedVersionEntry[], fileId: string, versionId: string): DownloadedVersionEntry;
@@ -1,50 +1,167 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
3
  import { logger } from '../../console/logger.js';
4
- // New lock file name, use old name for deletion of legacy lock file
5
4
  const GT_LOCK_FILE = 'gt-lock.json';
6
- const LEGACY_DOWNLOADED_VERSIONS_FILE = 'downloaded-versions.json';
7
- export function getDownloadedVersions(configDirectory) {
5
+ // ── Conversion helpers ──────────────────────────────────────────────
6
+ function convertV1ToV2(v1, branchId) {
7
+ const branchEntries = v1.entries?.[branchId];
8
+ if (!branchEntries) {
9
+ return { version: 2, branchId, entries: [] };
10
+ }
11
+ const entries = [];
12
+ for (const [fileId, versions] of Object.entries(branchEntries)) {
13
+ const versionIds = Object.keys(versions);
14
+ if (versionIds.length === 0)
15
+ continue;
16
+ // Pick the versionId with the most recent updatedAt, defaulting to the first
17
+ let latestVersionId = versionIds[0];
18
+ let latestTime = 0;
19
+ for (const [versionId, locales] of Object.entries(versions)) {
20
+ for (const entry of Object.values(locales)) {
21
+ const t = entry.updatedAt ? Date.parse(entry.updatedAt) : 0;
22
+ if (t > latestTime) {
23
+ latestTime = t;
24
+ latestVersionId = versionId;
25
+ }
26
+ }
27
+ }
28
+ const localeEntries = versions[latestVersionId];
29
+ const translations = {};
30
+ for (const [locale, entry] of Object.entries(localeEntries)) {
31
+ translations[locale] = {
32
+ ...(entry.updatedAt ? { updatedAt: entry.updatedAt } : {}),
33
+ ...(entry.postProcessHash
34
+ ? { postProcessHash: entry.postProcessHash }
35
+ : {}),
36
+ };
37
+ }
38
+ entries.push({
39
+ fileId,
40
+ versionId: latestVersionId,
41
+ translations,
42
+ });
43
+ }
44
+ return { version: 2, branchId, entries };
45
+ }
46
+ function convertV2ToV1Branch(v2) {
47
+ const branch = {};
48
+ for (const entry of v2.entries) {
49
+ if (!branch[entry.fileId]) {
50
+ branch[entry.fileId] = {};
51
+ }
52
+ if (!branch[entry.fileId][entry.versionId]) {
53
+ branch[entry.fileId][entry.versionId] = {};
54
+ }
55
+ for (const [locale, translation] of Object.entries(entry.translations)) {
56
+ branch[entry.fileId][entry.versionId][locale] = {
57
+ ...(translation.updatedAt ? { updatedAt: translation.updatedAt } : {}),
58
+ ...(translation.postProcessHash
59
+ ? { postProcessHash: translation.postProcessHash }
60
+ : {}),
61
+ };
62
+ }
63
+ }
64
+ return branch;
65
+ }
66
+ // ── Public API ──────────────────────────────────────────────────────
67
+ /**
68
+ * Reads the lockfile and returns v2 data regardless of the on-disk format.
69
+ * If the file is v1, `originalV1` contains the full v1 data so that
70
+ * `writeLockfile` can merge changes back without losing other branches.
71
+ */
72
+ export function readLockfile(settings) {
73
+ let branchId = settings._branchId ?? '';
74
+ let data;
75
+ let originalV1 = null;
8
76
  try {
9
- // Clean up legacy lock files inside the config directory
10
77
  const rootPath = path.join(process.cwd(), GT_LOCK_FILE);
11
- const legacyPath = path.join(configDirectory, LEGACY_DOWNLOADED_VERSIONS_FILE);
12
- try {
13
- if (fs.existsSync(legacyPath)) {
14
- fs.unlinkSync(legacyPath);
15
- }
78
+ if (!fs.existsSync(rootPath)) {
79
+ data = { version: 2, branchId, entries: [] };
16
80
  }
17
- catch { }
18
- const filepath = fs.existsSync(rootPath) ? rootPath : null;
19
- if (!filepath)
20
- return { version: 1, entries: {} };
21
- const raw = JSON.parse(fs.readFileSync(filepath, 'utf8'));
22
- if (raw && typeof raw === 'object' && raw.version && raw.entries) {
23
- return raw;
81
+ else {
82
+ const raw = JSON.parse(fs.readFileSync(rootPath, 'utf8'));
83
+ if (!raw || typeof raw !== 'object' || !raw.entries) {
84
+ data = { version: 2, branchId, entries: [] };
85
+ }
86
+ else if (raw.version === 2 && Array.isArray(raw.entries)) {
87
+ data = raw;
88
+ if (branchId)
89
+ data.branchId = branchId;
90
+ }
91
+ else {
92
+ originalV1 = raw;
93
+ if (!branchId) {
94
+ const branches = Object.keys(originalV1.entries);
95
+ if (branches.length > 0)
96
+ branchId = branches[0];
97
+ }
98
+ data = convertV1ToV2(originalV1, branchId);
99
+ }
24
100
  }
25
- return { version: 1, entries: {} };
26
101
  }
27
102
  catch (error) {
28
- logger.error(`An error occurred while getting downloaded versions: ${error}`);
29
- return { version: 1, entries: {} };
103
+ logger.error(`An error occurred while reading ${GT_LOCK_FILE}: ${error}`);
104
+ data = { version: 2, branchId, entries: [] };
30
105
  }
106
+ return { data, entryMap: buildEntryMap(data.entries), originalV1 };
31
107
  }
32
- export function saveDownloadedVersions(configDirectory, lock) {
108
+ /**
109
+ * Writes the lockfile. If `originalV1` is provided, merges the current
110
+ * branch's data back into the v1 structure (preserving other branches)
111
+ * and writes v1 format. Otherwise writes v2.
112
+ */
113
+ export function writeLockfile(data, originalV1) {
33
114
  try {
34
- // Write the lock file to the repo root
35
115
  const filepath = path.join(process.cwd(), GT_LOCK_FILE);
36
116
  fs.mkdirSync(path.dirname(filepath), { recursive: true });
37
- fs.writeFileSync(filepath, JSON.stringify(lock, null, 2));
117
+ if (originalV1) {
118
+ const mergedV1 = {
119
+ ...originalV1,
120
+ entries: {
121
+ ...originalV1.entries,
122
+ [data.branchId]: convertV2ToV1Branch(data),
123
+ },
124
+ };
125
+ fs.writeFileSync(filepath, JSON.stringify(mergedV1, null, 2));
126
+ }
127
+ else {
128
+ fs.writeFileSync(filepath, JSON.stringify(data, null, 2));
129
+ }
38
130
  }
39
131
  catch (error) {
40
132
  logger.error(`An error occurred while updating ${GT_LOCK_FILE}: ${error}`);
41
133
  }
42
134
  }
43
- export function ensureNestedObject(obj, path) {
44
- return path.reduce((current, key, index) => {
45
- if (index === path.length - 1)
46
- return current;
47
- current[key] = current[key] || {};
48
- return current[key];
49
- }, obj);
135
+ export function buildEntryMap(entries) {
136
+ return new Map(entries.map((e) => [e.fileId, e]));
137
+ }
138
+ /**
139
+ * Finds or creates an entry, keeping the map and backing array in sync.
140
+ * If the fileId exists but versionId changed, replaces it in-place.
141
+ */
142
+ export function findOrCreateEntry(entryMap, entries, fileId, versionId) {
143
+ const existing = entryMap.get(fileId);
144
+ if (existing) {
145
+ if (existing.versionId === versionId)
146
+ return existing;
147
+ // Version changed — replace in array and map
148
+ const updated = {
149
+ fileId,
150
+ versionId,
151
+ translations: {},
152
+ };
153
+ const idx = entries.indexOf(existing);
154
+ if (idx !== -1)
155
+ entries[idx] = updated;
156
+ entryMap.set(fileId, updated);
157
+ return updated;
158
+ }
159
+ const entry = {
160
+ fileId,
161
+ versionId,
162
+ translations: {},
163
+ };
164
+ entries.push(entry);
165
+ entryMap.set(fileId, entry);
166
+ return entry;
50
167
  }
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.10.4";
1
+ export declare const PACKAGE_VERSION = "2.10.5";
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const PACKAGE_VERSION = '2.10.4';
2
+ export const PACKAGE_VERSION = '2.10.5';
@@ -162,6 +162,7 @@ export type Settings = {
162
162
  stageTranslations: boolean;
163
163
  publish: boolean;
164
164
  _versionId?: string;
165
+ _branchId?: string;
165
166
  version?: string;
166
167
  description?: string;
167
168
  src?: string[];
@@ -1,5 +1,5 @@
1
1
  import * as fs from 'node:fs';
2
- import { ensureNestedObject, getDownloadedVersions, saveDownloadedVersions, } from '../fs/config/downloadedVersions.js';
2
+ import { findOrCreateEntry, readLockfile, writeLockfile, } from '../fs/config/downloadedVersions.js';
3
3
  import { hashStringSync } from './hash.js';
4
4
  /**
5
5
  * Persist postprocessed content hashes for recently downloaded files into gt-lock.json.
@@ -8,7 +8,7 @@ export function persistPostProcessHashes(settings, includeFiles, downloadedMeta)
8
8
  if (!includeFiles || includeFiles.size === 0 || downloadedMeta.size === 0) {
9
9
  return;
10
10
  }
11
- const downloadedVersions = getDownloadedVersions(settings.configDirectory);
11
+ const { data, entryMap, originalV1 } = readLockfile(settings);
12
12
  let lockUpdated = false;
13
13
  for (const filePath of includeFiles) {
14
14
  const meta = downloadedMeta.get(filePath);
@@ -18,15 +18,10 @@ export function persistPostProcessHashes(settings, includeFiles, downloadedMeta)
18
18
  continue;
19
19
  const content = fs.readFileSync(filePath, 'utf8');
20
20
  const hash = hashStringSync(content);
21
- ensureNestedObject(downloadedVersions.entries, [
22
- meta.branchId,
23
- meta.fileId,
24
- meta.versionId,
25
- meta.locale,
26
- ]);
27
- const existing = downloadedVersions.entries[meta.branchId][meta.fileId][meta.versionId][meta.locale] || {};
21
+ const entry = findOrCreateEntry(entryMap, data.entries, meta.fileId, meta.versionId);
22
+ const existing = entry.translations[meta.locale] || {};
28
23
  if (existing.postProcessHash !== hash) {
29
- downloadedVersions.entries[meta.branchId][meta.fileId][meta.versionId][meta.locale] = {
24
+ entry.translations[meta.locale] = {
30
25
  ...existing,
31
26
  postProcessHash: hash,
32
27
  };
@@ -34,6 +29,6 @@ export function persistPostProcessHashes(settings, includeFiles, downloadedMeta)
34
29
  }
35
30
  }
36
31
  if (lockUpdated) {
37
- saveDownloadedVersions(settings.configDirectory, downloadedVersions);
32
+ writeLockfile(data, originalV1);
38
33
  }
39
34
  }
@@ -4,7 +4,8 @@ import { Settings } from '../../types/index.js';
4
4
  export declare class UserEditDiffsStep extends WorkflowStep<FileReference[], FileReference[]> {
5
5
  private settings;
6
6
  private spinner;
7
- private completed;
7
+ private succeeded;
8
+ private failed;
8
9
  constructor(settings: Settings);
9
10
  run(files: FileReference[]): Promise<FileReference[]>;
10
11
  wait(): Promise<void>;
@@ -5,7 +5,8 @@ import { collectAndSendUserEditDiffs } from '../../api/collectUserEditDiffs.js';
5
5
  export class UserEditDiffsStep extends WorkflowStep {
6
6
  settings;
7
7
  spinner = logger.createSpinner('dots');
8
- completed = false;
8
+ succeeded = false;
9
+ failed = false;
9
10
  constructor(settings) {
10
11
  super();
11
12
  this.settings = settings;
@@ -14,17 +15,20 @@ export class UserEditDiffsStep extends WorkflowStep {
14
15
  this.spinner.start('Updating translations...');
15
16
  try {
16
17
  await collectAndSendUserEditDiffs(files, this.settings);
17
- this.completed = true;
18
+ this.succeeded = true;
18
19
  }
19
20
  catch {
20
21
  // Non-fatal; keep going to enqueue
21
- this.completed = true;
22
+ this.failed = true;
22
23
  }
23
24
  return files;
24
25
  }
25
26
  async wait() {
26
- if (this.completed) {
27
+ if (this.succeeded) {
27
28
  this.spinner.stop(chalk.green('Updated translations'));
28
29
  }
30
+ else if (this.failed) {
31
+ this.spinner.stop(chalk.yellow('Could not update translations'));
32
+ }
29
33
  }
30
34
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gt",
3
- "version": "2.10.4",
3
+ "version": "2.10.5",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [