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 +6 -0
- package/dist/api/collectUserEditDiffs.js +19 -22
- package/dist/api/downloadFileBatch.js +58 -58
- package/dist/formats/yaml/extractYaml.d.ts +13 -0
- package/dist/formats/yaml/extractYaml.js +33 -0
- package/dist/fs/config/downloadedVersions.d.ts +44 -5
- package/dist/fs/config/downloadedVersions.js +146 -29
- package/dist/generated/version.d.ts +1 -1
- package/dist/generated/version.js +1 -1
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/persistPostprocessHashes.js +6 -11
- package/dist/workflows/steps/UserEditDiffsStep.d.ts +2 -1
- package/dist/workflows/steps/UserEditDiffsStep.js +8 -4
- package/package.json +1 -1
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 {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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(
|
|
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 {
|
|
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 =
|
|
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
|
|
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
|
-
|
|
75
|
-
|
|
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
|
|
78
|
-
const
|
|
79
|
-
|
|
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
|
-
|
|
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 (
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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]:
|
|
32
|
+
[locale: string]: DownloadedVersionEntryV1;
|
|
14
33
|
};
|
|
15
34
|
};
|
|
16
35
|
};
|
|
17
36
|
};
|
|
18
37
|
};
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
if (fs.existsSync(legacyPath)) {
|
|
14
|
-
fs.unlinkSync(legacyPath);
|
|
15
|
-
}
|
|
78
|
+
if (!fs.existsSync(rootPath)) {
|
|
79
|
+
data = { version: 2, branchId, entries: [] };
|
|
16
80
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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.
|
|
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.
|
|
2
|
+
export const PACKAGE_VERSION = '2.10.5';
|
package/dist/types/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as fs from 'node:fs';
|
|
2
|
-
import {
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
18
|
+
this.succeeded = true;
|
|
18
19
|
}
|
|
19
20
|
catch {
|
|
20
21
|
// Non-fatal; keep going to enqueue
|
|
21
|
-
this.
|
|
22
|
+
this.failed = true;
|
|
22
23
|
}
|
|
23
24
|
return files;
|
|
24
25
|
}
|
|
25
26
|
async wait() {
|
|
26
|
-
if (this.
|
|
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
|
}
|