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 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.map((filePath) => {
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.map((filePath) => {
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
- const errorMsg = validation.error ? `: ${validation.error}` : '';
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) => file !== null);
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(), 'bundled-dictionary.js');
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
- .use(remarkFrontmatter, ['yaml', 'toml'])
191
- .use(remarkMdx);
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; &#123; &#x1A2B;
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, '&amp;'],
26
+ [/</g, '&lt;'],
27
+ [/>/g, '&gt;'],
28
+ [/"/g, '&quot;'],
29
+ [/'/g, '&#39;'],
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.endsWith('.md'));
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.endsWith('.md'));
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
- return transformDefaultLocaleImportPath(fullPath, patternHead, defaultLocale, hideDefaultLocale);
190
+ newPath = transformDefaultLocaleImportPath(fullPath, patternHead, defaultLocale, hideDefaultLocale);
181
191
  }
182
192
  else if (hideDefaultLocale) {
183
- return transformNonDefaultLocaleImportPathWithHidden(fullPath, patternHead, targetLocale, defaultLocale);
193
+ newPath = transformNonDefaultLocaleImportPathWithHidden(fullPath, patternHead, targetLocale, defaultLocale);
184
194
  }
185
195
  else {
186
- return transformNonDefaultLocaleImportPath(fullPath, patternHead, targetLocale, defaultLocale);
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
- if (!pattern.startsWith('/')) {
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
- // Visit only mdxjsEsm nodes (import/export statements)
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 and transform import paths in the node value
275
+ // Find import lines that need transformation
246
276
  const lines = node.value.split('\n');
247
- const transformedLines = lines.map((line) => {
277
+ lines.forEach((line) => {
248
278
  // Only process import lines that match our pattern
249
279
  if (!line.trim().startsWith('import ')) {
250
- return line;
280
+ return;
251
281
  }
252
282
  // Check if this line should be processed
253
283
  if (!shouldProcessImportPath(line, patternHead, targetLocale, defaultLocale)) {
254
- return line;
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 = [], isMarkdown = false) {
333
+ exclude = [], currentFilePath) {
333
334
  // Skip .md files entirely - they cannot have imports
334
- if (isMarkdown) {
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
- content = stringifyProcessor.stringify(processedAst);
375
+ .use(remarkMdx)
376
+ .use(escapeHtmlInTextNodes)
377
+ .use(remarkStringify, {
378
+ handlers: {
379
+ // Handler to prevent escaping (avoids '&lt;' -> '\&lt;')
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-alpha.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-alpha.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",