gtx-cli 2.5.13 → 2.5.14

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,13 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.5.14
4
+
5
+ ### Patch Changes
6
+
7
+ - [#837](https://github.com/generaltranslation/gt/pull/837) [`0772b57`](https://github.com/generaltranslation/gt/commit/0772b5714f1cfe8af5f5edcdf6bcb28125a1536f) Thanks [@fernando-aviles](https://github.com/fernando-aviles)! - Making experimentalAddHeaderAnchorIds independent of experimentalLocalizeStaticUrls and fetching anchor IDs from source files when present
8
+
9
+ - [#835](https://github.com/generaltranslation/gt/pull/835) [`79225fb`](https://github.com/generaltranslation/gt/commit/79225fb3bbea3bb7a453cc237c619b67dd0dd3da) Thanks [@brian-lou](https://github.com/brian-lou)! - When using --force translate, also force files to re-download
10
+
3
11
  ## 2.5.13
4
12
 
5
13
  ### Patch Changes
@@ -14,7 +14,8 @@ export async function handleTranslate(options, settings, fileVersionData, jobDat
14
14
  const { resolvedPaths, placeholderPaths, transformPaths } = settings.files;
15
15
  const fileMapping = createFileMapping(resolvedPaths, placeholderPaths, transformPaths, settings.locales, settings.defaultLocale);
16
16
  // Check for remaining translations
17
- await downloadTranslations(fileVersionData, jobData, branchData, settings.locales, options.timeout, (sourcePath, locale) => fileMapping[locale]?.[sourcePath] ?? null, settings, options.force, options.forceDownload);
17
+ await downloadTranslations(fileVersionData, jobData, branchData, settings.locales, options.timeout, (sourcePath, locale) => fileMapping[locale]?.[sourcePath] ?? null, settings, options.force, options.forceDownload || options.force // if force is true should also force download
18
+ );
18
19
  }
19
20
  }
20
21
  // Downloads translations that were originally staged
@@ -31,7 +32,7 @@ export async function handleDownload(options, settings) {
31
32
  const stagedVersionData = await getStagedVersions(settings.configDirectory);
32
33
  // Check for remaining translations
33
34
  await downloadTranslations(stagedVersionData, undefined, undefined, settings.locales, options.timeout, (sourcePath, locale) => fileMapping[locale][sourcePath] ?? null, settings, false, // force is not applicable for downloading staged translations
34
- options.forceDownload);
35
+ options.force || options.forceDownload);
35
36
  }
36
37
  export async function postProcessTranslations(settings, includeFiles) {
37
38
  // Localize static urls (/docs -> /[locale]/docs) and preserve anchor IDs for non-default locales
@@ -41,8 +42,12 @@ export async function postProcessTranslations(settings, includeFiles) {
41
42
  if (nonDefaultLocales.length > 0) {
42
43
  await localizeStaticUrls(settings, nonDefaultLocales, includeFiles);
43
44
  }
44
- // Add explicit anchor IDs to translated MDX/MD files to preserve navigation
45
- // Uses inline {#id} format by default, or div wrapping if experimentalAddHeaderAnchorIds is 'mintlify'
45
+ }
46
+ const shouldProcessAnchorIds = settings.options?.experimentalLocalizeStaticUrls ||
47
+ settings.options?.experimentalAddHeaderAnchorIds;
48
+ // Add explicit anchor IDs to translated MDX/MD files to preserve navigation
49
+ // Uses inline {#id} format by default, or div wrapping if experimentalAddHeaderAnchorIds is 'mintlify'
50
+ if (shouldProcessAnchorIds) {
46
51
  await processAnchorIds(settings, includeFiles);
47
52
  }
48
53
  // Localize static imports (import Snippet from /snippets/file.mdx -> import Snippet from /snippets/[locale]/file.mdx)
@@ -24,7 +24,11 @@ export type Options = {
24
24
  experimentalHideDefaultLocale?: boolean;
25
25
  experimentalFlattenJsonFiles?: boolean;
26
26
  experimentalLocalizeStaticImports?: boolean;
27
- experimentalAddHeaderAnchorIds?: 'mintlify';
27
+ experimentalAddHeaderAnchorIds?: 'mintlify' | 'default';
28
+ docsImportRewrites?: Array<{
29
+ match: string;
30
+ replace: string;
31
+ }>;
28
32
  };
29
33
  export type TranslateFlags = {
30
34
  config?: string;
@@ -49,9 +53,13 @@ export type TranslateFlags = {
49
53
  experimentalHideDefaultLocale?: boolean;
50
54
  experimentalFlattenJsonFiles?: boolean;
51
55
  experimentalLocalizeStaticImports?: boolean;
52
- experimentalAddHeaderAnchorIds?: 'mintlify';
56
+ experimentalAddHeaderAnchorIds?: 'mintlify' | 'default';
53
57
  excludeStaticUrls?: string[];
54
58
  excludeStaticImports?: string[];
59
+ docsImportRewrites?: Array<{
60
+ match: string;
61
+ replace: string;
62
+ }>;
55
63
  };
56
64
  export type WrapOptions = {
57
65
  src?: string[];
@@ -160,10 +168,14 @@ export type AdditionalOptions = {
160
168
  clearLocaleDirsExclude?: string[];
161
169
  experimentalLocalizeStaticImports?: boolean;
162
170
  experimentalLocalizeStaticUrls?: boolean;
163
- experimentalAddHeaderAnchorIds?: 'mintlify';
171
+ experimentalAddHeaderAnchorIds?: 'mintlify' | 'default';
164
172
  experimentalHideDefaultLocale?: boolean;
165
173
  experimentalFlattenJsonFiles?: boolean;
166
174
  baseDomain?: string;
175
+ docsImportRewrites?: Array<{
176
+ match: string;
177
+ replace: string;
178
+ }>;
167
179
  };
168
180
  export type SharedStaticAssetsConfig = {
169
181
  include: string | string[];
@@ -30,6 +30,16 @@ function extractHeadingText(heading) {
30
30
  });
31
31
  return text;
32
32
  }
33
+ function parseHeadingContent(text) {
34
+ // Support both {#id} and escaped \{#id\} forms
35
+ const anchorMatch = text.match(/(\\\{#([^}]+)\\\}|\{#([^}]+)\})\s*$/);
36
+ if (!anchorMatch) {
37
+ return { cleanedText: text };
38
+ }
39
+ const explicitId = anchorMatch[2] || anchorMatch[3];
40
+ const cleanedText = text.replace(anchorMatch[0], '').trimEnd();
41
+ return { cleanedText, explicitId };
42
+ }
33
43
  /**
34
44
  * Checks if a heading is already wrapped in a div with id
35
45
  */
@@ -57,15 +67,34 @@ export function extractHeadingInfo(mdxContent) {
57
67
  }
58
68
  catch (error) {
59
69
  console.warn(`Failed to parse MDX content: ${error instanceof Error ? error.message : String(error)}`);
60
- return [];
70
+ // Fallback: simple regex-based extraction to keep IDs usable
71
+ const fallbackHeadings = [];
72
+ const headingRegex = /^(#{1,6})\s+(.*)$/gm;
73
+ let position = 0;
74
+ let match;
75
+ while ((match = headingRegex.exec(mdxContent)) !== null) {
76
+ const hashes = match[1];
77
+ const rawText = match[2];
78
+ const { cleanedText, explicitId } = parseHeadingContent(rawText);
79
+ if (cleanedText || explicitId) {
80
+ fallbackHeadings.push({
81
+ text: cleanedText,
82
+ level: hashes.length,
83
+ slug: explicitId ?? generateSlug(cleanedText),
84
+ position: position++,
85
+ });
86
+ }
87
+ }
88
+ return fallbackHeadings;
61
89
  }
62
90
  let position = 0;
63
91
  visit(processedAst, 'heading', (heading) => {
64
92
  const headingText = extractHeadingText(heading);
65
- if (headingText) {
66
- const slug = generateSlug(headingText);
93
+ const { cleanedText, explicitId } = parseHeadingContent(headingText);
94
+ if (cleanedText || explicitId) {
95
+ const slug = explicitId ?? generateSlug(cleanedText);
67
96
  headings.push({
68
- text: headingText,
97
+ text: cleanedText,
69
98
  level: heading.depth,
70
99
  slug,
71
100
  position: position++,
@@ -114,13 +143,16 @@ export function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settin
114
143
  addedIds: [],
115
144
  };
116
145
  }
146
+ const translatedIsMdx = translatedPath
147
+ ? translatedPath.toLowerCase().endsWith('.mdx')
148
+ : true; // default to mdx-style escaping when unknown
117
149
  // Apply IDs to translated content
118
150
  let content;
119
151
  if (useDivWrapping) {
120
152
  content = applyDivWrappedIds(translatedContent, translatedHeadings, idMappings);
121
153
  }
122
154
  else {
123
- content = applyInlineIds(translatedContent, idMappings);
155
+ content = applyInlineIds(translatedContent, idMappings, translatedIsMdx);
124
156
  }
125
157
  return {
126
158
  content,
@@ -131,7 +163,17 @@ export function addExplicitAnchorIds(translatedContent, sourceHeadingMap, settin
131
163
  /**
132
164
  * Adds inline {#id} syntax to headings (standard markdown approach)
133
165
  */
134
- function applyInlineIds(translatedContent, idMappings) {
166
+ function applyInlineIds(translatedContent, idMappings, escapeAnchors) {
167
+ const escapeInlineAnchors = (content) => {
168
+ if (!escapeAnchors)
169
+ return content;
170
+ return content.replace(/\{#([A-Za-z0-9-_]+)\}/g, (match, id, offset, str) => {
171
+ if (offset > 0 && str[offset - 1] === '\\') {
172
+ return match;
173
+ }
174
+ return `\\{#${id}\\}`;
175
+ });
176
+ };
135
177
  // Parse the translated content
136
178
  let processedAst;
137
179
  try {
@@ -144,7 +186,7 @@ function applyInlineIds(translatedContent, idMappings) {
144
186
  }
145
187
  catch (error) {
146
188
  console.warn(`Failed to parse translated MDX content: ${error instanceof Error ? error.message : String(error)}`);
147
- return translatedContent;
189
+ return applyInlineIdsStringFallback(translatedContent, idMappings, escapeAnchors);
148
190
  }
149
191
  // Apply IDs to headings based on position
150
192
  let headingIndex = 0;
@@ -154,19 +196,33 @@ function applyInlineIds(translatedContent, idMappings) {
154
196
  if (id) {
155
197
  // Skip if heading already has explicit ID
156
198
  if (hasExplicitId(heading, processedAst)) {
199
+ if (escapeAnchors) {
200
+ // Normalize existing inline IDs to escaped form
201
+ const lastChild = heading.children[heading.children.length - 1];
202
+ if (lastChild?.type === 'text') {
203
+ const match = lastChild.value.match(/\{#([^}]+)\}\s*$/);
204
+ const alreadyEscaped = lastChild.value.match(/\\\{#[^}]+\\\}\s*$/);
205
+ if (match && !alreadyEscaped) {
206
+ const anchorId = match[1];
207
+ const base = lastChild.value.replace(/\s*\{#[^}]+\}\s*$/, '');
208
+ lastChild.value = `${base} \\{#${anchorId}\\}`;
209
+ actuallyModifiedContent = true;
210
+ }
211
+ }
212
+ }
157
213
  headingIndex++;
158
214
  return;
159
215
  }
160
216
  // Add the ID to the heading
161
217
  const lastChild = heading.children[heading.children.length - 1];
162
218
  if (lastChild?.type === 'text') {
163
- lastChild.value += ` \\{#${id}\\}`;
219
+ lastChild.value += escapeAnchors ? ` \\{#${id}\\}` : ` {#${id}}`;
164
220
  }
165
221
  else {
166
222
  // If last child is not text, add a new text node
167
223
  heading.children.push({
168
224
  type: 'text',
169
- value: ` \\{#${id}\\}`,
225
+ value: escapeAnchors ? ` \\{#${id}\\}` : ` {#${id}}`,
170
226
  });
171
227
  }
172
228
  actuallyModifiedContent = true;
@@ -175,7 +231,8 @@ function applyInlineIds(translatedContent, idMappings) {
175
231
  });
176
232
  // If we didn't modify any headings, return original content
177
233
  if (!actuallyModifiedContent) {
178
- return translatedContent;
234
+ const escaped = escapeInlineAnchors(translatedContent);
235
+ return escaped;
179
236
  }
180
237
  // Convert the modified AST back to MDX string
181
238
  try {
@@ -208,6 +265,31 @@ function applyInlineIds(translatedContent, idMappings) {
208
265
  return translatedContent;
209
266
  }
210
267
  }
268
+ /**
269
+ * Fallback string-based inline ID application when AST parsing fails
270
+ */
271
+ function applyInlineIdsStringFallback(translatedContent, idMappings, escapeAnchors) {
272
+ let headingIndex = 0;
273
+ return translatedContent.replace(/^(#{1,6}\s+)(.*)$/gm, (match, prefix, text) => {
274
+ const id = idMappings.get(headingIndex++);
275
+ if (!id) {
276
+ return match;
277
+ }
278
+ const hasEscaped = /\\\{#[^}]+\\\}\s*$/.test(text);
279
+ const hasUnescaped = /\{#[^}]+\}\s*$/.test(text);
280
+ if (hasEscaped) {
281
+ return match;
282
+ }
283
+ if (hasUnescaped) {
284
+ if (!escapeAnchors) {
285
+ return match;
286
+ }
287
+ return `${prefix}${text.replace(/\{#([^}]+)\}\s*$/, '\\\\{#$1\\\\}')}`;
288
+ }
289
+ const suffix = escapeAnchors ? ` \\{#${id}\\}` : ` {#${id}}`;
290
+ return `${prefix}${text}${suffix}`;
291
+ });
292
+ }
211
293
  /**
212
294
  * Wraps headings in divs with IDs (Mintlify approach)
213
295
  */
@@ -52,7 +52,7 @@ export default async function localizeStaticImports(settings, includeFiles) {
52
52
  const fileContent = await fs.promises.readFile(filePath, 'utf8');
53
53
  // Localize the file using default locale
54
54
  const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, settings.defaultLocale, // Process as default locale
55
- settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath);
55
+ settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath, settings.options);
56
56
  // Write the localized file back to the same path
57
57
  await fs.promises.writeFile(filePath, localizedFile);
58
58
  }));
@@ -73,7 +73,7 @@ export default async function localizeStaticImports(settings, includeFiles) {
73
73
  // Get file content
74
74
  const fileContent = await fs.promises.readFile(filePath, 'utf8');
75
75
  // Localize the file
76
- const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, locale, settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath);
76
+ const localizedFile = localizeStaticImportsForFile(fileContent, settings.defaultLocale, locale, settings.options?.docsHideDefaultLocaleImport || false, settings.options?.docsImportPattern, settings.options?.excludeStaticImports, filePath, settings.options);
77
77
  // Write the localized file to the target path
78
78
  await fs.promises.writeFile(filePath, localizedFile);
79
79
  }));
@@ -190,8 +190,33 @@ function transformNonDefaultLocaleImportPath(fullPath, patternHead, targetLocale
190
190
  /**
191
191
  * Main import path transformation function that delegates to specific scenarios
192
192
  */
193
- function transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale, currentFilePath, projectRoot = process.cwd() // fallback if not provided
194
- ) {
193
+ function transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale, currentFilePath, projectRoot = process.cwd(), // fallback if not provided
194
+ rewrites) {
195
+ // Apply explicit rewrites first (e.g., Docusaurus @site/docs -> @site/i18n/[locale]/...)
196
+ if (rewrites && rewrites.length > 0) {
197
+ const localeMap = {
198
+ '[locale]': targetLocale,
199
+ '[defaultLocale]': defaultLocale,
200
+ };
201
+ for (const { match, replace } of rewrites) {
202
+ const resolvedMatch = match.replace(/\[locale\]|\[defaultLocale\]/g, (token) => localeMap[token] || token);
203
+ if (fullPath.startsWith(resolvedMatch)) {
204
+ const remainder = fullPath.slice(resolvedMatch.length);
205
+ const resolvedReplace = replace.replace(/\[locale\]|\[defaultLocale\]/g, (token) => localeMap[token] || token);
206
+ let newPath;
207
+ if (resolvedReplace.endsWith('/')) {
208
+ newPath = `${resolvedReplace}${remainder.replace(/^\//, '')}`;
209
+ }
210
+ else if (remainder.startsWith('/')) {
211
+ newPath = `${resolvedReplace}${remainder}`;
212
+ }
213
+ else {
214
+ newPath = `${resolvedReplace}/${remainder}`;
215
+ }
216
+ return newPath;
217
+ }
218
+ }
219
+ }
195
220
  let newPath;
196
221
  if (targetLocale === defaultLocale) {
197
222
  newPath = transformDefaultLocaleImportPath(fullPath, patternHead, defaultLocale, hideDefaultLocale);
@@ -224,7 +249,7 @@ function transformImportPath(fullPath, patternHead, targetLocale, defaultLocale,
224
249
  /**
225
250
  * AST-based transformation for MDX files using remark-mdx
226
251
  */
227
- function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', exclude = [], currentFilePath) {
252
+ function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', exclude = [], currentFilePath, options) {
228
253
  const transformedImports = [];
229
254
  // Don't auto-prefix relative patterns that start with . or ..
230
255
  if (!pattern.startsWith('/') && !pattern.startsWith('.')) {
@@ -310,7 +335,7 @@ function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaul
310
335
  continue;
311
336
  const fullPath = line.slice(pathStart, pathEnd);
312
337
  // Transform the import path
313
- const newPath = transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale, currentFilePath);
338
+ const newPath = transformImportPath(fullPath, patternHead, targetLocale, defaultLocale, hideDefaultLocale, currentFilePath, process.cwd(), options?.docsImportRewrites);
314
339
  if (!newPath) {
315
340
  continue; // No transformation needed
316
341
  }
@@ -337,12 +362,8 @@ function transformMdxImports(mdxContent, defaultLocale, targetLocale, hideDefaul
337
362
  * AST-based transformation for MDX files only
338
363
  */
339
364
  function localizeStaticImportsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', // eg /docs/[locale] or /[locale]
340
- exclude = [], currentFilePath) {
341
- // Skip .md files entirely - they cannot have imports
342
- if (currentFilePath && currentFilePath.endsWith('.md')) {
343
- return file;
344
- }
365
+ exclude = [], currentFilePath, options) {
345
366
  // For MDX files, use AST-based transformation
346
- const result = transformMdxImports(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude, currentFilePath);
367
+ const result = transformMdxImports(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude, currentFilePath, options);
347
368
  return result.content;
348
369
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "2.5.13",
3
+ "version": "2.5.14",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [