gtx-cli 2.2.0-alpha.0 → 2.2.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 +47 -0
- package/dist/cli/commands/translate.js +5 -0
- package/dist/formats/files/translate.js +30 -7
- package/dist/react/parse/createDictionaryUpdates.js +2 -1
- package/dist/utils/addExplicitAnchorIds.js +9 -12
- package/dist/utils/escapeHtml.d.ts +8 -0
- package/dist/utils/escapeHtml.js +32 -0
- package/dist/utils/localizeStaticImports.js +51 -50
- package/dist/utils/localizeStaticUrls.js +17 -10
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,52 @@
|
|
|
1
1
|
# gtx-cli
|
|
2
2
|
|
|
3
|
+
## 2.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#638](https://github.com/generaltranslation/gt/pull/638) [`16bf30d`](https://github.com/generaltranslation/gt/commit/16bf30d70a0599ec863305f4f7a5a0852dd07e5d) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - feat: add locale aliasing
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [[`16bf30d`](https://github.com/generaltranslation/gt/commit/16bf30d70a0599ec863305f4f7a5a0852dd07e5d)]:
|
|
12
|
+
- generaltranslation@7.5.0
|
|
13
|
+
|
|
14
|
+
## 2.1.21
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- [#645](https://github.com/generaltranslation/gt/pull/645) [`58cfaee`](https://github.com/generaltranslation/gt/commit/58cfaee5cc1dcd187f0b72b2761f96c19b4f313e) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Escaping HTML to avoid parsing issues from MDX consumers
|
|
19
|
+
|
|
20
|
+
## 2.1.20
|
|
21
|
+
|
|
22
|
+
### Patch Changes
|
|
23
|
+
|
|
24
|
+
- [#643](https://github.com/generaltranslation/gt/pull/643) [`4f553c0`](https://github.com/generaltranslation/gt/commit/4f553c00c119f272edc5ccb3616f2d0effec8586) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Removing custom configuration on remarkStringify
|
|
25
|
+
|
|
26
|
+
## 2.1.19
|
|
27
|
+
|
|
28
|
+
### Patch Changes
|
|
29
|
+
|
|
30
|
+
- [#641](https://github.com/generaltranslation/gt/pull/641) [`4c67f77`](https://github.com/generaltranslation/gt/commit/4c67f775ee892b47eebcc3178c00ad6547a84d84) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Encoding placeholders that break MDX parse
|
|
31
|
+
|
|
32
|
+
## 2.1.18
|
|
33
|
+
|
|
34
|
+
### Patch Changes
|
|
35
|
+
|
|
36
|
+
- [#637](https://github.com/generaltranslation/gt/pull/637) [`9c40a3c`](https://github.com/generaltranslation/gt/commit/9c40a3c729bf690381959679078c11c9c29bcdf2) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Skipping over empty files when sending for translation
|
|
37
|
+
|
|
38
|
+
## 2.1.17
|
|
39
|
+
|
|
40
|
+
### Patch Changes
|
|
41
|
+
|
|
42
|
+
- [#635](https://github.com/generaltranslation/gt/pull/635) [`10aa051`](https://github.com/generaltranslation/gt/commit/10aa051592cea43f772615da200c8615d4dd1a78) Thanks [@brian-lou](https://github.com/brian-lou)! - Create dictionary with uuid to reduce flakiness
|
|
43
|
+
|
|
44
|
+
## 2.1.16
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- [#630](https://github.com/generaltranslation/gt/pull/630) [`1f0dc1b`](https://github.com/generaltranslation/gt/commit/1f0dc1b17f22737263938998f5c516e0aa136b1a) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Adding localization of import paths to MDX files
|
|
49
|
+
|
|
3
50
|
## 2.1.15
|
|
4
51
|
|
|
5
52
|
### Patch Changes
|
|
@@ -7,6 +7,7 @@ import flattenJsonFiles from '../../utils/flattenJsonFiles.js';
|
|
|
7
7
|
import localizeStaticUrls from '../../utils/localizeStaticUrls.js';
|
|
8
8
|
import processAnchorIds from '../../utils/processAnchorIds.js';
|
|
9
9
|
import { noFilesError, noVersionIdError } from '../../console/index.js';
|
|
10
|
+
import localizeStaticImports from '../../utils/localizeStaticImports.js';
|
|
10
11
|
// Downloads translations that were completed
|
|
11
12
|
export async function handleTranslate(options, settings, filesTranslationResponse) {
|
|
12
13
|
if (filesTranslationResponse && settings.files) {
|
|
@@ -46,6 +47,10 @@ export async function postProcessTranslations(settings) {
|
|
|
46
47
|
// Uses inline {#id} format by default, or div wrapping if experimentalAddHeaderAnchorIds is 'mintlify'
|
|
47
48
|
await processAnchorIds(settings);
|
|
48
49
|
}
|
|
50
|
+
// Localize static imports (import Snippet from /snippets/file.mdx -> import Snippet from /snippets/[locale]/file.mdx)
|
|
51
|
+
if (settings.options?.experimentalLocalizeStaticImports) {
|
|
52
|
+
await localizeStaticImports(settings);
|
|
53
|
+
}
|
|
49
54
|
// Flatten json files into a single file
|
|
50
55
|
if (settings.options?.experimentalFlattenJsonFiles) {
|
|
51
56
|
await flattenJsonFiles(settings);
|
|
@@ -35,30 +35,46 @@ export async function aggregateFiles(settings) {
|
|
|
35
35
|
else {
|
|
36
36
|
dataFormat = 'JSX';
|
|
37
37
|
}
|
|
38
|
-
const jsonFiles = filePaths.json
|
|
38
|
+
const jsonFiles = filePaths.json
|
|
39
|
+
.map((filePath) => {
|
|
39
40
|
const content = readFile(filePath);
|
|
40
|
-
const parsedJson = parseJson(content, filePath, settings.options || {}, settings.defaultLocale);
|
|
41
41
|
const relativePath = getRelative(filePath);
|
|
42
|
+
const parsedJson = parseJson(content, filePath, settings.options || {}, settings.defaultLocale);
|
|
42
43
|
return {
|
|
43
44
|
content: parsedJson,
|
|
44
45
|
fileName: relativePath,
|
|
45
46
|
fileFormat: 'JSON',
|
|
46
47
|
dataFormat,
|
|
47
48
|
};
|
|
49
|
+
})
|
|
50
|
+
.filter((file) => {
|
|
51
|
+
if (!file || typeof file.content !== 'string' || !file.content.trim()) {
|
|
52
|
+
logWarning(`Skipping ${file?.fileName ?? 'unknown'}: JSON file is empty`);
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return true;
|
|
48
56
|
});
|
|
49
57
|
allFiles.push(...jsonFiles);
|
|
50
58
|
}
|
|
51
59
|
// Process YAML files
|
|
52
60
|
if (filePaths.yaml) {
|
|
53
|
-
const yamlFiles = filePaths.yaml
|
|
61
|
+
const yamlFiles = filePaths.yaml
|
|
62
|
+
.map((filePath) => {
|
|
54
63
|
const content = readFile(filePath);
|
|
55
|
-
const { content: parsedYaml, fileFormat } = parseYaml(content, filePath, settings.options || {});
|
|
56
64
|
const relativePath = getRelative(filePath);
|
|
65
|
+
const { content: parsedYaml, fileFormat } = parseYaml(content, filePath, settings.options || {});
|
|
57
66
|
return {
|
|
58
67
|
content: parsedYaml,
|
|
59
68
|
fileName: relativePath,
|
|
60
69
|
fileFormat,
|
|
61
70
|
};
|
|
71
|
+
})
|
|
72
|
+
.filter((file) => {
|
|
73
|
+
if (!file || typeof file.content !== 'string' || !file.content.trim()) {
|
|
74
|
+
logWarning(`Skipping ${file?.fileName ?? 'unknown'}: YAML file is empty`);
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
return true;
|
|
62
78
|
});
|
|
63
79
|
allFiles.push(...yamlFiles);
|
|
64
80
|
}
|
|
@@ -73,8 +89,7 @@ export async function aggregateFiles(settings) {
|
|
|
73
89
|
if (fileType === 'mdx') {
|
|
74
90
|
const validation = isValidMdx(content, filePath);
|
|
75
91
|
if (!validation.isValid) {
|
|
76
|
-
|
|
77
|
-
logWarning(`Skipping ${relativePath}: MDX file is not AST parsable${errorMsg}`);
|
|
92
|
+
logWarning(`Skipping ${relativePath}: MDX file is not AST parsable${validation.error ? `: ${validation.error}` : ''}`);
|
|
78
93
|
return null;
|
|
79
94
|
}
|
|
80
95
|
}
|
|
@@ -85,7 +100,15 @@ export async function aggregateFiles(settings) {
|
|
|
85
100
|
fileFormat: fileType.toUpperCase(),
|
|
86
101
|
};
|
|
87
102
|
})
|
|
88
|
-
.filter((file) =>
|
|
103
|
+
.filter((file) => {
|
|
104
|
+
if (!file ||
|
|
105
|
+
typeof file.content !== 'string' ||
|
|
106
|
+
!file.content.trim()) {
|
|
107
|
+
logWarning(`Skipping ${file?.fileName ?? 'unknown'}: File is empty after sanitization`);
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
return true;
|
|
111
|
+
});
|
|
89
112
|
allFiles.push(...files);
|
|
90
113
|
}
|
|
91
114
|
}
|
|
@@ -7,6 +7,7 @@ import loadJSON from '../../fs/loadJSON.js';
|
|
|
7
7
|
import { hashSource } from 'generaltranslation/id';
|
|
8
8
|
import getEntryAndMetadata from '../utils/getEntryAndMetadata.js';
|
|
9
9
|
import { logError } from '../../console/logging.js';
|
|
10
|
+
import { randomUUID } from 'node:crypto';
|
|
10
11
|
export async function createDictionaryUpdates(dictionaryPath, esbuildConfig) {
|
|
11
12
|
let dictionary;
|
|
12
13
|
// ---- HANDLE JSON STRING DICTIONARY ----- //
|
|
@@ -21,7 +22,7 @@ export async function createDictionaryUpdates(dictionaryPath, esbuildConfig) {
|
|
|
21
22
|
write: false,
|
|
22
23
|
});
|
|
23
24
|
const bundledCode = result.outputFiles[0].text;
|
|
24
|
-
const tempFilePath = path.join(os.tmpdir(),
|
|
25
|
+
const tempFilePath = path.join(os.tmpdir(), `bundled-dictionary-${randomUUID()}.js`);
|
|
25
26
|
await fs.promises.writeFile(tempFilePath, bundledCode);
|
|
26
27
|
// Load the module using dynamic import
|
|
27
28
|
let dictionaryModule;
|
|
@@ -5,6 +5,7 @@ import remarkFrontmatter from 'remark-frontmatter';
|
|
|
5
5
|
import remarkStringify from 'remark-stringify';
|
|
6
6
|
import { visit } from 'unist-util-visit';
|
|
7
7
|
import { logWarning } from '../console/logging.js';
|
|
8
|
+
import { escapeHtmlInTextNodes } from './escapeHtml.js';
|
|
8
9
|
/**
|
|
9
10
|
* Generates a slug from heading text
|
|
10
11
|
*/
|
|
@@ -158,13 +159,13 @@ function applyInlineIds(translatedContent, idMappings) {
|
|
|
158
159
|
// Add the ID to the heading
|
|
159
160
|
const lastChild = heading.children[heading.children.length - 1];
|
|
160
161
|
if (lastChild?.type === 'text') {
|
|
161
|
-
lastChild.value += ` {#${id}}`;
|
|
162
|
+
lastChild.value += ` \\{#${id}\\}`;
|
|
162
163
|
}
|
|
163
164
|
else {
|
|
164
165
|
// If last child is not text, add a new text node
|
|
165
166
|
heading.children.push({
|
|
166
167
|
type: 'text',
|
|
167
|
-
value: ` {#${id}}`,
|
|
168
|
+
value: ` \\{#${id}\\}`,
|
|
168
169
|
});
|
|
169
170
|
}
|
|
170
171
|
}
|
|
@@ -173,23 +174,19 @@ function applyInlineIds(translatedContent, idMappings) {
|
|
|
173
174
|
// Convert the modified AST back to MDX string
|
|
174
175
|
try {
|
|
175
176
|
const stringifyProcessor = unified()
|
|
177
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
178
|
+
.use(remarkMdx)
|
|
179
|
+
.use(escapeHtmlInTextNodes)
|
|
176
180
|
.use(remarkStringify, {
|
|
177
|
-
bullet: '-',
|
|
178
|
-
emphasis: '_',
|
|
179
|
-
strong: '*',
|
|
180
|
-
rule: '-',
|
|
181
|
-
ruleRepetition: 3,
|
|
182
|
-
ruleSpaces: false,
|
|
183
181
|
handlers: {
|
|
184
182
|
// Custom handler to prevent escaping of {#id} syntax
|
|
185
183
|
text(node) {
|
|
186
184
|
return node.value;
|
|
187
185
|
},
|
|
188
186
|
},
|
|
189
|
-
})
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
let content = stringifyProcessor.stringify(processedAst);
|
|
187
|
+
});
|
|
188
|
+
const outTree = stringifyProcessor.runSync(processedAst);
|
|
189
|
+
let content = stringifyProcessor.stringify(outTree);
|
|
193
190
|
// Handle newline formatting to match original input
|
|
194
191
|
if (content.endsWith('\n') && !translatedContent.endsWith('\n')) {
|
|
195
192
|
content = content.slice(0, -1);
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Plugin } from 'unified';
|
|
2
|
+
import type { Root } from 'mdast';
|
|
3
|
+
/**
|
|
4
|
+
* Escape HTML-sensitive characters (`&`, `<`, `>`, `"`, `'`) in text nodes,
|
|
5
|
+
* leaving code, math, MDX expressions, and front-matter untouched.
|
|
6
|
+
* Ensures literals render safely without altering already-escaped entities.
|
|
7
|
+
*/
|
|
8
|
+
export declare const escapeHtmlInTextNodes: Plugin<[], Root>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { findAndReplace } from 'mdast-util-find-and-replace';
|
|
2
|
+
const IGNORE_PARENTS = [
|
|
3
|
+
'code',
|
|
4
|
+
'inlineCode',
|
|
5
|
+
'mdxFlowExpression',
|
|
6
|
+
'mdxTextExpression',
|
|
7
|
+
'mdxjsEsm',
|
|
8
|
+
'heading',
|
|
9
|
+
'yaml',
|
|
10
|
+
'toml',
|
|
11
|
+
'math',
|
|
12
|
+
'inlineMath',
|
|
13
|
+
];
|
|
14
|
+
// & that is NOT already an entity: &word; { ᨫ
|
|
15
|
+
const AMP_NOT_ENTITY = /&(?![a-zA-Z][a-zA-Z0-9]*;|#\d+;|#x[0-9A-Fa-f]+;)/g;
|
|
16
|
+
/**
|
|
17
|
+
* Escape HTML-sensitive characters (`&`, `<`, `>`, `"`, `'`) in text nodes,
|
|
18
|
+
* leaving code, math, MDX expressions, and front-matter untouched.
|
|
19
|
+
* Ensures literals render safely without altering already-escaped entities.
|
|
20
|
+
*/
|
|
21
|
+
export const escapeHtmlInTextNodes = function () {
|
|
22
|
+
return (tree) => {
|
|
23
|
+
findAndReplace(tree, [
|
|
24
|
+
// Order matters: & first (idempotency), then the rest
|
|
25
|
+
[AMP_NOT_ENTITY, '&'],
|
|
26
|
+
[/</g, '<'],
|
|
27
|
+
[/>/g, '>'],
|
|
28
|
+
[/"/g, '"'],
|
|
29
|
+
[/'/g, '''],
|
|
30
|
+
], { ignore: IGNORE_PARENTS });
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
2
3
|
import { createFileMapping } from '../formats/files/fileMapping.js';
|
|
3
4
|
import micromatch from 'micromatch';
|
|
4
5
|
import { unified } from 'unified';
|
|
5
6
|
import remarkParse from 'remark-parse';
|
|
6
7
|
import remarkMdx from 'remark-mdx';
|
|
7
8
|
import remarkFrontmatter from 'remark-frontmatter';
|
|
8
|
-
import remarkStringify from 'remark-stringify';
|
|
9
9
|
import { visit } from 'unist-util-visit';
|
|
10
10
|
const { isMatch } = micromatch;
|
|
11
11
|
/**
|
|
@@ -44,11 +44,15 @@ export default async function localizeStaticImports(settings) {
|
|
|
44
44
|
}
|
|
45
45
|
if (defaultLocaleFiles.length > 0) {
|
|
46
46
|
const defaultPromise = Promise.all(defaultLocaleFiles.map(async (filePath) => {
|
|
47
|
+
// Check if file exists before processing
|
|
48
|
+
if (!fs.existsSync(filePath)) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
47
51
|
// Get file content
|
|
48
52
|
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
49
53
|
// Localize the file using default locale
|
|
50
54
|
const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, settings.defaultLocale, // Process as default locale
|
|
51
|
-
settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath
|
|
55
|
+
settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath);
|
|
52
56
|
// Write the localized file back to the same path
|
|
53
57
|
await fs.promises.writeFile(filePath, localizedFile);
|
|
54
58
|
}));
|
|
@@ -61,10 +65,14 @@ export default async function localizeStaticImports(settings) {
|
|
|
61
65
|
const targetFiles = Object.values(filesMap).filter((path) => path.endsWith('.md') || path.endsWith('.mdx'));
|
|
62
66
|
// Replace the placeholder path with the target path
|
|
63
67
|
await Promise.all(targetFiles.map(async (filePath) => {
|
|
68
|
+
// Check if file exists before processing
|
|
69
|
+
if (!fs.existsSync(filePath)) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
64
72
|
// Get file content
|
|
65
73
|
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
66
74
|
// Localize the file
|
|
67
|
-
const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, locale, settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath
|
|
75
|
+
const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, locale, settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath);
|
|
68
76
|
// Write the localized file to the target path
|
|
69
77
|
await fs.promises.writeFile(filePath, localizedFile);
|
|
70
78
|
}));
|
|
@@ -175,23 +183,44 @@ function transformNonDefaultLocaleImportPath(fullPath, patternHead, targetLocale
|
|
|
175
183
|
/**
|
|
176
184
|
* Main import path transformation function that delegates to specific scenarios
|
|
177
185
|
*/
|
|
178
|
-
function transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale)
|
|
186
|
+
function transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale, currentFilePath, projectRoot = process.cwd() // fallback if not provided
|
|
187
|
+
) {
|
|
188
|
+
let newPath;
|
|
179
189
|
if (targetLocale === defaultLocale) {
|
|
180
|
-
|
|
190
|
+
newPath = transformDefaultLocaleImportPath(fullPath, patternHead, defaultLocale, hideDefaultLocale);
|
|
181
191
|
}
|
|
182
192
|
else if (hideDefaultLocale) {
|
|
183
|
-
|
|
193
|
+
newPath = transformNonDefaultLocaleImportPathWithHidden(fullPath, patternHead, targetLocale, defaultLocale);
|
|
184
194
|
}
|
|
185
195
|
else {
|
|
186
|
-
|
|
196
|
+
newPath = transformNonDefaultLocaleImportPath(fullPath, patternHead, targetLocale, defaultLocale);
|
|
187
197
|
}
|
|
198
|
+
if (!newPath)
|
|
199
|
+
return null;
|
|
200
|
+
if (currentFilePath) {
|
|
201
|
+
let resolvedPath;
|
|
202
|
+
if (newPath.startsWith('/')) {
|
|
203
|
+
// Interpret as project-root relative
|
|
204
|
+
resolvedPath = path.join(projectRoot, newPath.replace(/^\//, ''));
|
|
205
|
+
}
|
|
206
|
+
else {
|
|
207
|
+
// Relative to current file
|
|
208
|
+
const currentDir = path.dirname(currentFilePath);
|
|
209
|
+
resolvedPath = path.resolve(currentDir, newPath);
|
|
210
|
+
}
|
|
211
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return newPath;
|
|
188
216
|
}
|
|
189
217
|
/**
|
|
190
218
|
* AST-based transformation for MDX files using remark-mdx
|
|
191
219
|
*/
|
|
192
|
-
function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', exclude = []) {
|
|
220
|
+
function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', exclude = [], currentFilePath) {
|
|
193
221
|
const transformedImports = [];
|
|
194
|
-
|
|
222
|
+
// Don't auto-prefix relative patterns that start with . or ..
|
|
223
|
+
if (!pattern.startsWith('/') && !pattern.startsWith('.')) {
|
|
195
224
|
pattern = '/' + pattern;
|
|
196
225
|
}
|
|
197
226
|
const patternHead = pattern.split('[locale]')[0];
|
|
@@ -239,23 +268,23 @@ function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaul
|
|
|
239
268
|
transformedImports: [],
|
|
240
269
|
};
|
|
241
270
|
}
|
|
242
|
-
|
|
271
|
+
let content = mdxContent;
|
|
272
|
+
// Visit only mdxjsEsm nodes (import/export statements) to collect replacements
|
|
243
273
|
visit(processedAst, 'mdxjsEsm', (node) => {
|
|
244
274
|
if (node.value && node.value.includes(patternHead.replace(/\/$/, ''))) {
|
|
245
|
-
// Find
|
|
275
|
+
// Find import lines that need transformation
|
|
246
276
|
const lines = node.value.split('\n');
|
|
247
|
-
|
|
277
|
+
lines.forEach((line) => {
|
|
248
278
|
// Only process import lines that match our pattern
|
|
249
279
|
if (!line.trim().startsWith('import ')) {
|
|
250
|
-
return
|
|
280
|
+
return;
|
|
251
281
|
}
|
|
252
282
|
// Check if this line should be processed
|
|
253
283
|
if (!shouldProcessImportPath(line, patternHead, targetLocale, defaultLocale)) {
|
|
254
|
-
return
|
|
284
|
+
return;
|
|
255
285
|
}
|
|
256
286
|
// Extract the path from the import statement
|
|
257
287
|
const quotes = ['"', "'", '`'];
|
|
258
|
-
let transformedLine = line;
|
|
259
288
|
for (const quote of quotes) {
|
|
260
289
|
// Try both with and without trailing slash
|
|
261
290
|
let startPattern = `${quote}${patternHead}`;
|
|
@@ -274,7 +303,7 @@ function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaul
|
|
|
274
303
|
continue;
|
|
275
304
|
const fullPath = line.slice(pathStart, pathEnd);
|
|
276
305
|
// Transform the import path
|
|
277
|
-
const newPath = transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale);
|
|
306
|
+
const newPath = transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale, currentFilePath);
|
|
278
307
|
if (!newPath) {
|
|
279
308
|
continue; // No transformation needed
|
|
280
309
|
}
|
|
@@ -282,43 +311,15 @@ function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaul
|
|
|
282
311
|
if (isImportPathExcluded(fullPath, exclude, defaultLocale)) {
|
|
283
312
|
continue;
|
|
284
313
|
}
|
|
285
|
-
// Apply the transformation
|
|
314
|
+
// Apply the transformation to the original content
|
|
315
|
+
// Simply replace the import path with the new path
|
|
316
|
+
content = content.replace(`${quote}${fullPath}${quote}`, `${quote}${newPath}${quote}`);
|
|
286
317
|
transformedImports.push({ originalPath: fullPath, newPath });
|
|
287
|
-
transformedLine =
|
|
288
|
-
line.slice(0, pathStart) + newPath + line.slice(pathEnd);
|
|
289
318
|
break;
|
|
290
319
|
}
|
|
291
|
-
return transformedLine;
|
|
292
320
|
});
|
|
293
|
-
node.value = transformedLines.join('\n');
|
|
294
321
|
}
|
|
295
322
|
});
|
|
296
|
-
// Convert the modified AST back to MDX string
|
|
297
|
-
let content;
|
|
298
|
-
try {
|
|
299
|
-
const stringifyProcessor = unified()
|
|
300
|
-
.use(remarkStringify)
|
|
301
|
-
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
302
|
-
.use(remarkMdx);
|
|
303
|
-
content = stringifyProcessor.stringify(processedAst);
|
|
304
|
-
}
|
|
305
|
-
catch (error) {
|
|
306
|
-
console.warn(`Failed to stringify MDX content: ${error instanceof Error ? error.message : String(error)}`);
|
|
307
|
-
console.warn('Returning original content unchanged due to stringify error.');
|
|
308
|
-
return {
|
|
309
|
-
content: mdxContent,
|
|
310
|
-
hasChanges: false,
|
|
311
|
-
transformedImports: [],
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
// Handle newline formatting to match original input
|
|
315
|
-
if (content.endsWith('\n') && !mdxContent.endsWith('\n')) {
|
|
316
|
-
content = content.slice(0, -1);
|
|
317
|
-
}
|
|
318
|
-
// Preserve leading newlines from original content
|
|
319
|
-
if (mdxContent.startsWith('\n') && !content.startsWith('\n')) {
|
|
320
|
-
content = '\n' + content;
|
|
321
|
-
}
|
|
322
323
|
return {
|
|
323
324
|
content,
|
|
324
325
|
hasChanges: transformedImports.length > 0,
|
|
@@ -329,12 +330,12 @@ function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaul
|
|
|
329
330
|
* AST-based transformation for MDX files only
|
|
330
331
|
*/
|
|
331
332
|
function localizeStaticImportsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', // eg /docs/[locale] or /[locale]
|
|
332
|
-
exclude = [],
|
|
333
|
+
exclude = [], currentFilePath) {
|
|
333
334
|
// Skip .md files entirely - they cannot have imports
|
|
334
|
-
if (
|
|
335
|
+
if (currentFilePath && currentFilePath.endsWith('.md')) {
|
|
335
336
|
return file;
|
|
336
337
|
}
|
|
337
338
|
// For MDX files, use AST-based transformation
|
|
338
|
-
const result = transformMdxImports(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude);
|
|
339
|
+
const result = transformMdxImports(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude, currentFilePath);
|
|
339
340
|
return result.content;
|
|
340
341
|
}
|
|
@@ -7,6 +7,7 @@ import remarkMdx from 'remark-mdx';
|
|
|
7
7
|
import remarkFrontmatter from 'remark-frontmatter';
|
|
8
8
|
import remarkStringify from 'remark-stringify';
|
|
9
9
|
import { visit } from 'unist-util-visit';
|
|
10
|
+
import { escapeHtmlInTextNodes } from './escapeHtml.js';
|
|
10
11
|
const { isMatch } = micromatch;
|
|
11
12
|
/**
|
|
12
13
|
* Localizes static urls in content files.
|
|
@@ -49,6 +50,10 @@ export default async function localizeStaticUrls(settings, targetLocales) {
|
|
|
49
50
|
}
|
|
50
51
|
if (defaultLocaleFiles.length > 0) {
|
|
51
52
|
const defaultPromise = Promise.all(defaultLocaleFiles.map(async (filePath) => {
|
|
53
|
+
// Check if file exists before processing
|
|
54
|
+
if (!fs.existsSync(filePath)) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
52
57
|
// Get file content
|
|
53
58
|
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
54
59
|
// Localize the file using default locale
|
|
@@ -366,17 +371,19 @@ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLo
|
|
|
366
371
|
let content;
|
|
367
372
|
try {
|
|
368
373
|
const stringifyProcessor = unified()
|
|
369
|
-
.use(remarkStringify, {
|
|
370
|
-
bullet: '-',
|
|
371
|
-
emphasis: '_',
|
|
372
|
-
strong: '*',
|
|
373
|
-
rule: '-',
|
|
374
|
-
ruleRepetition: 3,
|
|
375
|
-
ruleSpaces: false,
|
|
376
|
-
})
|
|
377
374
|
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
378
|
-
.use(remarkMdx)
|
|
379
|
-
|
|
375
|
+
.use(remarkMdx)
|
|
376
|
+
.use(escapeHtmlInTextNodes)
|
|
377
|
+
.use(remarkStringify, {
|
|
378
|
+
handlers: {
|
|
379
|
+
// Handler to prevent escaping (avoids '<' -> '\<')
|
|
380
|
+
text(node) {
|
|
381
|
+
return node.value;
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
const outTree = stringifyProcessor.runSync(processedAst);
|
|
386
|
+
content = stringifyProcessor.stringify(outTree);
|
|
380
387
|
}
|
|
381
388
|
catch (error) {
|
|
382
389
|
console.warn(`Failed to stringify MDX content: ${error instanceof Error ? error.message : String(error)}`);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtx-cli",
|
|
3
|
-
"version": "2.2.0
|
|
3
|
+
"version": "2.2.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": "dist/main.js",
|
|
6
6
|
"files": [
|
|
@@ -87,10 +87,11 @@
|
|
|
87
87
|
"esbuild": "^0.25.4",
|
|
88
88
|
"fast-glob": "^3.3.3",
|
|
89
89
|
"form-data": "^4.0.4",
|
|
90
|
-
"generaltranslation": "^7.5.0
|
|
90
|
+
"generaltranslation": "^7.5.0",
|
|
91
91
|
"json-pointer": "^0.6.2",
|
|
92
92
|
"jsonpath-plus": "^10.3.0",
|
|
93
93
|
"jsonpointer": "^5.0.1",
|
|
94
|
+
"mdast-util-find-and-replace": "^3.0.2",
|
|
94
95
|
"micromatch": "^4.0.8",
|
|
95
96
|
"open": "^10.1.1",
|
|
96
97
|
"ora": "^8.2.0",
|