gtx-cli 2.1.0 → 2.1.2
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 +12 -0
- package/dist/utils/localizeStaticImports.js +290 -65
- package/dist/utils/localizeStaticUrls.js +321 -107
- package/package.json +8 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# gtx-cli
|
|
2
2
|
|
|
3
|
+
## 2.1.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#562](https://github.com/generaltranslation/gt/pull/562) [`8461c5e`](https://github.com/generaltranslation/gt/commit/8461c5ee2ca25cf50d4e366cb4d1e765107851fd) Thanks [@SamEggert](https://github.com/SamEggert)! - localStaticImports gracefully handles invalid MDX
|
|
8
|
+
|
|
9
|
+
## 2.1.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [#554](https://github.com/generaltranslation/gt/pull/554) [`77fb048`](https://github.com/generaltranslation/gt/commit/77fb048ab2e4432739df1c4fbabe165712e84fb3) Thanks [@SamEggert](https://github.com/SamEggert)! - use MDX AST for static imports/urls
|
|
14
|
+
|
|
3
15
|
## 2.1.0
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import { createFileMapping } from '../formats/files/fileMapping.js';
|
|
3
|
-
import { logError } from '../console/logging.js';
|
|
4
3
|
import micromatch from 'micromatch';
|
|
4
|
+
import { unified } from 'unified';
|
|
5
|
+
import remarkParse from 'remark-parse';
|
|
6
|
+
import remarkMdx from 'remark-mdx';
|
|
7
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
8
|
+
import remarkStringify from 'remark-stringify';
|
|
9
|
+
import { visit } from 'unist-util-visit';
|
|
5
10
|
const { isMatch } = micromatch;
|
|
6
11
|
/**
|
|
7
12
|
* Localizes static imports in content files.
|
|
@@ -25,7 +30,33 @@ export default async function localizeStaticImports(settings) {
|
|
|
25
30
|
const { resolvedPaths: sourceFiles } = settings.files;
|
|
26
31
|
const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, settings.defaultLocale);
|
|
27
32
|
// Process all file types at once with a single call
|
|
28
|
-
|
|
33
|
+
const processPromises = [];
|
|
34
|
+
// First, process default locale files (from source files)
|
|
35
|
+
// This is needed because they might not be in the fileMapping if they're not being translated
|
|
36
|
+
if (!fileMapping[settings.defaultLocale]) {
|
|
37
|
+
const defaultLocaleFiles = [];
|
|
38
|
+
// Collect all .md and .mdx files from sourceFiles
|
|
39
|
+
if (sourceFiles.md) {
|
|
40
|
+
defaultLocaleFiles.push(...sourceFiles.md);
|
|
41
|
+
}
|
|
42
|
+
if (sourceFiles.mdx) {
|
|
43
|
+
defaultLocaleFiles.push(...sourceFiles.mdx);
|
|
44
|
+
}
|
|
45
|
+
if (defaultLocaleFiles.length > 0) {
|
|
46
|
+
const defaultPromise = Promise.all(defaultLocaleFiles.map(async (filePath) => {
|
|
47
|
+
// Get file content
|
|
48
|
+
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
49
|
+
// Localize the file using default locale
|
|
50
|
+
const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, settings.defaultLocale, // Process as default locale
|
|
51
|
+
settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath.endsWith('.md'));
|
|
52
|
+
// Write the localized file back to the same path
|
|
53
|
+
await fs.promises.writeFile(filePath, localizedFile);
|
|
54
|
+
}));
|
|
55
|
+
processPromises.push(defaultPromise);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Then process all other locales from fileMapping
|
|
59
|
+
const mappingPromises = Object.entries(fileMapping).map(async ([locale, filesMap]) => {
|
|
29
60
|
// Get all files that are md or mdx
|
|
30
61
|
const targetFiles = Object.values(filesMap).filter((path) => path.endsWith('.md') || path.endsWith('.mdx'));
|
|
31
62
|
// Replace the placeholder path with the target path
|
|
@@ -33,83 +64,277 @@ export default async function localizeStaticImports(settings) {
|
|
|
33
64
|
// Get file content
|
|
34
65
|
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
35
66
|
// Localize the file
|
|
36
|
-
const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, locale, settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports);
|
|
67
|
+
const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, locale, settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath.endsWith('.md'));
|
|
37
68
|
// Write the localized file to the target path
|
|
38
69
|
await fs.promises.writeFile(filePath, localizedFile);
|
|
39
70
|
}));
|
|
40
|
-
})
|
|
71
|
+
});
|
|
72
|
+
processPromises.push(...mappingPromises);
|
|
73
|
+
await Promise.all(processPromises);
|
|
41
74
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
// Escape special regex characters and remove trailing slash if present
|
|
51
|
-
const escapedPatternHead = patternHead
|
|
52
|
-
.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
53
|
-
.replace(/\/$/, '');
|
|
54
|
-
let regex;
|
|
55
|
-
if (hideDefaultLocale) {
|
|
56
|
-
// Match complete import statements: `import { Foo } from '/docs/foo.md'`
|
|
57
|
-
regex = new RegExp(`import\\s+(.*?)\\s+from\\s+(["'])${escapedPatternHead}(?:/([^"']*?))?\\2`, 'g');
|
|
75
|
+
/**
|
|
76
|
+
* Determines if an import path should be processed based on pattern matching
|
|
77
|
+
*/
|
|
78
|
+
function shouldProcessImportPath(importPath, patternHead, targetLocale, defaultLocale) {
|
|
79
|
+
const patternWithoutSlash = patternHead.replace(/\/$/, '');
|
|
80
|
+
if (targetLocale === defaultLocale) {
|
|
81
|
+
// For default locale processing, check if path contains the pattern
|
|
82
|
+
return importPath.includes(patternWithoutSlash);
|
|
58
83
|
}
|
|
59
84
|
else {
|
|
60
|
-
//
|
|
61
|
-
|
|
85
|
+
// For non-default locales, check if path starts with pattern
|
|
86
|
+
return importPath.includes(patternWithoutSlash);
|
|
62
87
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks if an import path should be excluded based on exclusion patterns
|
|
91
|
+
*/
|
|
92
|
+
function isImportPathExcluded(importPath, exclude, defaultLocale) {
|
|
93
|
+
const excludePatterns = exclude.map((p) => p.replace(/\[locale\]/g, defaultLocale));
|
|
94
|
+
return excludePatterns.some((pattern) => isMatch(importPath, pattern));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Transforms import path for default locale processing
|
|
98
|
+
*/
|
|
99
|
+
function transformDefaultLocaleImportPath(fullPath, patternHead, defaultLocale, hideDefaultLocale) {
|
|
100
|
+
if (hideDefaultLocale) {
|
|
101
|
+
// Remove locale from imports that have it: '/snippets/en/file.mdx' -> '/snippets/file.mdx'
|
|
102
|
+
if (fullPath.includes(`/${defaultLocale}/`)) {
|
|
103
|
+
return fullPath.replace(`/${defaultLocale}/`, '/');
|
|
104
|
+
}
|
|
105
|
+
else if (fullPath.endsWith(`/${defaultLocale}`)) {
|
|
106
|
+
return fullPath.replace(`/${defaultLocale}`, '');
|
|
107
|
+
}
|
|
108
|
+
return null; // Path doesn't have default locale
|
|
66
109
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
: `${patternHead}${defaultLocale}/${pathContent}`;
|
|
110
|
+
else {
|
|
111
|
+
// Add locale to imports that don't have it: '/snippets/file.mdx' -> '/snippets/en/file.mdx'
|
|
112
|
+
if (fullPath.includes(`/${defaultLocale}/`) ||
|
|
113
|
+
fullPath.endsWith(`/${defaultLocale}`)) {
|
|
114
|
+
return null; // Already has default locale
|
|
115
|
+
}
|
|
116
|
+
if (fullPath.startsWith(patternHead)) {
|
|
117
|
+
const pathAfterHead = fullPath.slice(patternHead.length);
|
|
118
|
+
if (pathAfterHead) {
|
|
119
|
+
return `${patternHead}${defaultLocale}/${pathAfterHead}`;
|
|
78
120
|
}
|
|
79
121
|
else {
|
|
80
|
-
|
|
81
|
-
? `${patternHead}`
|
|
82
|
-
: `${patternHead}${defaultLocale}`;
|
|
122
|
+
return `${patternHead.replace(/\/$/, '')}/${defaultLocale}`;
|
|
83
123
|
}
|
|
84
|
-
if (exclude.some((pattern) => isMatch(matchPath, pattern))) {
|
|
85
|
-
return match; // Don't localize excluded paths
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
// get the quote type
|
|
89
|
-
quoteType = match.match(/["']/)?.[0] || '"';
|
|
90
|
-
if (!quoteType) {
|
|
91
|
-
logError(`Failed to localize static imports: Import pattern must include quotes in ${pattern}`);
|
|
92
|
-
return match;
|
|
93
124
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
125
|
+
return null; // Path doesn't match pattern
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Transforms import path for non-default locale processing with hideDefaultLocale=true
|
|
130
|
+
*/
|
|
131
|
+
function transformNonDefaultLocaleImportPathWithHidden(fullPath, patternHead, targetLocale, defaultLocale) {
|
|
132
|
+
// Check if already localized
|
|
133
|
+
if (fullPath.startsWith(`${patternHead}${targetLocale}/`) ||
|
|
134
|
+
fullPath === `${patternHead}${targetLocale}`) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// Replace default locale with target locale
|
|
138
|
+
const expectedPathWithDefaultLocale = `${patternHead}${defaultLocale}`;
|
|
139
|
+
if (fullPath.startsWith(`${expectedPathWithDefaultLocale}/`) ||
|
|
140
|
+
fullPath === expectedPathWithDefaultLocale) {
|
|
141
|
+
return fullPath.replace(`${patternHead}${defaultLocale}`, `${patternHead}${targetLocale}`);
|
|
142
|
+
}
|
|
143
|
+
// Handle exact pattern match
|
|
144
|
+
if (fullPath === patternHead.replace(/\/$/, '')) {
|
|
145
|
+
return `${patternHead.replace(/\/$/, '')}/${targetLocale}`;
|
|
146
|
+
}
|
|
147
|
+
// Add target locale to path without any locale
|
|
148
|
+
const pathAfterHead = fullPath.slice(patternHead.length);
|
|
149
|
+
return pathAfterHead
|
|
150
|
+
? `${patternHead}${targetLocale}/${pathAfterHead}`
|
|
151
|
+
: `${patternHead}${targetLocale}`;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Transforms import path for non-default locale processing with hideDefaultLocale=false
|
|
155
|
+
*/
|
|
156
|
+
function transformNonDefaultLocaleImportPath(fullPath, patternHead, targetLocale, defaultLocale) {
|
|
157
|
+
const expectedPathWithLocale = `${patternHead}${defaultLocale}`;
|
|
158
|
+
if (fullPath.startsWith(`${expectedPathWithLocale}/`) ||
|
|
159
|
+
fullPath === expectedPathWithLocale) {
|
|
160
|
+
// Replace existing default locale with target locale
|
|
161
|
+
return fullPath.replace(`${patternHead}${defaultLocale}`, `${patternHead}${targetLocale}`);
|
|
162
|
+
}
|
|
163
|
+
else if (fullPath.startsWith(patternHead)) {
|
|
164
|
+
// Add target locale to path that doesn't have any locale
|
|
165
|
+
const pathAfterHead = fullPath.slice(patternHead.length);
|
|
166
|
+
if (pathAfterHead) {
|
|
167
|
+
return `${patternHead}${targetLocale}/${pathAfterHead}`;
|
|
107
168
|
}
|
|
108
169
|
else {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
170
|
+
return `${patternHead.replace(/\/$/, '')}/${targetLocale}`;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return null; // Path doesn't match pattern
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Main import path transformation function that delegates to specific scenarios
|
|
177
|
+
*/
|
|
178
|
+
function transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale) {
|
|
179
|
+
if (targetLocale === defaultLocale) {
|
|
180
|
+
return transformDefaultLocaleImportPath(fullPath, patternHead, defaultLocale, hideDefaultLocale);
|
|
181
|
+
}
|
|
182
|
+
else if (hideDefaultLocale) {
|
|
183
|
+
return transformNonDefaultLocaleImportPathWithHidden(fullPath, patternHead, targetLocale, defaultLocale);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
return transformNonDefaultLocaleImportPath(fullPath, patternHead, targetLocale, defaultLocale);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* AST-based transformation for MDX files using remark-mdx
|
|
191
|
+
*/
|
|
192
|
+
function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', exclude = []) {
|
|
193
|
+
const transformedImports = [];
|
|
194
|
+
if (!pattern.startsWith('/')) {
|
|
195
|
+
pattern = '/' + pattern;
|
|
196
|
+
}
|
|
197
|
+
const patternHead = pattern.split('[locale]')[0];
|
|
198
|
+
// Quick check: if the file doesn't contain the pattern, skip expensive AST parsing
|
|
199
|
+
// For default locale processing, we also need to check if content might need adjustment
|
|
200
|
+
if (targetLocale === defaultLocale) {
|
|
201
|
+
// For default locale files, we always need to check as we're looking for either:
|
|
202
|
+
// - paths without locale (when hideDefaultLocale=false)
|
|
203
|
+
// - paths with default locale (when hideDefaultLocale=true)
|
|
204
|
+
const patternWithoutSlash = patternHead.replace(/\/$/, '');
|
|
205
|
+
if (!mdxContent.includes(patternWithoutSlash)) {
|
|
206
|
+
return {
|
|
207
|
+
content: mdxContent,
|
|
208
|
+
hasChanges: false,
|
|
209
|
+
transformedImports: [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
// For non-default locales, use the original logic
|
|
215
|
+
if (!mdxContent.includes(patternHead.replace(/\/$/, ''))) {
|
|
216
|
+
return {
|
|
217
|
+
content: mdxContent,
|
|
218
|
+
hasChanges: false,
|
|
219
|
+
transformedImports: [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Parse the MDX content into an AST
|
|
224
|
+
let processedAst;
|
|
225
|
+
try {
|
|
226
|
+
const parseProcessor = unified()
|
|
227
|
+
.use(remarkParse)
|
|
228
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
229
|
+
.use(remarkMdx);
|
|
230
|
+
const ast = parseProcessor.parse(mdxContent);
|
|
231
|
+
processedAst = parseProcessor.runSync(ast);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
console.warn(`Failed to parse MDX content: ${error instanceof Error ? error.message : String(error)}`);
|
|
235
|
+
console.warn('Returning original content unchanged due to parsing error.');
|
|
236
|
+
return {
|
|
237
|
+
content: mdxContent,
|
|
238
|
+
hasChanges: false,
|
|
239
|
+
transformedImports: [],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Visit only mdxjsEsm nodes (import/export statements)
|
|
243
|
+
visit(processedAst, 'mdxjsEsm', (node) => {
|
|
244
|
+
if (node.value && node.value.includes(patternHead.replace(/\/$/, ''))) {
|
|
245
|
+
// Find and transform import paths in the node value
|
|
246
|
+
const lines = node.value.split('\n');
|
|
247
|
+
const transformedLines = lines.map((line) => {
|
|
248
|
+
// Only process import lines that match our pattern
|
|
249
|
+
if (!line.trim().startsWith('import ')) {
|
|
250
|
+
return line;
|
|
251
|
+
}
|
|
252
|
+
// Check if this line should be processed
|
|
253
|
+
if (!shouldProcessImportPath(line, patternHead, targetLocale, defaultLocale)) {
|
|
254
|
+
return line;
|
|
255
|
+
}
|
|
256
|
+
// Extract the path from the import statement
|
|
257
|
+
const quotes = ['"', "'", '`'];
|
|
258
|
+
let transformedLine = line;
|
|
259
|
+
for (const quote of quotes) {
|
|
260
|
+
// Try both with and without trailing slash
|
|
261
|
+
let startPattern = `${quote}${patternHead}`;
|
|
262
|
+
let startIndex = line.indexOf(startPattern);
|
|
263
|
+
// If pattern has trailing slash but path doesn't, try without slash
|
|
264
|
+
if (startIndex === -1 && patternHead.endsWith('/')) {
|
|
265
|
+
const patternWithoutSlash = patternHead.slice(0, -1);
|
|
266
|
+
startPattern = `${quote}${patternWithoutSlash}`;
|
|
267
|
+
startIndex = line.indexOf(startPattern);
|
|
268
|
+
}
|
|
269
|
+
if (startIndex === -1)
|
|
270
|
+
continue;
|
|
271
|
+
const pathStart = startIndex + 1; // After the quote
|
|
272
|
+
const pathEnd = line.indexOf(quote, pathStart);
|
|
273
|
+
if (pathEnd === -1)
|
|
274
|
+
continue;
|
|
275
|
+
const fullPath = line.slice(pathStart, pathEnd);
|
|
276
|
+
// Transform the import path
|
|
277
|
+
const newPath = transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale);
|
|
278
|
+
if (!newPath) {
|
|
279
|
+
continue; // No transformation needed
|
|
280
|
+
}
|
|
281
|
+
// Check exclusions
|
|
282
|
+
if (isImportPathExcluded(fullPath, exclude, defaultLocale)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
// Apply the transformation
|
|
286
|
+
transformedImports.push({ originalPath: fullPath, newPath });
|
|
287
|
+
transformedLine =
|
|
288
|
+
line.slice(0, pathStart) + newPath + line.slice(pathEnd);
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
return transformedLine;
|
|
292
|
+
});
|
|
293
|
+
node.value = transformedLines.join('\n');
|
|
112
294
|
}
|
|
113
295
|
});
|
|
114
|
-
|
|
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
|
+
return {
|
|
323
|
+
content,
|
|
324
|
+
hasChanges: transformedImports.length > 0,
|
|
325
|
+
transformedImports,
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* AST-based transformation for MDX files only
|
|
330
|
+
*/
|
|
331
|
+
function localizeStaticImportsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', // eg /docs/[locale] or /[locale]
|
|
332
|
+
exclude = [], isMarkdown = false) {
|
|
333
|
+
// Skip .md files entirely - they cannot have imports
|
|
334
|
+
if (isMarkdown) {
|
|
335
|
+
return file;
|
|
336
|
+
}
|
|
337
|
+
// For MDX files, use AST-based transformation
|
|
338
|
+
const result = transformMdxImports(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude);
|
|
339
|
+
return result.content;
|
|
115
340
|
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import * as fs from 'fs';
|
|
2
2
|
import { createFileMapping } from '../formats/files/fileMapping.js';
|
|
3
3
|
import micromatch from 'micromatch';
|
|
4
|
+
import { unified } from 'unified';
|
|
5
|
+
import remarkParse from 'remark-parse';
|
|
6
|
+
import remarkMdx from 'remark-mdx';
|
|
7
|
+
import remarkFrontmatter from 'remark-frontmatter';
|
|
8
|
+
import remarkStringify from 'remark-stringify';
|
|
9
|
+
import { visit } from 'unist-util-visit';
|
|
4
10
|
const { isMatch } = micromatch;
|
|
5
11
|
/**
|
|
6
12
|
* Localizes static urls in content files.
|
|
@@ -24,147 +30,355 @@ export default async function localizeStaticUrls(settings) {
|
|
|
24
30
|
const { resolvedPaths: sourceFiles } = settings.files;
|
|
25
31
|
const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, settings.defaultLocale);
|
|
26
32
|
// Process all file types at once with a single call
|
|
27
|
-
|
|
33
|
+
const processPromises = [];
|
|
34
|
+
// First, process default locale files (from source files)
|
|
35
|
+
// This is needed because they might not be in the fileMapping if they're not being translated
|
|
36
|
+
if (!fileMapping[settings.defaultLocale]) {
|
|
37
|
+
const defaultLocaleFiles = [];
|
|
38
|
+
// Collect all .md and .mdx files from sourceFiles
|
|
39
|
+
if (sourceFiles.md) {
|
|
40
|
+
defaultLocaleFiles.push(...sourceFiles.md);
|
|
41
|
+
}
|
|
42
|
+
if (sourceFiles.mdx) {
|
|
43
|
+
defaultLocaleFiles.push(...sourceFiles.mdx);
|
|
44
|
+
}
|
|
45
|
+
if (defaultLocaleFiles.length > 0) {
|
|
46
|
+
const defaultPromise = Promise.all(defaultLocaleFiles.map(async (filePath) => {
|
|
47
|
+
// Get file content
|
|
48
|
+
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
49
|
+
// Localize the file using default locale
|
|
50
|
+
const localizedFile = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, settings.defaultLocale, // Process as default locale
|
|
51
|
+
settings.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern, settings.options?.excludeStaticUrls);
|
|
52
|
+
// Write the localized file back to the same path
|
|
53
|
+
await fs.promises.writeFile(filePath, localizedFile);
|
|
54
|
+
}));
|
|
55
|
+
processPromises.push(defaultPromise);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Then process all other locales from fileMapping
|
|
59
|
+
const mappingPromises = Object.entries(fileMapping).map(async ([locale, filesMap]) => {
|
|
28
60
|
// Get all files that are md or mdx
|
|
29
61
|
const targetFiles = Object.values(filesMap).filter((path) => path.endsWith('.md') || path.endsWith('.mdx'));
|
|
30
62
|
// Replace the placeholder path with the target path
|
|
31
63
|
await Promise.all(targetFiles.map(async (filePath) => {
|
|
32
64
|
// Get file content
|
|
33
65
|
const fileContent = await fs.promises.readFile(filePath, 'utf8');
|
|
34
|
-
// Localize the file
|
|
66
|
+
// Localize the file (handles both URLs and hrefs in single AST pass)
|
|
35
67
|
const localizedFile = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, locale, settings.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern, settings.options?.excludeStaticUrls);
|
|
36
|
-
const localizedFileHrefs = localizeStaticHrefsForFile(localizedFile, settings.defaultLocale, locale, settings.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern, settings.options?.excludeStaticUrls);
|
|
37
68
|
// Write the localized file to the target path
|
|
38
|
-
await fs.promises.writeFile(filePath,
|
|
69
|
+
await fs.promises.writeFile(filePath, localizedFile);
|
|
39
70
|
}));
|
|
40
|
-
})
|
|
71
|
+
});
|
|
72
|
+
processPromises.push(...mappingPromises);
|
|
73
|
+
await Promise.all(processPromises);
|
|
41
74
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
75
|
+
/**
|
|
76
|
+
* Determines if a URL should be processed based on pattern matching
|
|
77
|
+
*/
|
|
78
|
+
function shouldProcessUrl(originalUrl, patternHead, targetLocale, defaultLocale) {
|
|
79
|
+
const patternWithoutSlash = patternHead.replace(/\/$/, '');
|
|
80
|
+
if (targetLocale === defaultLocale) {
|
|
81
|
+
// For default locale processing, check if URL contains the pattern
|
|
82
|
+
return originalUrl.includes(patternWithoutSlash);
|
|
47
83
|
}
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
84
|
+
else {
|
|
85
|
+
// For non-default locales, check if URL starts with pattern
|
|
86
|
+
return originalUrl.startsWith(patternWithoutSlash);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Checks if a URL should be excluded based on exclusion patterns
|
|
91
|
+
*/
|
|
92
|
+
function isUrlExcluded(originalUrl, exclude, defaultLocale) {
|
|
93
|
+
const excludePatterns = exclude.map((p) => p.replace(/\[locale\]/g, defaultLocale));
|
|
94
|
+
return excludePatterns.some((pattern) => isMatch(originalUrl, pattern));
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Transforms URL for default locale processing
|
|
98
|
+
*/
|
|
99
|
+
function transformDefaultLocaleUrl(originalUrl, patternHead, defaultLocale, hideDefaultLocale) {
|
|
55
100
|
if (hideDefaultLocale) {
|
|
56
|
-
//
|
|
57
|
-
|
|
101
|
+
// Remove locale from URLs that have it: '/docs/en/file' -> '/docs/file'
|
|
102
|
+
if (originalUrl.includes(`/${defaultLocale}/`)) {
|
|
103
|
+
return originalUrl.replace(`/${defaultLocale}/`, '/');
|
|
104
|
+
}
|
|
105
|
+
else if (originalUrl.endsWith(`/${defaultLocale}`)) {
|
|
106
|
+
return originalUrl.replace(`/${defaultLocale}`, '');
|
|
107
|
+
}
|
|
108
|
+
return null; // URL doesn't have default locale
|
|
58
109
|
}
|
|
59
110
|
else {
|
|
60
|
-
//
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
const localizedFile = file.replace(regex, (match, pathContent) => {
|
|
70
|
-
if (exclude.length > 0) {
|
|
71
|
-
// Check if this path should be excluded from localization
|
|
72
|
-
let matchPath = '';
|
|
73
|
-
if (pathContent) {
|
|
74
|
-
matchPath = hideDefaultLocale
|
|
75
|
-
? `${patternHead}${pathContent}`
|
|
76
|
-
: `${patternHead}${defaultLocale}/${pathContent}`;
|
|
111
|
+
// Add locale to URLs that don't have it: '/docs/file' -> '/docs/en/file'
|
|
112
|
+
if (originalUrl.includes(`/${defaultLocale}/`) ||
|
|
113
|
+
originalUrl.endsWith(`/${defaultLocale}`)) {
|
|
114
|
+
return null; // Already has default locale
|
|
115
|
+
}
|
|
116
|
+
if (originalUrl.startsWith(patternHead)) {
|
|
117
|
+
const pathAfterHead = originalUrl.slice(patternHead.length);
|
|
118
|
+
if (pathAfterHead) {
|
|
119
|
+
return `${patternHead}${defaultLocale}/${pathAfterHead}`;
|
|
77
120
|
}
|
|
78
121
|
else {
|
|
79
|
-
|
|
80
|
-
? `${patternHead}`
|
|
81
|
-
: `${patternHead}${defaultLocale}`;
|
|
82
|
-
}
|
|
83
|
-
if (exclude.some((pattern) => isMatch(matchPath, pattern))) {
|
|
84
|
-
return match; // Don't localize excluded paths
|
|
122
|
+
return `${patternHead.replace(/\/$/, '')}/${defaultLocale}`;
|
|
85
123
|
}
|
|
86
124
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
125
|
+
return null; // URL doesn't match pattern
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Transforms URL for non-default locale processing with hideDefaultLocale=true
|
|
130
|
+
*/
|
|
131
|
+
function transformNonDefaultLocaleUrlWithHidden(originalUrl, patternHead, targetLocale, defaultLocale) {
|
|
132
|
+
// Check if already localized
|
|
133
|
+
if (originalUrl.startsWith(`${patternHead}${targetLocale}/`) ||
|
|
134
|
+
originalUrl === `${patternHead}${targetLocale}`) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
// Replace default locale with target locale
|
|
138
|
+
const expectedPathWithDefaultLocale = `${patternHead}${defaultLocale}`;
|
|
139
|
+
if (originalUrl.startsWith(`${expectedPathWithDefaultLocale}/`) ||
|
|
140
|
+
originalUrl === expectedPathWithDefaultLocale) {
|
|
141
|
+
return originalUrl.replace(`${patternHead}${defaultLocale}`, `${patternHead}${targetLocale}`);
|
|
142
|
+
}
|
|
143
|
+
// Handle exact pattern match
|
|
144
|
+
if (originalUrl === patternHead.replace(/\/$/, '')) {
|
|
145
|
+
return `${patternHead.replace(/\/$/, '')}/${targetLocale}`;
|
|
146
|
+
}
|
|
147
|
+
// Add target locale to URL without any locale
|
|
148
|
+
const pathAfterHead = originalUrl.slice(patternHead.length);
|
|
149
|
+
return pathAfterHead
|
|
150
|
+
? `${patternHead}${targetLocale}/${pathAfterHead}`
|
|
151
|
+
: `${patternHead}${targetLocale}`;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Transforms URL for non-default locale processing with hideDefaultLocale=false
|
|
155
|
+
*/
|
|
156
|
+
function transformNonDefaultLocaleUrl(originalUrl, patternHead, targetLocale, defaultLocale) {
|
|
157
|
+
const expectedPathWithLocale = `${patternHead}${defaultLocale}`;
|
|
158
|
+
if (originalUrl.startsWith(`${expectedPathWithLocale}/`) ||
|
|
159
|
+
originalUrl === expectedPathWithLocale) {
|
|
160
|
+
// Replace existing default locale with target locale
|
|
161
|
+
return originalUrl.replace(`${patternHead}${defaultLocale}`, `${patternHead}${targetLocale}`);
|
|
162
|
+
}
|
|
163
|
+
else if (originalUrl.startsWith(patternHead)) {
|
|
164
|
+
// Add target locale to URL that doesn't have any locale
|
|
165
|
+
const pathAfterHead = originalUrl.slice(patternHead.length);
|
|
166
|
+
if (pathAfterHead) {
|
|
167
|
+
return `${patternHead}${targetLocale}/${pathAfterHead}`;
|
|
100
168
|
}
|
|
101
169
|
else {
|
|
102
|
-
|
|
103
|
-
// pathContent contains everything after the default locale (no leading slash if present)
|
|
104
|
-
const result = `](${patternHead}${targetLocale}${pathContent ? '/' + pathContent : ''})`;
|
|
105
|
-
return result;
|
|
170
|
+
return `${patternHead.replace(/\/$/, '')}/${targetLocale}`;
|
|
106
171
|
}
|
|
107
|
-
}
|
|
108
|
-
return
|
|
172
|
+
}
|
|
173
|
+
return null; // URL doesn't match pattern
|
|
109
174
|
}
|
|
110
|
-
|
|
111
|
-
|
|
175
|
+
/**
|
|
176
|
+
* Main URL transformation function that delegates to specific scenarios
|
|
177
|
+
*/
|
|
178
|
+
function transformUrlPath(originalUrl, patternHead, targetLocale, defaultLocale, hideDefaultLocale) {
|
|
179
|
+
if (targetLocale === defaultLocale) {
|
|
180
|
+
return transformDefaultLocaleUrl(originalUrl, patternHead, defaultLocale, hideDefaultLocale);
|
|
181
|
+
}
|
|
182
|
+
else if (hideDefaultLocale) {
|
|
183
|
+
return transformNonDefaultLocaleUrlWithHidden(originalUrl, patternHead, targetLocale, defaultLocale);
|
|
184
|
+
}
|
|
185
|
+
else {
|
|
186
|
+
return transformNonDefaultLocaleUrl(originalUrl, patternHead, targetLocale, defaultLocale);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* AST-based transformation for MDX files using remark-mdx
|
|
191
|
+
*/
|
|
192
|
+
function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', exclude = []) {
|
|
193
|
+
const transformedUrls = [];
|
|
112
194
|
if (!pattern.startsWith('/')) {
|
|
113
195
|
pattern = '/' + pattern;
|
|
114
196
|
}
|
|
115
|
-
// 1. Search for all instances of:
|
|
116
197
|
const patternHead = pattern.split('[locale]')[0];
|
|
117
|
-
//
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
198
|
+
// Quick check: if the file doesn't contain the pattern, skip expensive AST parsing
|
|
199
|
+
// For default locale processing, we also need to check if content might need adjustment
|
|
200
|
+
if (targetLocale === defaultLocale) {
|
|
201
|
+
// For default locale files, we always need to check as we're looking for either:
|
|
202
|
+
// - paths without locale (when hideDefaultLocale=false)
|
|
203
|
+
// - paths with default locale (when hideDefaultLocale=true)
|
|
204
|
+
const patternWithoutSlash = patternHead.replace(/\/$/, '');
|
|
205
|
+
if (!mdxContent.includes(patternWithoutSlash)) {
|
|
206
|
+
return {
|
|
207
|
+
content: mdxContent,
|
|
208
|
+
hasChanges: false,
|
|
209
|
+
transformedUrls: [],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
125
212
|
}
|
|
126
213
|
else {
|
|
127
|
-
//
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
214
|
+
// For non-default locales, use the original logic
|
|
215
|
+
if (!mdxContent.includes(patternHead.replace(/\/$/, ''))) {
|
|
216
|
+
return {
|
|
217
|
+
content: mdxContent,
|
|
218
|
+
hasChanges: false,
|
|
219
|
+
transformedUrls: [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
// Parse the MDX content into an AST
|
|
224
|
+
let processedAst;
|
|
225
|
+
try {
|
|
226
|
+
const parseProcessor = unified()
|
|
227
|
+
.use(remarkParse)
|
|
228
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
229
|
+
.use(remarkMdx);
|
|
230
|
+
const ast = parseProcessor.parse(mdxContent);
|
|
231
|
+
processedAst = parseProcessor.runSync(ast);
|
|
232
|
+
}
|
|
233
|
+
catch (error) {
|
|
234
|
+
console.warn(`Failed to parse MDX content: ${error instanceof Error ? error.message : String(error)}`);
|
|
235
|
+
console.warn('Returning original content unchanged due to parsing error.');
|
|
236
|
+
return {
|
|
237
|
+
content: mdxContent,
|
|
238
|
+
hasChanges: false,
|
|
239
|
+
transformedUrls: [],
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// Helper function to transform URL based on pattern
|
|
243
|
+
const transformUrl = (originalUrl, linkType) => {
|
|
244
|
+
// Check if URL should be processed
|
|
245
|
+
if (!shouldProcessUrl(originalUrl, patternHead, targetLocale, defaultLocale)) {
|
|
246
|
+
return null;
|
|
247
|
+
}
|
|
248
|
+
// Transform the URL based on locale and configuration
|
|
249
|
+
const newUrl = transformUrlPath(originalUrl, patternHead, targetLocale, defaultLocale, hideDefaultLocale);
|
|
250
|
+
if (!newUrl) {
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
// Check exclusions
|
|
254
|
+
if (isUrlExcluded(originalUrl, exclude, defaultLocale)) {
|
|
255
|
+
return null;
|
|
256
|
+
}
|
|
257
|
+
transformedUrls.push({
|
|
258
|
+
originalPath: originalUrl,
|
|
259
|
+
newPath: newUrl,
|
|
260
|
+
type: linkType,
|
|
261
|
+
});
|
|
262
|
+
return newUrl;
|
|
263
|
+
};
|
|
264
|
+
// Visit markdown link nodes: [text](url)
|
|
265
|
+
visit(processedAst, 'link', (node) => {
|
|
266
|
+
if (node.url) {
|
|
267
|
+
const newUrl = transformUrl(node.url, 'markdown');
|
|
268
|
+
if (newUrl) {
|
|
269
|
+
node.url = newUrl;
|
|
144
270
|
}
|
|
145
|
-
|
|
146
|
-
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
// Visit JSX/HTML elements for href attributes: <a href="url"> or <Card href="url">
|
|
274
|
+
visit(processedAst, ['mdxJsxFlowElement', 'mdxJsxTextElement'], (node) => {
|
|
275
|
+
const jsxNode = node;
|
|
276
|
+
if (jsxNode.attributes) {
|
|
277
|
+
for (const attr of jsxNode.attributes) {
|
|
278
|
+
if (attr.type === 'mdxJsxAttribute' &&
|
|
279
|
+
attr.name === 'href' &&
|
|
280
|
+
attr.value) {
|
|
281
|
+
// Handle MdxJsxAttribute with string or MdxJsxAttributeValueExpression
|
|
282
|
+
const hrefValue = typeof attr.value === 'string' ? attr.value : attr.value.value;
|
|
283
|
+
if (typeof hrefValue === 'string') {
|
|
284
|
+
const newUrl = transformUrl(hrefValue, 'href');
|
|
285
|
+
if (newUrl) {
|
|
286
|
+
if (typeof attr.value === 'string') {
|
|
287
|
+
attr.value = newUrl;
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
attr.value.value = newUrl;
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
147
295
|
}
|
|
148
296
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
297
|
+
});
|
|
298
|
+
// Visit raw JSX nodes for href attributes in JSX strings
|
|
299
|
+
visit(processedAst, 'jsx', (node) => {
|
|
300
|
+
if (node.value && typeof node.value === 'string') {
|
|
301
|
+
const jsxContent = node.value;
|
|
302
|
+
// Use regex to find href attributes in the JSX string
|
|
303
|
+
const hrefRegex = /href\s*=\s*["']([^"']+)["']/g;
|
|
304
|
+
let match;
|
|
305
|
+
const replacements = [];
|
|
306
|
+
// Reset regex lastIndex to avoid issues with global flag
|
|
307
|
+
hrefRegex.lastIndex = 0;
|
|
308
|
+
while ((match = hrefRegex.exec(jsxContent)) !== null) {
|
|
309
|
+
const originalHref = match[1];
|
|
310
|
+
const newUrl = transformUrl(originalHref, 'href');
|
|
311
|
+
if (newUrl) {
|
|
312
|
+
// Store replacement info
|
|
313
|
+
const oldHrefAttr = match[0]; // The full match like 'href="/quickstart"'
|
|
314
|
+
const quote = oldHrefAttr.includes('"') ? '"' : "'";
|
|
315
|
+
const newHrefAttr = `href=${quote}${newUrl}${quote}`;
|
|
316
|
+
replacements.push({
|
|
317
|
+
start: match.index,
|
|
318
|
+
end: match.index + oldHrefAttr.length,
|
|
319
|
+
oldHrefAttr,
|
|
320
|
+
newHrefAttr,
|
|
321
|
+
});
|
|
155
322
|
}
|
|
156
323
|
}
|
|
157
|
-
//
|
|
158
|
-
if (
|
|
159
|
-
|
|
324
|
+
// Apply replacements in reverse order (from end to start) to avoid position shifts
|
|
325
|
+
if (replacements.length > 0) {
|
|
326
|
+
let newJsxContent = jsxContent;
|
|
327
|
+
replacements
|
|
328
|
+
.sort((a, b) => b.start - a.start)
|
|
329
|
+
.forEach(({ start, end, newHrefAttr }) => {
|
|
330
|
+
newJsxContent =
|
|
331
|
+
newJsxContent.slice(0, start) +
|
|
332
|
+
newHrefAttr +
|
|
333
|
+
newJsxContent.slice(end);
|
|
334
|
+
});
|
|
335
|
+
node.value = newJsxContent;
|
|
160
336
|
}
|
|
161
|
-
return `href="${patternHead}${targetLocale}/${pathContent}"`;
|
|
162
|
-
}
|
|
163
|
-
else {
|
|
164
|
-
// For non-hideDefaultLocale, replace defaultLocale with targetLocale
|
|
165
|
-
// pathContent contains everything after the default locale (no leading slash if present)
|
|
166
|
-
return `href="${patternHead}${targetLocale}${pathContent ? '/' + pathContent : ''}"`;
|
|
167
337
|
}
|
|
168
338
|
});
|
|
169
|
-
|
|
339
|
+
// Convert the modified AST back to MDX string
|
|
340
|
+
let content;
|
|
341
|
+
try {
|
|
342
|
+
const stringifyProcessor = unified()
|
|
343
|
+
.use(remarkStringify, {
|
|
344
|
+
bullet: '-',
|
|
345
|
+
emphasis: '_',
|
|
346
|
+
strong: '*',
|
|
347
|
+
rule: '-',
|
|
348
|
+
ruleRepetition: 3,
|
|
349
|
+
ruleSpaces: false,
|
|
350
|
+
})
|
|
351
|
+
.use(remarkFrontmatter, ['yaml', 'toml'])
|
|
352
|
+
.use(remarkMdx);
|
|
353
|
+
content = stringifyProcessor.stringify(processedAst);
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
console.warn(`Failed to stringify MDX content: ${error instanceof Error ? error.message : String(error)}`);
|
|
357
|
+
console.warn('Returning original content unchanged due to stringify error.');
|
|
358
|
+
return {
|
|
359
|
+
content: mdxContent,
|
|
360
|
+
hasChanges: false,
|
|
361
|
+
transformedUrls: [],
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
// Handle newline formatting to match original input
|
|
365
|
+
if (content.endsWith('\n') && !mdxContent.endsWith('\n')) {
|
|
366
|
+
content = content.slice(0, -1);
|
|
367
|
+
}
|
|
368
|
+
// Preserve leading newlines from original content
|
|
369
|
+
if (mdxContent.startsWith('\n') && !content.startsWith('\n')) {
|
|
370
|
+
content = '\n' + content;
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
content,
|
|
374
|
+
hasChanges: transformedUrls.length > 0,
|
|
375
|
+
transformedUrls,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
// AST-based transformation for MDX files using remark
|
|
379
|
+
function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', // eg /docs/[locale] or /[locale]
|
|
380
|
+
exclude = []) {
|
|
381
|
+
// Use AST-based transformation for MDX files
|
|
382
|
+
const result = transformMdxUrls(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude);
|
|
383
|
+
return result.content;
|
|
170
384
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gtx-cli",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": "dist/main.js",
|
|
6
6
|
"files": [
|
|
@@ -94,8 +94,14 @@
|
|
|
94
94
|
"micromatch": "^4.0.8",
|
|
95
95
|
"open": "^10.1.1",
|
|
96
96
|
"ora": "^8.2.0",
|
|
97
|
+
"remark-frontmatter": "^5.0.0",
|
|
98
|
+
"remark-mdx": "^3.1.0",
|
|
99
|
+
"remark-parse": "^11.0.0",
|
|
100
|
+
"remark-stringify": "^11.0.0",
|
|
97
101
|
"resolve": "^1.22.10",
|
|
98
102
|
"tsconfig-paths": "^4.2.0",
|
|
103
|
+
"unified": "^11.0.5",
|
|
104
|
+
"unist-util-visit": "^5.0.0",
|
|
99
105
|
"yaml": "^2.8.0"
|
|
100
106
|
},
|
|
101
107
|
"devDependencies": {
|
|
@@ -109,6 +115,7 @@
|
|
|
109
115
|
"@types/resolve": "^1.20.2",
|
|
110
116
|
"eslint": "^9.20.0",
|
|
111
117
|
"esm": "^3.2.25",
|
|
118
|
+
"mdast": "^3.0.0",
|
|
112
119
|
"prettier": "^3.4.2",
|
|
113
120
|
"ts-node": "^10.9.2",
|
|
114
121
|
"typescript": "^5.5.4"
|