gtx-cli 2.1.6 → 2.1.8-alpha.1

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,17 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.1.8
4
+
5
+ ### Patch Changes
6
+
7
+ - [#594](https://github.com/generaltranslation/gt/pull/594) [`3fa9c41`](https://github.com/generaltranslation/gt/commit/3fa9c41e2e37933b04e6c3d6c0f94271a07d0ff6) Thanks [@brian-lou](https://github.com/brian-lou)! - Fix <GTProvider> wizard scan behavior
8
+
9
+ ## 2.1.7
10
+
11
+ ### Patch Changes
12
+
13
+ - [#579](https://github.com/generaltranslation/gt/pull/579) [`a485533`](https://github.com/generaltranslation/gt/commit/a4855336dfe5242cfdb24fd2e981f86b0bffdf05) Thanks [@SamEggert](https://github.com/SamEggert)! - fix localize static urls, add baseDomain functionality
14
+
3
15
  ## 2.1.6
4
16
 
5
17
  ### Patch Changes
@@ -3,7 +3,6 @@ import { createFileMapping } from '../../formats/files/fileMapping.js';
3
3
  import { logError } from '../../console/logging.js';
4
4
  import { getStagedVersions } from '../../fs/config/updateVersions.js';
5
5
  import copyFile from '../../fs/copyFile.js';
6
- import localizeStaticImports from '../../utils/localizeStaticImports.js';
7
6
  import flattenJsonFiles from '../../utils/flattenJsonFiles.js';
8
7
  import localizeStaticUrls from '../../utils/localizeStaticUrls.js';
9
8
  import { noFilesError, noVersionIdError } from '../../console/index.js';
@@ -35,13 +34,13 @@ export async function handleDownload(options, settings) {
35
34
  await checkFileTranslations(stagedVersionData, settings.locales, options.timeout, (sourcePath, locale) => fileMapping[locale][sourcePath] ?? null, settings);
36
35
  }
37
36
  export async function postProcessTranslations(settings) {
38
- // Localize static urls (/docs -> /[locale]/docs)
37
+ // Localize static urls (/docs -> /[locale]/docs) for non-default locales only
38
+ // Default locale is processed earlier in the flow in base.ts
39
39
  if (settings.options?.experimentalLocalizeStaticUrls) {
40
- await localizeStaticUrls(settings);
41
- }
42
- // Localize static imports (/docs -> /[locale]/docs)
43
- if (settings.options?.experimentalLocalizeStaticImports) {
44
- await localizeStaticImports(settings);
40
+ const nonDefaultLocales = settings.locales.filter((locale) => locale !== settings.defaultLocale);
41
+ if (nonDefaultLocales.length > 0) {
42
+ await localizeStaticUrls(settings, nonDefaultLocales);
43
+ }
45
44
  }
46
45
  // Flatten json files into a single file
47
46
  if (settings.options?.experimentalFlattenJsonFiles) {
@@ -104,7 +104,7 @@ export async function generateSettings(options, cwd = process.cwd()) {
104
104
  // For human review, always stage the project
105
105
  mergedOptions.stageTranslations = mergedOptions.stageTranslations ?? false;
106
106
  // Add publish if not provided
107
- mergedOptions.publish = mergedOptions.publish ?? false;
107
+ mergedOptions.publish = (gtConfig.publish || options.publish) ?? false;
108
108
  // Populate src if not provided
109
109
  mergedOptions.src = mergedOptions.src || DEFAULT_SRC_PATTERNS;
110
110
  // Resolve all glob patterns in the files object
@@ -61,8 +61,22 @@ export async function wrapContentNext(options, pkg, errors, warnings) {
61
61
  if (pkg === 'gt-next' &&
62
62
  options.addGTProvider &&
63
63
  isHtmlElement(path.node.openingElement)) {
64
- // Find the body element in the HTML children
65
- const bodyElement = path.node.children.find((child) => t.isJSXElement(child) && isBodyElement(child.openingElement));
64
+ // Find the body element recursively in the HTML tree
65
+ const findBodyElement = (children) => {
66
+ for (const child of children) {
67
+ if (t.isJSXElement(child) &&
68
+ isBodyElement(child.openingElement)) {
69
+ return child;
70
+ }
71
+ if (t.isJSXElement(child)) {
72
+ const bodyInChild = findBodyElement(child.children);
73
+ if (bodyInChild)
74
+ return bodyInChild;
75
+ }
76
+ }
77
+ return null;
78
+ };
79
+ const bodyElement = findBodyElement(path.node.children);
66
80
  if (!bodyElement) {
67
81
  warnings.push(`File ${file} has a <html> tag without a <body> tag. Skipping GTProvider insertion.`);
68
82
  return;
@@ -1,3 +1,6 @@
1
+ export declare const MSG_TRANSLATION_HOOK = "msg";
2
+ export declare const INLINE_TRANSLATION_HOOK = "useGT";
3
+ export declare const INLINE_TRANSLATION_HOOK_ASYNC = "getGT";
1
4
  export declare const GT_TRANSLATION_FUNCS: string[];
2
5
  export declare const VARIABLE_COMPONENTS: string[];
3
6
  export declare const GT_ATTRIBUTES_WITH_SUGAR: string[];
@@ -1,7 +1,11 @@
1
+ export const MSG_TRANSLATION_HOOK = 'msg';
2
+ export const INLINE_TRANSLATION_HOOK = 'useGT';
3
+ export const INLINE_TRANSLATION_HOOK_ASYNC = 'getGT';
1
4
  // GT translation functions
2
5
  export const GT_TRANSLATION_FUNCS = [
3
- 'useGT',
4
- 'getGT',
6
+ INLINE_TRANSLATION_HOOK,
7
+ INLINE_TRANSLATION_HOOK_ASYNC,
8
+ MSG_TRANSLATION_HOOK,
5
9
  'T',
6
10
  'Var',
7
11
  'DateTime',
@@ -1,6 +1,6 @@
1
1
  import * as t from '@babel/types';
2
2
  import { isStaticExpression } from '../evaluateJsx.js';
3
- import { GT_ATTRIBUTES_WITH_SUGAR, mapAttributeName } from './constants.js';
3
+ import { GT_ATTRIBUTES_WITH_SUGAR, MSG_TRANSLATION_HOOK, INLINE_TRANSLATION_HOOK, INLINE_TRANSLATION_HOOK_ASYNC, mapAttributeName, } from './constants.js';
4
4
  import { warnNonStaticExpressionSync, warnNonStringSync, warnTemplateLiteralSync, warnAsyncUseGT, warnSyncGetGT, } from '../../../console/index.js';
5
5
  import generateModule from '@babel/generator';
6
6
  import traverseModule from '@babel/traverse';
@@ -22,7 +22,7 @@ import resolve from 'resolve';
22
22
  * - Metadata extraction from options object
23
23
  * - Error reporting for non-static expressions and template literals with expressions
24
24
  */
25
- function processTranslationCall(tPath, updates, errors, file) {
25
+ function processTranslationCall(tPath, updates, errors, file, ignoreAdditionalData) {
26
26
  if (tPath.parent.type === 'CallExpression' &&
27
27
  tPath.parent.arguments.length > 0) {
28
28
  const arg = tPath.parent.arguments[0];
@@ -43,7 +43,7 @@ function processTranslationCall(tPath, updates, errors, file) {
43
43
  if (!result.isStatic) {
44
44
  errors.push(warnNonStaticExpressionSync(file, attribute, generate(prop.value).code, `${prop.loc?.start?.line}:${prop.loc?.start?.column}`));
45
45
  }
46
- if (result.isStatic && result.value) {
46
+ if (result.isStatic && result.value && !ignoreAdditionalData) {
47
47
  // Map $id and $context to id and context
48
48
  metadata[mapAttributeName(attribute)] = result.value;
49
49
  }
@@ -145,11 +145,11 @@ function resolveVariableAliases(scope, variableName, visited = new Set()) {
145
145
  * This covers both direct translation calls (t('hello')) and prop drilling
146
146
  * where the translation callback is passed to other functions (getData(t)).
147
147
  */
148
- function handleFunctionCall(tPath, updates, errors, file, importMap) {
148
+ function handleFunctionCall(tPath, updates, errors, file, importMap, ignoreAdditionalData) {
149
149
  if (tPath.parent.type === 'CallExpression' &&
150
150
  tPath.parent.callee === tPath.node) {
151
151
  // Direct translation call: t('hello')
152
- processTranslationCall(tPath, updates, errors, file);
152
+ processTranslationCall(tPath, updates, errors, file, ignoreAdditionalData);
153
153
  }
154
154
  else if (tPath.parent.type === 'CallExpression' &&
155
155
  t.isExpression(tPath.node) &&
@@ -161,7 +161,7 @@ function handleFunctionCall(tPath, updates, errors, file, importMap) {
161
161
  const calleeBinding = tPath.scope.getBinding(callee.name);
162
162
  if (calleeBinding && calleeBinding.path.isFunction()) {
163
163
  const functionPath = calleeBinding.path;
164
- processFunctionIfMatches(callee.name, argIndex, functionPath.node, functionPath, updates, errors, file);
164
+ processFunctionIfMatches(callee.name, argIndex, functionPath.node, functionPath, updates, errors, file, ignoreAdditionalData);
165
165
  }
166
166
  // Handle arrow functions assigned to variables: const getData = (t) => {...}
167
167
  else if (calleeBinding &&
@@ -170,14 +170,14 @@ function handleFunctionCall(tPath, updates, errors, file, importMap) {
170
170
  (t.isArrowFunctionExpression(calleeBinding.path.node.init) ||
171
171
  t.isFunctionExpression(calleeBinding.path.node.init))) {
172
172
  const initPath = calleeBinding.path.get('init');
173
- processFunctionIfMatches(callee.name, argIndex, calleeBinding.path.node.init, initPath, updates, errors, file);
173
+ processFunctionIfMatches(callee.name, argIndex, calleeBinding.path.node.init, initPath, updates, errors, file, ignoreAdditionalData);
174
174
  }
175
175
  // If not found locally, check if it's an imported function
176
176
  else if (importMap.has(callee.name)) {
177
177
  const importPath = importMap.get(callee.name);
178
178
  const resolvedPath = resolveImportPath(file, importPath);
179
179
  if (resolvedPath) {
180
- findFunctionInFile(resolvedPath, callee.name, argIndex, updates, errors);
180
+ findFunctionInFile(resolvedPath, callee.name, argIndex, updates, errors, ignoreAdditionalData);
181
181
  }
182
182
  }
183
183
  }
@@ -188,12 +188,12 @@ function handleFunctionCall(tPath, updates, errors, file, importMap) {
188
188
  * Validates the function has enough parameters and traces how the translation callback
189
189
  * is used within that function's body.
190
190
  */
191
- function processFunctionIfMatches(_functionName, argIndex, functionNode, functionPath, updates, errors, filePath) {
191
+ function processFunctionIfMatches(_functionName, argIndex, functionNode, functionPath, updates, errors, filePath, ignoreAdditionalData) {
192
192
  if (functionNode.params.length > argIndex) {
193
193
  const param = functionNode.params[argIndex];
194
194
  const paramName = extractParameterName(param);
195
195
  if (paramName) {
196
- findFunctionParameterUsage(functionPath, paramName, updates, errors, filePath);
196
+ findFunctionParameterUsage(functionPath, paramName, updates, errors, filePath, ignoreAdditionalData);
197
197
  }
198
198
  }
199
199
  }
@@ -205,7 +205,7 @@ function processFunctionIfMatches(_functionName, argIndex, functionNode, functio
205
205
  * Example: In function getInfo(t) { return t('hello'); }, this finds the t('hello') call.
206
206
  * Example: In function getData(t) { return getFooter(t); }, this finds and traces into getFooter.
207
207
  */
208
- function findFunctionParameterUsage(functionPath, parameterName, updates, errors, file) {
208
+ function findFunctionParameterUsage(functionPath, parameterName, updates, errors, file, ignoreAdditionalData) {
209
209
  // Look for the function body and find all usages of the parameter
210
210
  if (functionPath.isFunction()) {
211
211
  const functionScope = functionPath.scope;
@@ -220,7 +220,7 @@ function findFunctionParameterUsage(functionPath, parameterName, updates, errors
220
220
  const binding = functionScope.bindings[name];
221
221
  if (binding) {
222
222
  binding.referencePaths.forEach((refPath) => {
223
- handleFunctionCall(refPath, updates, errors, file, importMap);
223
+ handleFunctionCall(refPath, updates, errors, file, importMap, ignoreAdditionalData);
224
224
  });
225
225
  }
226
226
  });
@@ -299,7 +299,7 @@ function resolveImportPath(currentFile, importPath) {
299
299
  * - export function getInfo(t) { ... }
300
300
  * - const getInfo = (t) => { ... }
301
301
  */
302
- function findFunctionInFile(filePath, functionName, argIndex, updates, errors) {
302
+ function findFunctionInFile(filePath, functionName, argIndex, updates, errors, ignoreAdditionalData) {
303
303
  try {
304
304
  const code = fs.readFileSync(filePath, 'utf8');
305
305
  const ast = parse(code, {
@@ -310,7 +310,7 @@ function findFunctionInFile(filePath, functionName, argIndex, updates, errors) {
310
310
  // Handle function declarations: function getInfo(t) { ... }
311
311
  FunctionDeclaration(path) {
312
312
  if (path.node.id?.name === functionName) {
313
- processFunctionIfMatches(functionName, argIndex, path.node, path, updates, errors, filePath);
313
+ processFunctionIfMatches(functionName, argIndex, path.node, path, updates, errors, filePath, ignoreAdditionalData);
314
314
  }
315
315
  },
316
316
  // Handle variable declarations: const getInfo = (t) => { ... }
@@ -321,7 +321,7 @@ function findFunctionInFile(filePath, functionName, argIndex, updates, errors) {
321
321
  (t.isArrowFunctionExpression(path.node.init) ||
322
322
  t.isFunctionExpression(path.node.init))) {
323
323
  const initPath = path.get('init');
324
- processFunctionIfMatches(functionName, argIndex, path.node.init, initPath, updates, errors, filePath);
324
+ processFunctionIfMatches(functionName, argIndex, path.node.init, initPath, updates, errors, filePath, ignoreAdditionalData);
325
325
  }
326
326
  },
327
327
  });
@@ -348,21 +348,33 @@ export function parseStrings(importName, originalName, path, updates, errors, fi
348
348
  const importMap = buildImportMap(path.scope.getProgramParent().path);
349
349
  const referencePaths = path.scope.bindings[importName]?.referencePaths || [];
350
350
  for (const refPath of referencePaths) {
351
- // Find call expressions of useGT() / await getGT()
351
+ // Handle msg() calls directly without variable assignment
352
+ if (originalName === MSG_TRANSLATION_HOOK) {
353
+ const ignoreAdditionalData = true;
354
+ // Check if this is a direct call to msg('string')
355
+ if (refPath.parent.type === 'CallExpression' &&
356
+ refPath.parent.callee === refPath.node) {
357
+ processTranslationCall(refPath, updates, errors, file, ignoreAdditionalData);
358
+ }
359
+ continue;
360
+ }
361
+ // Handle useGT() and getGT() calls that need variable assignment
352
362
  const callExpr = refPath.findParent((p) => p.isCallExpression());
353
363
  if (callExpr) {
354
364
  // Get the parent, handling both await and non-await cases
355
365
  const parentPath = callExpr.parentPath;
356
366
  const parentFunction = refPath.getFunctionParent();
357
367
  const asyncScope = parentFunction?.node.async;
358
- if (asyncScope && originalName === 'useGT') {
368
+ if (asyncScope && originalName === INLINE_TRANSLATION_HOOK) {
359
369
  errors.push(warnAsyncUseGT(file, `${refPath.node.loc?.start?.line}:${refPath.node.loc?.start?.column}`));
360
370
  return;
361
371
  }
362
- else if (!asyncScope && originalName === 'getGT') {
372
+ else if (!asyncScope &&
373
+ originalName === INLINE_TRANSLATION_HOOK_ASYNC) {
363
374
  errors.push(warnSyncGetGT(file, `${refPath.node.loc?.start?.line}:${refPath.node.loc?.start?.column}`));
364
375
  return;
365
376
  }
377
+ const ignoreAdditionalData = false;
366
378
  const effectiveParent = parentPath?.node.type === 'AwaitExpression'
367
379
  ? parentPath.parentPath
368
380
  : parentPath;
@@ -372,10 +384,16 @@ export function parseStrings(importName, originalName, path, updates, errors, fi
372
384
  const tFuncName = effectiveParent.node.id.name;
373
385
  // Get the scope from the variable declaration
374
386
  const variableScope = effectiveParent.scope;
375
- const tReferencePaths = variableScope.bindings[tFuncName]?.referencePaths || [];
376
- for (const tPath of tReferencePaths) {
377
- handleFunctionCall(tPath, updates, errors, file, importMap);
378
- }
387
+ // Resolve all aliases of the translation function
388
+ // Example: translate -> [translate, t, a, b] for const t = translate; const a = t; const b = a;
389
+ const allTranslationNames = resolveVariableAliases(variableScope, tFuncName);
390
+ // Process references for all translation function names and their aliases
391
+ allTranslationNames.forEach((name) => {
392
+ const tReferencePaths = variableScope.bindings[name]?.referencePaths || [];
393
+ for (const tPath of tReferencePaths) {
394
+ handleFunctionCall(tPath, updates, errors, file, importMap, ignoreAdditionalData);
395
+ }
396
+ });
379
397
  }
380
398
  }
381
399
  }
@@ -1,4 +1,5 @@
1
1
  import { warnAsyncUseGT, warnSyncGetGT } from '../../../console/index.js';
2
+ import { INLINE_TRANSLATION_HOOK, INLINE_TRANSLATION_HOOK_ASYNC, } from './constants.js';
2
3
  /**
3
4
  * Validate useGT() / await getGT() calls
4
5
  * 1. Validates that the call does not violate the rules of React (no hooks in async functions)
@@ -12,13 +13,13 @@ export function validateStringFunction(localImportName, path, updates, errors, f
12
13
  callPath.node.callee.name === localImportName) {
13
14
  // Check the function scope
14
15
  const functionScope = callPath.getFunctionParent();
15
- if (originalImportName === 'useGT') {
16
+ if (originalImportName === INLINE_TRANSLATION_HOOK) {
16
17
  // useGT should NOT be in an async function
17
18
  if (functionScope && functionScope.node.async) {
18
19
  errors.push(warnAsyncUseGT(file, `${callPath.node.loc?.start?.line}:${callPath.node.loc?.start?.column}`));
19
20
  }
20
21
  }
21
- else if (originalImportName === 'getGT') {
22
+ else if (originalImportName === INLINE_TRANSLATION_HOOK_ASYNC) {
22
23
  // getGT should be in an async function
23
24
  if (!functionScope || !functionScope.node.async) {
24
25
  errors.push(warnSyncGetGT(file, `${callPath.node.loc?.start?.line}:${callPath.node.loc?.start?.column}`));
@@ -8,7 +8,7 @@ import { parseJSXElement } from '../jsx/utils/parseJsx.js';
8
8
  import { parseStrings } from '../jsx/utils/parseStringFunction.js';
9
9
  import { extractImportName } from '../jsx/utils/parseAst.js';
10
10
  import { logError } from '../../console/logging.js';
11
- import { GT_TRANSLATION_FUNCS } from '../jsx/utils/constants.js';
11
+ import { GT_TRANSLATION_FUNCS, INLINE_TRANSLATION_HOOK, INLINE_TRANSLATION_HOOK_ASYNC, MSG_TRANSLATION_HOOK, } from '../jsx/utils/constants.js';
12
12
  import { matchFiles } from '../../fs/matchFiles.js';
13
13
  import { DEFAULT_SRC_PATTERNS } from '../../config/generateSettings.js';
14
14
  export async function createInlineUpdates(pkg, validate, filePatterns) {
@@ -38,7 +38,9 @@ export async function createInlineUpdates(pkg, validate, filePatterns) {
38
38
  if (path.node.source.value.startsWith(pkg)) {
39
39
  const importName = extractImportName(path.node, pkg, GT_TRANSLATION_FUNCS);
40
40
  for (const name of importName) {
41
- if (name.original === 'useGT' || name.original === 'getGT') {
41
+ if (name.original === INLINE_TRANSLATION_HOOK ||
42
+ name.original === INLINE_TRANSLATION_HOOK_ASYNC ||
43
+ name.original === MSG_TRANSLATION_HOOK) {
42
44
  translationPaths.push({
43
45
  localName: name.local,
44
46
  path,
@@ -65,7 +67,9 @@ export async function createInlineUpdates(pkg, validate, filePatterns) {
65
67
  if (parentPath.isVariableDeclaration()) {
66
68
  const importName = extractImportName(parentPath.node, pkg, GT_TRANSLATION_FUNCS);
67
69
  for (const name of importName) {
68
- if (name.original === 'useGT' || name.original === 'getGT') {
70
+ if (name.original === INLINE_TRANSLATION_HOOK ||
71
+ name.original === INLINE_TRANSLATION_HOOK_ASYNC ||
72
+ name.original === MSG_TRANSLATION_HOOK) {
69
73
  translationPaths.push({
70
74
  localName: name.local,
71
75
  path: parentPath,
@@ -141,6 +141,7 @@ export type AdditionalOptions = {
141
141
  experimentalLocalizeStaticUrls?: boolean;
142
142
  experimentalHideDefaultLocale?: boolean;
143
143
  experimentalFlattenJsonFiles?: boolean;
144
+ baseDomain?: string;
144
145
  };
145
146
  export type JsonSchema = {
146
147
  preset?: 'mintlify';
@@ -12,4 +12,8 @@ import { Settings } from '../types/index.js';
12
12
  * - Support more file types
13
13
  * - Support more complex paths
14
14
  */
15
- export default function localizeStaticUrls(settings: Settings): Promise<void>;
15
+ export default function localizeStaticUrls(settings: Settings, targetLocales?: string[]): Promise<void>;
16
+ /**
17
+ * Main URL transformation function that delegates to specific scenarios
18
+ */
19
+ export declare function transformUrlPath(originalUrl: string, patternHead: string, targetLocale: string, defaultLocale: string, hideDefaultLocale: boolean): string | null;
@@ -21,19 +21,24 @@ const { isMatch } = micromatch;
21
21
  * - Support more file types
22
22
  * - Support more complex paths
23
23
  */
24
- export default async function localizeStaticUrls(settings) {
24
+ export default async function localizeStaticUrls(settings, targetLocales) {
25
25
  if (!settings.files ||
26
26
  (Object.keys(settings.files.placeholderPaths).length === 1 &&
27
27
  settings.files.placeholderPaths.gt)) {
28
28
  return;
29
29
  }
30
30
  const { resolvedPaths: sourceFiles } = settings.files;
31
- const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, settings.defaultLocale);
31
+ // Use filtered locales if provided, otherwise use all locales
32
+ const locales = targetLocales || settings.locales;
33
+ const fileMapping = createFileMapping(sourceFiles, settings.files.placeholderPaths, settings.files.transformPaths, settings.locales, // Always use all locales for mapping, filter later
34
+ settings.defaultLocale);
32
35
  // Process all file types at once with a single call
33
36
  const processPromises = [];
34
37
  // First, process default locale files (from source files)
35
38
  // This is needed because they might not be in the fileMapping if they're not being translated
36
- if (!fileMapping[settings.defaultLocale]) {
39
+ // Only process default locale if it's in the target locales filter
40
+ if (!fileMapping[settings.defaultLocale] &&
41
+ locales.includes(settings.defaultLocale)) {
37
42
  const defaultLocaleFiles = [];
38
43
  // Collect all .md and .mdx files from sourceFiles
39
44
  if (sourceFiles.md) {
@@ -47,16 +52,20 @@ export default async function localizeStaticUrls(settings) {
47
52
  // Get file content
48
53
  const fileContent = await fs.promises.readFile(filePath, 'utf8');
49
54
  // Localize the file using default locale
50
- const localizedFile = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, settings.defaultLocale, // Process as default locale
51
- settings.options?.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);
55
+ const result = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, settings.defaultLocale, // Process as default locale
56
+ settings.options?.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern, settings.options?.excludeStaticUrls, settings.options?.baseDomain);
57
+ // Only write the file if there were changes
58
+ if (result.hasChanges) {
59
+ await fs.promises.writeFile(filePath, result.content);
60
+ }
54
61
  }));
55
62
  processPromises.push(defaultPromise);
56
63
  }
57
64
  }
58
65
  // Then process all other locales from fileMapping
59
- const mappingPromises = Object.entries(fileMapping).map(async ([locale, filesMap]) => {
66
+ const mappingPromises = Object.entries(fileMapping)
67
+ .filter(([locale, filesMap]) => locales.includes(locale)) // Filter by target locales
68
+ .map(async ([locale, filesMap]) => {
60
69
  // Get all files that are md or mdx
61
70
  const targetFiles = Object.values(filesMap).filter((path) => path.endsWith('.md') || path.endsWith('.mdx'));
62
71
  // Replace the placeholder path with the target path
@@ -64,9 +73,11 @@ export default async function localizeStaticUrls(settings) {
64
73
  // Get file content
65
74
  const fileContent = await fs.promises.readFile(filePath, 'utf8');
66
75
  // Localize the file (handles both URLs and hrefs in single AST pass)
67
- const localizedFile = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, locale, settings.options?.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern, settings.options?.excludeStaticUrls);
68
- // Write the localized file to the target path
69
- await fs.promises.writeFile(filePath, localizedFile);
76
+ const result = localizeStaticUrlsForFile(fileContent, settings.defaultLocale, locale, settings.options?.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern, settings.options?.excludeStaticUrls, settings.options?.baseDomain);
77
+ // Only write the file if there were changes
78
+ if (result.hasChanges) {
79
+ await fs.promises.writeFile(filePath, result.content);
80
+ }
70
81
  }));
71
82
  });
72
83
  processPromises.push(...mappingPromises);
@@ -75,21 +86,28 @@ export default async function localizeStaticUrls(settings) {
75
86
  /**
76
87
  * Determines if a URL should be processed based on pattern matching
77
88
  */
78
- function shouldProcessUrl(originalUrl, patternHead, targetLocale, defaultLocale) {
79
- // Skip absolute URLs (http://, https://, //, etc.)
80
- if (originalUrl.includes(':')) {
81
- return false;
82
- }
89
+ function shouldProcessUrl(originalUrl, patternHead, targetLocale, defaultLocale, baseDomain) {
83
90
  const patternWithoutSlash = patternHead.replace(/\/$/, '');
91
+ // Handle absolute URLs with baseDomain
92
+ let urlToCheck = originalUrl;
93
+ if (baseDomain && originalUrl.startsWith(baseDomain)) {
94
+ urlToCheck = originalUrl.substring(baseDomain.length);
95
+ }
84
96
  if (targetLocale === defaultLocale) {
85
97
  // For default locale processing, check if URL contains the pattern
86
- return originalUrl.includes(patternWithoutSlash);
98
+ return urlToCheck.includes(patternWithoutSlash);
87
99
  }
88
100
  else {
89
101
  // For non-default locales, check if URL starts with pattern
90
- return originalUrl.startsWith(patternWithoutSlash);
102
+ return urlToCheck.startsWith(patternWithoutSlash);
91
103
  }
92
104
  }
105
+ /**
106
+ * Determines if a URL should be processed based on the base domain
107
+ */
108
+ function shouldProcessAbsoluteUrl(originalUrl, baseDomain) {
109
+ return originalUrl.startsWith(baseDomain);
110
+ }
93
111
  /**
94
112
  * Checks if a URL should be excluded based on exclusion patterns
95
113
  */
@@ -98,102 +116,79 @@ function isUrlExcluded(originalUrl, exclude, defaultLocale) {
98
116
  return excludePatterns.some((pattern) => isMatch(originalUrl, pattern));
99
117
  }
100
118
  /**
101
- * Transforms URL for default locale processing
102
- */
103
- function transformDefaultLocaleUrl(originalUrl, patternHead, defaultLocale, hideDefaultLocale) {
104
- if (hideDefaultLocale) {
105
- // Remove locale from URLs that have it: '/docs/en/file' -> '/docs/file'
106
- if (originalUrl.includes(`/${defaultLocale}/`)) {
107
- return originalUrl.replace(`/${defaultLocale}/`, '/');
108
- }
109
- else if (originalUrl.endsWith(`/${defaultLocale}`)) {
110
- return originalUrl.replace(`/${defaultLocale}`, '');
111
- }
112
- return null; // URL doesn't have default locale
113
- }
114
- else {
115
- // Add locale to URLs that don't have it: '/docs/file' -> '/docs/en/file'
116
- if (originalUrl.includes(`/${defaultLocale}/`) ||
117
- originalUrl.endsWith(`/${defaultLocale}`)) {
118
- return null; // Already has default locale
119
- }
120
- if (originalUrl.startsWith(patternHead)) {
121
- const pathAfterHead = originalUrl.slice(patternHead.length);
122
- if (pathAfterHead) {
123
- return `${patternHead}${defaultLocale}/${pathAfterHead}`;
124
- }
125
- else {
126
- return `${patternHead.replace(/\/$/, '')}/${defaultLocale}`;
127
- }
128
- }
129
- return null; // URL doesn't match pattern
130
- }
131
- }
132
- /**
133
- * Transforms URL for non-default locale processing with hideDefaultLocale=true
119
+ * Main URL transformation function that delegates to specific scenarios
134
120
  */
135
- function transformNonDefaultLocaleUrlWithHidden(originalUrl, patternHead, targetLocale, defaultLocale) {
136
- // Check if already localized
137
- if (originalUrl.startsWith(`${patternHead}${targetLocale}/`) ||
138
- originalUrl === `${patternHead}${targetLocale}`) {
121
+ export function transformUrlPath(originalUrl, patternHead, targetLocale, defaultLocale, hideDefaultLocale) {
122
+ const originalPathArray = originalUrl
123
+ .split('/')
124
+ .filter((path) => path !== '');
125
+ const patternHeadArray = patternHead.split('/').filter((path) => path !== '');
126
+ // check if the pattern head matches the original path
127
+ if (!checkIfPathMatchesPattern(originalPathArray, patternHeadArray)) {
139
128
  return null;
140
129
  }
141
- // Replace default locale with target locale
142
- const expectedPathWithDefaultLocale = `${patternHead}${defaultLocale}`;
143
- if (originalUrl.startsWith(`${expectedPathWithDefaultLocale}/`) ||
144
- originalUrl === expectedPathWithDefaultLocale) {
145
- return originalUrl.replace(`${patternHead}${defaultLocale}`, `${patternHead}${targetLocale}`);
146
- }
147
- // Handle exact pattern match
148
- if (originalUrl === patternHead.replace(/\/$/, '')) {
149
- return `${patternHead.replace(/\/$/, '')}/${targetLocale}`;
130
+ if (patternHeadArray.length > originalPathArray.length) {
131
+ return null; // Pattern is longer than the URL path
150
132
  }
151
- // Add target locale to URL without any locale
152
- const pathAfterHead = originalUrl.slice(originalUrl.startsWith(patternHead) ? patternHead.length : 0);
153
- return pathAfterHead
154
- ? `${patternHead}${targetLocale}/${pathAfterHead}`
155
- : `${patternHead}${targetLocale}`;
156
- }
157
- /**
158
- * Transforms URL for non-default locale processing with hideDefaultLocale=false
159
- */
160
- function transformNonDefaultLocaleUrl(originalUrl, patternHead, targetLocale, defaultLocale) {
161
- const expectedPathWithLocale = `${patternHead}${defaultLocale}`;
162
- if (originalUrl.startsWith(`${expectedPathWithLocale}/`) ||
163
- originalUrl === expectedPathWithLocale) {
164
- // Replace existing default locale with target locale
165
- return originalUrl.replace(`${patternHead}${defaultLocale}`, `${patternHead}${targetLocale}`);
166
- }
167
- else if (originalUrl.startsWith(patternHead)) {
168
- // Add target locale to URL that doesn't have any locale
169
- const pathAfterHead = originalUrl.slice(patternHead.length);
170
- if (pathAfterHead) {
171
- return `${patternHead}${targetLocale}/${pathAfterHead}`;
133
+ let result = null;
134
+ if (targetLocale === defaultLocale) {
135
+ if (hideDefaultLocale) {
136
+ // check if default locale is already present
137
+ if (originalPathArray?.[patternHeadArray.length] !== defaultLocale) {
138
+ return null;
139
+ }
140
+ // remove default locale
141
+ const newPathArray = [
142
+ ...originalPathArray.slice(0, patternHeadArray.length),
143
+ ...originalPathArray.slice(patternHeadArray.length + 1),
144
+ ];
145
+ result = newPathArray.join('/');
172
146
  }
173
147
  else {
174
- return `${patternHead.replace(/\/$/, '')}/${targetLocale}`;
148
+ // check if default locale is already present
149
+ if (originalPathArray?.[patternHeadArray.length] === defaultLocale) {
150
+ return null;
151
+ }
152
+ // insert default locale
153
+ const newPathArray = [
154
+ ...originalPathArray.slice(0, patternHeadArray.length),
155
+ defaultLocale,
156
+ ...originalPathArray.slice(patternHeadArray.length),
157
+ ];
158
+ result = newPathArray.join('/');
175
159
  }
176
160
  }
177
- return null; // URL doesn't match pattern
178
- }
179
- /**
180
- * Main URL transformation function that delegates to specific scenarios
181
- */
182
- function transformUrlPath(originalUrl, patternHead, targetLocale, defaultLocale, hideDefaultLocale) {
183
- if (targetLocale === defaultLocale) {
184
- return transformDefaultLocaleUrl(originalUrl, patternHead, defaultLocale, hideDefaultLocale);
185
- }
186
161
  else if (hideDefaultLocale) {
187
- return transformNonDefaultLocaleUrlWithHidden(originalUrl, patternHead, targetLocale, defaultLocale);
162
+ const newPathArray = [
163
+ ...originalPathArray.slice(0, patternHeadArray.length),
164
+ targetLocale,
165
+ ...originalPathArray.slice(patternHeadArray.length),
166
+ ];
167
+ result = newPathArray.join('/');
188
168
  }
189
169
  else {
190
- return transformNonDefaultLocaleUrl(originalUrl, patternHead, targetLocale, defaultLocale);
170
+ // check default locale
171
+ if (originalPathArray?.[patternHeadArray.length] !== defaultLocale) {
172
+ return null;
173
+ }
174
+ // replace default locale with target locale
175
+ const newPathArray = [...originalPathArray];
176
+ newPathArray[patternHeadArray.length] = targetLocale;
177
+ result = newPathArray.join('/');
191
178
  }
179
+ // check for leading and trailing slashes
180
+ if (originalUrl.startsWith('/')) {
181
+ result = '/' + result;
182
+ }
183
+ if (originalUrl.endsWith('/')) {
184
+ result = result + '/';
185
+ }
186
+ return result;
192
187
  }
193
188
  /**
194
189
  * AST-based transformation for MDX files using remark-mdx
195
190
  */
196
- function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', exclude = []) {
191
+ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', exclude = [], baseDomain) {
197
192
  const transformedUrls = [];
198
193
  if (!pattern.startsWith('/')) {
199
194
  pattern = '/' + pattern;
@@ -240,13 +235,32 @@ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLo
240
235
  return {
241
236
  content: mdxContent,
242
237
  hasChanges: false,
243
- transformedUrls: [],
238
+ transformedUrls,
244
239
  };
245
240
  }
246
241
  // Helper function to transform URL based on pattern
247
242
  const transformUrl = (originalUrl, linkType) => {
248
243
  // Check if URL should be processed
249
- if (!shouldProcessUrl(originalUrl, patternHead, targetLocale, defaultLocale)) {
244
+ if (!shouldProcessUrl(originalUrl, patternHead, targetLocale, defaultLocale, baseDomain)) {
245
+ return null;
246
+ }
247
+ // Skip absolute URLs (http://, https://, //, etc.)
248
+ if (baseDomain && shouldProcessAbsoluteUrl(originalUrl, baseDomain)) {
249
+ // Get everything after the base domain
250
+ const afterDomain = originalUrl.substring(baseDomain.length);
251
+ const transformedPath = transformUrlPath(afterDomain, patternHead, targetLocale, defaultLocale, hideDefaultLocale);
252
+ if (!transformedPath) {
253
+ return null;
254
+ }
255
+ transformedUrls.push({
256
+ originalPath: originalUrl,
257
+ newPath: transformedPath,
258
+ type: linkType,
259
+ });
260
+ return transformedPath ? baseDomain + transformedPath : null;
261
+ }
262
+ // Exclude colon-prefixed URLs (http://, https://, //, etc.)
263
+ if (originalUrl.split('?')[0].includes(':')) {
250
264
  return null;
251
265
  }
252
266
  // Transform the URL based on locale and configuration
@@ -381,8 +395,23 @@ function transformMdxUrls(mdxContent, defaultLocale, targetLocale, hideDefaultLo
381
395
  }
382
396
  // AST-based transformation for MDX files using remark
383
397
  function localizeStaticUrlsForFile(file, defaultLocale, targetLocale, hideDefaultLocale, pattern = '/[locale]', // eg /docs/[locale] or /[locale]
384
- exclude = []) {
398
+ exclude = [], baseDomain) {
385
399
  // Use AST-based transformation for MDX files
386
- const result = transformMdxUrls(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude);
387
- return result.content;
400
+ return transformMdxUrls(file, defaultLocale, targetLocale, hideDefaultLocale, pattern, exclude, baseDomain || '');
401
+ }
402
+ function cleanPath(path) {
403
+ let cleanedPath = path.startsWith('/') ? path.slice(1) : path;
404
+ cleanedPath = cleanedPath.endsWith('/')
405
+ ? cleanedPath.slice(0, -1)
406
+ : cleanedPath;
407
+ return cleanedPath;
408
+ }
409
+ function checkIfPathMatchesPattern(originalUrlArray, patternHeadArray) {
410
+ // check if the pattern head matches the original path
411
+ for (let i = 0; i < patternHeadArray.length; i++) {
412
+ if (patternHeadArray[i] !== originalUrlArray?.[i]) {
413
+ return false;
414
+ }
415
+ }
416
+ return true;
388
417
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "2.1.6",
3
+ "version": "2.1.8-alpha.1",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [