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 +12 -0
- package/dist/cli/commands/translate.js +6 -7
- package/dist/config/generateSettings.js +1 -1
- package/dist/next/parse/wrapContent.js +16 -2
- package/dist/react/jsx/utils/constants.d.ts +3 -0
- package/dist/react/jsx/utils/constants.js +6 -2
- package/dist/react/jsx/utils/parseStringFunction.js +40 -22
- package/dist/react/jsx/utils/validateStringFunction.js +3 -2
- package/dist/react/parse/createInlineUpdates.js +7 -3
- package/dist/types/index.d.ts +1 -0
- package/dist/utils/localizeStaticUrls.d.ts +5 -1
- package/dist/utils/localizeStaticUrls.js +132 -103
- package/package.json +1 -1
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
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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 =
|
|
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
|
|
65
|
-
const
|
|
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
|
-
|
|
4
|
-
|
|
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
|
-
//
|
|
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 ===
|
|
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 &&
|
|
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
|
-
|
|
376
|
-
for
|
|
377
|
-
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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 ===
|
|
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,
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
51
|
-
settings.options?.experimentalHideDefaultLocale || false, settings.options?.docsUrlPattern, settings.options?.excludeStaticUrls);
|
|
52
|
-
//
|
|
53
|
-
|
|
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)
|
|
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
|
|
68
|
-
//
|
|
69
|
-
|
|
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
|
|
98
|
+
return urlToCheck.includes(patternWithoutSlash);
|
|
87
99
|
}
|
|
88
100
|
else {
|
|
89
101
|
// For non-default locales, check if URL starts with pattern
|
|
90
|
-
return
|
|
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
|
-
*
|
|
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
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
142
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
387
|
-
|
|
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
|
}
|