gtx-cli 2.3.14 → 2.4.0
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 +17 -0
- package/dist/api/checkFileTranslations.js +1 -0
- package/dist/api/collectUserEditDiffs.d.ts +14 -0
- package/dist/api/collectUserEditDiffs.js +157 -0
- package/dist/api/saveLocalEdits.d.ts +6 -0
- package/dist/api/saveLocalEdits.js +30 -0
- package/dist/api/sendFiles.js +15 -1
- package/dist/api/sendUserEdits.d.ts +19 -0
- package/dist/api/sendUserEdits.js +15 -0
- package/dist/cli/base.d.ts +1 -0
- package/dist/cli/base.js +13 -0
- package/dist/cli/commands/edits.d.ts +8 -0
- package/dist/cli/commands/edits.js +32 -0
- package/dist/utils/addExplicitAnchorIds.js +12 -2
- package/dist/utils/gitDiff.d.ts +8 -0
- package/dist/utils/gitDiff.js +32 -0
- package/dist/utils/localizeStaticImports.js +6 -0
- package/dist/utils/localizeStaticUrls.js +16 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,22 @@
|
|
|
1
1
|
# gtx-cli
|
|
2
2
|
|
|
3
|
+
## 2.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#745](https://github.com/generaltranslation/gt/pull/745) [`5208937`](https://github.com/generaltranslation/gt/commit/520893719480b40774ccd749fe73727cf490f46c) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Adding local translation editing. Local user edits to translation will now be saved and used to inform future translations of the same file.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`5208937`](https://github.com/generaltranslation/gt/commit/520893719480b40774ccd749fe73727cf490f46c)]:
|
|
12
|
+
- generaltranslation@7.7.0
|
|
13
|
+
|
|
14
|
+
## 2.3.15
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- [#741](https://github.com/generaltranslation/gt/pull/741) [`559c0bf`](https://github.com/generaltranslation/gt/commit/559c0bfa7ff9e6664f65317eddbab419682a3c95) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Avoid localizing relative MDX links
|
|
19
|
+
|
|
3
20
|
## 2.3.14
|
|
4
21
|
|
|
5
22
|
### Patch Changes
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Settings } from '../types/index.js';
|
|
2
|
+
type UploadedFileRef = {
|
|
3
|
+
fileId: string;
|
|
4
|
+
versionId: string;
|
|
5
|
+
fileName: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Collects local user edits by diffing the latest downloaded server translation version
|
|
9
|
+
* against the current local translation file, and submits the diffs upstream.
|
|
10
|
+
*
|
|
11
|
+
* Must run before enqueueing new translations so rules are available to the generator.
|
|
12
|
+
*/
|
|
13
|
+
export declare function collectAndSendUserEditDiffs(uploadedFiles: UploadedFileRef[], settings: Settings): Promise<void>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import * as fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
import { getDownloadedVersions } from '../fs/config/downloadedVersions.js';
|
|
4
|
+
import { createFileMapping } from '../formats/files/fileMapping.js';
|
|
5
|
+
import { getGitUnifiedDiff } from '../utils/gitDiff.js';
|
|
6
|
+
import { sendUserEditDiffs } from './sendUserEdits.js';
|
|
7
|
+
import { gt } from '../utils/gt.js';
|
|
8
|
+
const MAX_DIFF_BATCH_BYTES = 1_500_000;
|
|
9
|
+
const MAX_DOWNLOAD_BATCH = 100;
|
|
10
|
+
/**
|
|
11
|
+
* Collects local user edits by diffing the latest downloaded server translation version
|
|
12
|
+
* against the current local translation file, and submits the diffs upstream.
|
|
13
|
+
*
|
|
14
|
+
* Must run before enqueueing new translations so rules are available to the generator.
|
|
15
|
+
*/
|
|
16
|
+
export async function collectAndSendUserEditDiffs(uploadedFiles, settings) {
|
|
17
|
+
if (!settings.files)
|
|
18
|
+
return;
|
|
19
|
+
const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
|
|
20
|
+
const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
|
|
21
|
+
const downloadedVersions = getDownloadedVersions(settings.configDirectory);
|
|
22
|
+
const tempDir = path.join(settings.configDirectory, 'tmp');
|
|
23
|
+
if (!fs.existsSync(tempDir))
|
|
24
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
25
|
+
const candidates = [];
|
|
26
|
+
for (const uploadedFile of uploadedFiles) {
|
|
27
|
+
for (const locale of settings.locales) {
|
|
28
|
+
const resolvedLocale = gt.resolveAliasLocale(locale);
|
|
29
|
+
const outputPath = fileMapping[locale]?.[uploadedFile.fileName] ?? null;
|
|
30
|
+
if (!outputPath)
|
|
31
|
+
continue;
|
|
32
|
+
if (!fs.existsSync(outputPath))
|
|
33
|
+
continue;
|
|
34
|
+
const lockKeyById = uploadedFile.fileId
|
|
35
|
+
? `${uploadedFile.fileId}:${resolvedLocale}`
|
|
36
|
+
: null;
|
|
37
|
+
const lockKeyByName = `${uploadedFile.fileName}:${resolvedLocale}`;
|
|
38
|
+
const lockEntry = (lockKeyById && downloadedVersions.entries[lockKeyById]) ||
|
|
39
|
+
downloadedVersions.entries[lockKeyByName];
|
|
40
|
+
const versionId = lockEntry?.versionId;
|
|
41
|
+
if (!versionId)
|
|
42
|
+
continue;
|
|
43
|
+
candidates.push({
|
|
44
|
+
fileName: uploadedFile.fileName,
|
|
45
|
+
fileId: uploadedFile.fileId,
|
|
46
|
+
versionId,
|
|
47
|
+
locale: resolvedLocale,
|
|
48
|
+
outputPath,
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
const collectedDiffs = [];
|
|
53
|
+
if (candidates.length > 0) {
|
|
54
|
+
const fileQueryData = candidates.map((c) => ({
|
|
55
|
+
versionId: c.versionId,
|
|
56
|
+
fileName: c.fileName,
|
|
57
|
+
locale: c.locale,
|
|
58
|
+
}));
|
|
59
|
+
// Single batched check to obtain translation IDs
|
|
60
|
+
const checkResponse = await gt.checkFileTranslations(fileQueryData);
|
|
61
|
+
const translations = (checkResponse?.translations || []).filter((t) => t && t.isReady && t.id && t.fileName && t.locale);
|
|
62
|
+
// Map fileName:resolvedLocale -> translationId
|
|
63
|
+
const idByKey = new Map();
|
|
64
|
+
for (const t of translations) {
|
|
65
|
+
const resolved = gt.resolveAliasLocale(t.locale);
|
|
66
|
+
idByKey.set(`${t.fileName}:${resolved}`, t.id);
|
|
67
|
+
}
|
|
68
|
+
// Collect translation IDs in batches and download contents
|
|
69
|
+
const ids = [];
|
|
70
|
+
for (const c of candidates) {
|
|
71
|
+
const id = idByKey.get(`${c.fileName}:${c.locale}`);
|
|
72
|
+
if (id)
|
|
73
|
+
ids.push(id);
|
|
74
|
+
}
|
|
75
|
+
// Helper to chunk array
|
|
76
|
+
function chunk(arr, size) {
|
|
77
|
+
const res = [];
|
|
78
|
+
for (let i = 0; i < arr.length; i += size)
|
|
79
|
+
res.push(arr.slice(i, i + size));
|
|
80
|
+
return res;
|
|
81
|
+
}
|
|
82
|
+
const serverContentByKey = new Map();
|
|
83
|
+
for (const idChunk of chunk(ids, MAX_DOWNLOAD_BATCH)) {
|
|
84
|
+
try {
|
|
85
|
+
const resp = await gt.downloadFileBatch(idChunk);
|
|
86
|
+
const files = resp?.files || [];
|
|
87
|
+
for (const f of files) {
|
|
88
|
+
// Find corresponding candidate key via idByKey reverse lookup
|
|
89
|
+
for (const [key, id] of idByKey.entries()) {
|
|
90
|
+
if (id === f.id) {
|
|
91
|
+
serverContentByKey.set(key, f.data);
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
// Ignore chunk failures; proceed with what we have
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Compute diffs using fetched server contents
|
|
102
|
+
for (const c of candidates) {
|
|
103
|
+
const key = `${c.fileName}:${c.locale}`;
|
|
104
|
+
const serverContent = serverContentByKey.get(key);
|
|
105
|
+
if (!serverContent)
|
|
106
|
+
continue;
|
|
107
|
+
try {
|
|
108
|
+
const safeName = Buffer.from(`${c.fileName}:${c.locale}`)
|
|
109
|
+
.toString('base64')
|
|
110
|
+
.replace(/=+$/g, '');
|
|
111
|
+
const tempServerFile = path.join(tempDir, `${safeName}.server`);
|
|
112
|
+
await fs.promises.writeFile(tempServerFile, serverContent, 'utf8');
|
|
113
|
+
const diff = await getGitUnifiedDiff(tempServerFile, c.outputPath);
|
|
114
|
+
try {
|
|
115
|
+
await fs.promises.unlink(tempServerFile);
|
|
116
|
+
}
|
|
117
|
+
catch { }
|
|
118
|
+
if (diff && diff.trim().length > 0) {
|
|
119
|
+
const localContent = await fs.promises.readFile(c.outputPath, 'utf8');
|
|
120
|
+
collectedDiffs.push({
|
|
121
|
+
fileName: c.fileName,
|
|
122
|
+
locale: c.locale,
|
|
123
|
+
diff,
|
|
124
|
+
versionId: c.versionId,
|
|
125
|
+
fileId: c.fileId,
|
|
126
|
+
localContent,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Ignore failures for this file
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (collectedDiffs.length > 0) {
|
|
136
|
+
// Batch by payload size
|
|
137
|
+
const maxBatchBytes = MAX_DIFF_BATCH_BYTES;
|
|
138
|
+
const batches = [];
|
|
139
|
+
let currentBatch = [];
|
|
140
|
+
for (const d of collectedDiffs) {
|
|
141
|
+
const tentative = [...currentBatch, d];
|
|
142
|
+
const bytes = Buffer.byteLength(JSON.stringify({ projectId: settings.projectId, diffs: tentative }), 'utf8');
|
|
143
|
+
if (bytes > maxBatchBytes && currentBatch.length > 0) {
|
|
144
|
+
batches.push(currentBatch);
|
|
145
|
+
currentBatch = [d];
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
currentBatch = tentative;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (currentBatch.length > 0)
|
|
152
|
+
batches.push(currentBatch);
|
|
153
|
+
for (const batch of batches) {
|
|
154
|
+
await sendUserEditDiffs(batch, settings);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Settings } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Uploads current source files to obtain file references, then collects and sends
|
|
4
|
+
* diffs for all locales based on last downloaded versions. Does not enqueue translations.
|
|
5
|
+
*/
|
|
6
|
+
export declare function saveLocalEdits(settings: Settings): Promise<void>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { gt } from '../utils/gt.js';
|
|
2
|
+
import { aggregateFiles } from '../formats/files/translate.js';
|
|
3
|
+
import { collectAndSendUserEditDiffs } from './collectUserEditDiffs.js';
|
|
4
|
+
/**
|
|
5
|
+
* Uploads current source files to obtain file references, then collects and sends
|
|
6
|
+
* diffs for all locales based on last downloaded versions. Does not enqueue translations.
|
|
7
|
+
*/
|
|
8
|
+
export async function saveLocalEdits(settings) {
|
|
9
|
+
if (!settings.files)
|
|
10
|
+
return;
|
|
11
|
+
// Collect current files from config
|
|
12
|
+
const files = await aggregateFiles(settings);
|
|
13
|
+
if (!files.length)
|
|
14
|
+
return;
|
|
15
|
+
const uploads = files.map(({ content, fileName, fileFormat, dataFormat }) => ({
|
|
16
|
+
source: {
|
|
17
|
+
content,
|
|
18
|
+
fileName,
|
|
19
|
+
fileFormat,
|
|
20
|
+
dataFormat,
|
|
21
|
+
locale: settings.defaultLocale,
|
|
22
|
+
},
|
|
23
|
+
}));
|
|
24
|
+
// Upload sources only to get file references, then compute diffs
|
|
25
|
+
const upload = await gt.uploadSourceFiles(uploads, {
|
|
26
|
+
sourceLocale: settings.defaultLocale,
|
|
27
|
+
modelProvider: settings.modelProvider,
|
|
28
|
+
});
|
|
29
|
+
await collectAndSendUserEditDiffs(upload.uploadedFiles, settings);
|
|
30
|
+
}
|
package/dist/api/sendFiles.js
CHANGED
|
@@ -2,6 +2,7 @@ import chalk from 'chalk';
|
|
|
2
2
|
import { createSpinner, logErrorAndExit, logMessage, logSuccess, } from '../console/logging.js';
|
|
3
3
|
import { gt } from '../utils/gt.js';
|
|
4
4
|
import { TEMPLATE_FILE_NAME } from '../cli/commands/stage.js';
|
|
5
|
+
import { collectAndSendUserEditDiffs } from './collectUserEditDiffs.js';
|
|
5
6
|
/**
|
|
6
7
|
* Sends multiple files for translation to the API
|
|
7
8
|
* @param files - Array of file objects to translate
|
|
@@ -88,7 +89,20 @@ export async function sendFiles(files, options, settings) {
|
|
|
88
89
|
setupSpinner.stop(chalk.yellow(`Setup ${setupFailedMessage ? 'failed' : 'timed out'} — proceeding without setup${setupFailedMessage ? ` (${setupFailedMessage})` : ''}`));
|
|
89
90
|
}
|
|
90
91
|
}
|
|
91
|
-
// Step 3:
|
|
92
|
+
// Step 3: Prior to enqueue, detect and submit user edit diffs (minimal UX)
|
|
93
|
+
const prepSpinner = createSpinner('dots');
|
|
94
|
+
currentSpinner = prepSpinner;
|
|
95
|
+
prepSpinner.start('Updating translations...');
|
|
96
|
+
try {
|
|
97
|
+
await collectAndSendUserEditDiffs(upload.uploadedFiles, settings);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Non-fatal; keep going to enqueue
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
prepSpinner.stop('Updated translations');
|
|
104
|
+
}
|
|
105
|
+
// Step 4: Enqueue translations by reference
|
|
92
106
|
const enqueueSpinner = createSpinner('dots');
|
|
93
107
|
currentSpinner = enqueueSpinner;
|
|
94
108
|
enqueueSpinner.start('Enqueuing translations...');
|
|
@@ -0,0 +1,19 @@
|
|
|
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>;
|
|
@@ -0,0 +1,15 @@
|
|
|
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
|
+
}
|
package/dist/cli/base.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ export declare class BaseCLI {
|
|
|
18
18
|
execute(): void;
|
|
19
19
|
protected setupStageCommand(): void;
|
|
20
20
|
protected setupTranslateCommand(): void;
|
|
21
|
+
protected setupSendDiffsCommand(): void;
|
|
21
22
|
protected handleStage(initOptions: TranslateFlags): Promise<void>;
|
|
22
23
|
protected handleTranslate(initOptions: TranslateFlags): Promise<void>;
|
|
23
24
|
protected setupUploadCommand(): void;
|
package/dist/cli/base.js
CHANGED
|
@@ -20,6 +20,7 @@ import { handleDownload, handleTranslate, postProcessTranslations, } from './com
|
|
|
20
20
|
import { getDownloaded, clearDownloaded } from '../state/recentDownloads.js';
|
|
21
21
|
import updateConfig from '../fs/config/updateConfig.js';
|
|
22
22
|
import { createLoadTranslationsFile } from '../fs/createLoadTranslationsFile.js';
|
|
23
|
+
import { saveLocalEdits } from '../api/saveLocalEdits.js';
|
|
23
24
|
export class BaseCLI {
|
|
24
25
|
library;
|
|
25
26
|
additionalModules;
|
|
@@ -34,6 +35,7 @@ export class BaseCLI {
|
|
|
34
35
|
this.setupSetupCommand();
|
|
35
36
|
this.setupUploadCommand();
|
|
36
37
|
this.setupLoginCommand();
|
|
38
|
+
this.setupSendDiffsCommand();
|
|
37
39
|
}
|
|
38
40
|
// Init is never called in a child class
|
|
39
41
|
init() {
|
|
@@ -64,6 +66,17 @@ export class BaseCLI {
|
|
|
64
66
|
endCommand('Done!');
|
|
65
67
|
});
|
|
66
68
|
}
|
|
69
|
+
setupSendDiffsCommand() {
|
|
70
|
+
this.program
|
|
71
|
+
.command('save-local')
|
|
72
|
+
.description('Save local edits for all configured files by sending diffs (no translation enqueued)')
|
|
73
|
+
.action(async () => {
|
|
74
|
+
const config = findFilepath(['gt.config.json']);
|
|
75
|
+
const settings = await generateSettings({ config });
|
|
76
|
+
await saveLocalEdits(settings);
|
|
77
|
+
endCommand('Saved local edits');
|
|
78
|
+
});
|
|
79
|
+
}
|
|
67
80
|
async handleStage(initOptions) {
|
|
68
81
|
const settings = await generateSettings(initOptions);
|
|
69
82
|
if (!settings.stageTranslations) {
|
|
@@ -0,0 +1,32 @@
|
|
|
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
|
+
}
|
|
@@ -36,7 +36,7 @@ function extractHeadingText(heading) {
|
|
|
36
36
|
function hasExplicitId(heading, ast) {
|
|
37
37
|
const lastChild = heading.children[heading.children.length - 1];
|
|
38
38
|
if (lastChild?.type === 'text') {
|
|
39
|
-
return /(\{#[^}]+\}|\[[^\]]+\])
|
|
39
|
+
return /(\{#[^}]+\}|\\\{#[^}]+\\\}|\[[^\]]+\])\s*$/.test(lastChild.value);
|
|
40
40
|
}
|
|
41
41
|
return false;
|
|
42
42
|
}
|
|
@@ -124,7 +124,7 @@ export function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settin
|
|
|
124
124
|
}
|
|
125
125
|
return {
|
|
126
126
|
content,
|
|
127
|
-
hasChanges:
|
|
127
|
+
hasChanges: content !== translatedContent,
|
|
128
128
|
addedIds,
|
|
129
129
|
};
|
|
130
130
|
}
|
|
@@ -148,6 +148,7 @@ function applyInlineIds(translatedContent, idMappings) {
|
|
|
148
148
|
}
|
|
149
149
|
// Apply IDs to headings based on position
|
|
150
150
|
let headingIndex = 0;
|
|
151
|
+
let actuallyModifiedContent = false;
|
|
151
152
|
visit(processedAst, 'heading', (heading) => {
|
|
152
153
|
const id = idMappings.get(headingIndex);
|
|
153
154
|
if (id) {
|
|
@@ -168,9 +169,14 @@ function applyInlineIds(translatedContent, idMappings) {
|
|
|
168
169
|
value: ` \\{#${id}\\}`,
|
|
169
170
|
});
|
|
170
171
|
}
|
|
172
|
+
actuallyModifiedContent = true;
|
|
171
173
|
}
|
|
172
174
|
headingIndex++;
|
|
173
175
|
});
|
|
176
|
+
// If we didn't modify any headings, return original content
|
|
177
|
+
if (!actuallyModifiedContent) {
|
|
178
|
+
return translatedContent;
|
|
179
|
+
}
|
|
174
180
|
// Convert the modified AST back to MDX string
|
|
175
181
|
try {
|
|
176
182
|
const stringifyProcessor = unified()
|
|
@@ -248,6 +254,10 @@ function applyDivWrappedIds(translatedContent, translatedHeadings, idMappings) {
|
|
|
248
254
|
// Process headings from longest to shortest original line to avoid partial matches
|
|
249
255
|
const sortedHeadings = headingsToWrap.sort((a, b) => b.originalLine.length - a.originalLine.length);
|
|
250
256
|
for (const heading of sortedHeadings) {
|
|
257
|
+
// If already wrapped with this id, skip (idempotent)
|
|
258
|
+
if (content.includes(`<div id="${heading.id}">`)) {
|
|
259
|
+
continue;
|
|
260
|
+
}
|
|
251
261
|
// Escape the original line for use in regex
|
|
252
262
|
const escapedLine = heading.originalLine.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
253
263
|
const headingPattern = new RegExp(`^${escapedLine}\\s*$`, 'gm');
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a unified diff using the system's git, comparing two paths even if not in a repo.
|
|
3
|
+
* Uses `git diff --no-index` so neither path needs to be tracked by git.
|
|
4
|
+
*
|
|
5
|
+
* Exit codes: 0 (no changes), 1 (changes), >1 (error). We treat 0/1 as success.
|
|
6
|
+
* Throws if git is unavailable or another error occurs.
|
|
7
|
+
*/
|
|
8
|
+
export declare function getGitUnifiedDiff(oldPath: string, newPath: string): Promise<string>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execFile } from 'node:child_process';
|
|
2
|
+
import { promisify } from 'node:util';
|
|
3
|
+
const execFileAsync = promisify(execFile);
|
|
4
|
+
/**
|
|
5
|
+
* Returns a unified diff using the system's git, comparing two paths even if not in a repo.
|
|
6
|
+
* Uses `git diff --no-index` so neither path needs to be tracked by git.
|
|
7
|
+
*
|
|
8
|
+
* Exit codes: 0 (no changes), 1 (changes), >1 (error). We treat 0/1 as success.
|
|
9
|
+
* Throws if git is unavailable or another error occurs.
|
|
10
|
+
*/
|
|
11
|
+
export async function getGitUnifiedDiff(oldPath, newPath) {
|
|
12
|
+
const res = await execFileAsync('git', [
|
|
13
|
+
'diff',
|
|
14
|
+
'--no-index',
|
|
15
|
+
'--text',
|
|
16
|
+
'--unified=3',
|
|
17
|
+
'--no-color',
|
|
18
|
+
'--',
|
|
19
|
+
oldPath,
|
|
20
|
+
newPath,
|
|
21
|
+
], {
|
|
22
|
+
windowsHide: true,
|
|
23
|
+
}).catch((error) => {
|
|
24
|
+
// Exit code 1 means differences found; stdout contains the diff
|
|
25
|
+
if (error && error.code === 1 && typeof error.stdout === 'string') {
|
|
26
|
+
return { stdout: error.stdout };
|
|
27
|
+
}
|
|
28
|
+
throw error;
|
|
29
|
+
});
|
|
30
|
+
// When there are no changes, stdout is empty string and exit code 0
|
|
31
|
+
return res.stdout || '';
|
|
32
|
+
}
|
|
@@ -163,6 +163,12 @@ function transformNonDefaultLocaleImportPathWithHidden(fullPath, patternHead, ta
|
|
|
163
163
|
* Transforms import path for non-default locale processing with hideDefaultLocale=false
|
|
164
164
|
*/
|
|
165
165
|
function transformNonDefaultLocaleImportPath(fullPath, patternHead, targetLocale, defaultLocale) {
|
|
166
|
+
// If already localized to target, skip
|
|
167
|
+
const expectedPathWithTarget = `${patternHead}${targetLocale}`;
|
|
168
|
+
if (fullPath.startsWith(`${expectedPathWithTarget}/`) ||
|
|
169
|
+
fullPath === expectedPathWithTarget) {
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
166
172
|
const expectedPathWithLocale = `${patternHead}${defaultLocale}`;
|
|
167
173
|
if (fullPath.startsWith(`${expectedPathWithLocale}/`) ||
|
|
168
174
|
fullPath === expectedPathWithLocale) {
|
|
@@ -175,6 +175,10 @@ export function transformUrlPath(originalUrl, patternHead, targetLocale, default
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
else if (hideDefaultLocale) {
|
|
178
|
+
// Avoid duplicating target locale if already present
|
|
179
|
+
if (originalPathArray?.[patternHeadArray.length] === targetLocale) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
178
182
|
const newPathArray = [
|
|
179
183
|
...originalPathArray.slice(0, patternHeadArray.length),
|
|
180
184
|
targetLocale,
|
|
@@ -256,6 +260,18 @@ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLo
|
|
|
256
260
|
}
|
|
257
261
|
// Helper function to transform URL based on pattern
|
|
258
262
|
const transformUrl = (originalUrl, linkType) => {
|
|
263
|
+
// For Markdown links [text](path), only process absolute-root paths starting with '/'
|
|
264
|
+
// Relative markdown links should remain relative to the current page and not be localized.
|
|
265
|
+
if (linkType === 'markdown') {
|
|
266
|
+
const isFragment = /^\s*#/.test(originalUrl);
|
|
267
|
+
const isAbsoluteRoot = originalUrl.startsWith('/');
|
|
268
|
+
const looksAbsoluteWithDomain = baseDomain
|
|
269
|
+
? shouldProcessAbsoluteUrl(originalUrl, baseDomain)
|
|
270
|
+
: false;
|
|
271
|
+
if (!isAbsoluteRoot && !looksAbsoluteWithDomain && !isFragment) {
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
259
275
|
// Check if URL should be processed
|
|
260
276
|
if (!shouldProcessUrl(originalUrl, patternHead, targetLocale, defaultLocale, baseDomain)) {
|
|
261
277
|
return null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtx-cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": "dist/main.js",
|
|
6
6
|
"files": [
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"unified": "^11.0.5",
|
|
92
92
|
"unist-util-visit": "^5.0.0",
|
|
93
93
|
"yaml": "^2.8.0",
|
|
94
|
-
"generaltranslation": "7.
|
|
94
|
+
"generaltranslation": "7.7.0"
|
|
95
95
|
},
|
|
96
96
|
"devDependencies": {
|
|
97
97
|
"@babel/types": "^7.28.4",
|