gtx-cli 1.2.21 → 1.2.23

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,20 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 1.2.23
4
+
5
+ ### Patch Changes
6
+
7
+ - [#336](https://github.com/generaltranslation/gt/pull/336) [`d22c287`](https://github.com/generaltranslation/gt/commit/d22c2871f1b474bc6cf981621a37400a92b4bbff) Thanks [@brian-lou](https://github.com/brian-lou)! - Fix string translation fallback function tracing when translating
8
+
9
+ ## 1.2.22
10
+
11
+ ### Patch Changes
12
+
13
+ - [#316](https://github.com/generaltranslation/gt/pull/316) [`274a88e`](https://github.com/generaltranslation/gt/commit/274a88e2ac2e4d60360bf950f56c4ee2850804fe) Thanks [@michellee-wang](https://github.com/michellee-wang)! - updated localeselector
14
+
15
+ - Updated dependencies [[`274a88e`](https://github.com/generaltranslation/gt/commit/274a88e2ac2e4d60360bf950f56c4ee2850804fe)]:
16
+ - generaltranslation@6.2.10
17
+
3
18
  ## 1.2.21
4
19
 
5
20
  ### Patch Changes
@@ -6,12 +6,12 @@ export declare const attributes: string[];
6
6
  *
7
7
  * Supports complex patterns including:
8
8
  * 1. Direct calls: const t = useGT(); t('hello');
9
- * 2. Function parameters: const t = useGT(); getInfo(t); where getInfo uses t() internally
10
- * 3. Cross-file function calls: imported functions that receive t as a parameter
9
+ * 2. Translation callback prop drilling: const t = useGT(); getInfo(t); where getInfo uses t() internally
10
+ * 3. Cross-file function calls: imported functions that receive the translation callback as a parameter
11
11
  *
12
12
  * Example flow:
13
13
  * - const t = useGT();
14
14
  * - const { home } = getInfo(t); // getInfo is imported from './constants'
15
- * - This will parse constants.ts to find t() calls within getInfo function
15
+ * - This will parse constants.ts to find translation calls within getInfo function
16
16
  */
17
17
  export declare function parseStrings(importName: string, path: NodePath, updates: Updates, errors: string[], file: string): void;
@@ -105,21 +105,163 @@ function processTranslationCall(tPath, updates, errors, file) {
105
105
  }
106
106
  }
107
107
  /**
108
- * Finds all usages of a function parameter within a function's scope and processes
109
- * any translation calls made with that parameter.
108
+ * Extracts the parameter name from a function parameter node, handling TypeScript annotations.
109
+ */
110
+ function extractParameterName(param) {
111
+ if (t.isIdentifier(param)) {
112
+ return param.name;
113
+ }
114
+ return null;
115
+ }
116
+ /**
117
+ * Builds a map of imported function names to their import paths from a given program path.
118
+ * Handles both named imports and default imports.
119
+ *
120
+ * Example: import { getInfo } from './constants' -> Map { 'getInfo' => './constants' }
121
+ * Example: import utils from './utils' -> Map { 'utils' => './utils' }
122
+ */
123
+ function buildImportMap(programPath) {
124
+ const importMap = new Map();
125
+ programPath.traverse({
126
+ ImportDeclaration(importPath) {
127
+ if (t.isStringLiteral(importPath.node.source)) {
128
+ const importSource = importPath.node.source.value;
129
+ importPath.node.specifiers.forEach((spec) => {
130
+ if (t.isImportSpecifier(spec) &&
131
+ t.isIdentifier(spec.imported) &&
132
+ t.isIdentifier(spec.local)) {
133
+ importMap.set(spec.local.name, importSource);
134
+ }
135
+ else if (t.isImportDefaultSpecifier(spec) &&
136
+ t.isIdentifier(spec.local)) {
137
+ importMap.set(spec.local.name, importSource);
138
+ }
139
+ });
140
+ }
141
+ },
142
+ });
143
+ return importMap;
144
+ }
145
+ /**
146
+ * Recursively resolves variable assignments to find all aliases of a translation callback parameter.
147
+ * Handles cases like: const t = translate; const a = translate; const b = a; const c = b;
148
+ *
149
+ * @param scope The scope to search within
150
+ * @param variableName The variable name to resolve
151
+ * @param visited Set to track already visited variables to prevent infinite loops
152
+ * @returns Array of all variable names that reference the original translation callback
153
+ */
154
+ function resolveVariableAliases(scope, variableName, visited = new Set()) {
155
+ if (visited.has(variableName)) {
156
+ return []; // Prevent infinite loops
157
+ }
158
+ visited.add(variableName);
159
+ const aliases = [variableName];
160
+ const binding = scope.bindings[variableName];
161
+ if (binding) {
162
+ // Look for variable declarations that assign this variable to another name
163
+ // Example: const t = translate; or const a = t;
164
+ for (const [otherVarName, otherBinding] of Object.entries(scope.bindings)) {
165
+ if (otherVarName === variableName || visited.has(otherVarName))
166
+ continue;
167
+ const otherBindingTyped = otherBinding;
168
+ if (otherBindingTyped.path &&
169
+ otherBindingTyped.path.isVariableDeclarator() &&
170
+ otherBindingTyped.path.node.init &&
171
+ t.isIdentifier(otherBindingTyped.path.node.init) &&
172
+ otherBindingTyped.path.node.init.name === variableName) {
173
+ // Found an alias: const otherVarName = variableName;
174
+ const nestedAliases = resolveVariableAliases(scope, otherVarName, visited);
175
+ aliases.push(...nestedAliases);
176
+ }
177
+ }
178
+ }
179
+ return aliases;
180
+ }
181
+ /**
182
+ * Handles how translation callbacks are used within code.
183
+ * This covers both direct translation calls (t('hello')) and prop drilling
184
+ * where the translation callback is passed to other functions (getData(t)).
185
+ */
186
+ function handleFunctionCall(tPath, updates, errors, file, importMap) {
187
+ if (tPath.parent.type === 'CallExpression' &&
188
+ tPath.parent.callee === tPath.node) {
189
+ // Direct translation call: t('hello')
190
+ processTranslationCall(tPath, updates, errors, file);
191
+ }
192
+ else if (tPath.parent.type === 'CallExpression' &&
193
+ t.isExpression(tPath.node) &&
194
+ tPath.parent.arguments.includes(tPath.node)) {
195
+ // Parameter passed to another function: getData(t)
196
+ const argIndex = tPath.parent.arguments.indexOf(tPath.node);
197
+ const callee = tPath.parent.callee;
198
+ if (t.isIdentifier(callee)) {
199
+ const calleeBinding = tPath.scope.getBinding(callee.name);
200
+ if (calleeBinding && calleeBinding.path.isFunction()) {
201
+ const functionPath = calleeBinding.path;
202
+ processFunctionIfMatches(callee.name, argIndex, functionPath.node, functionPath, updates, errors, file);
203
+ }
204
+ // Handle arrow functions assigned to variables: const getData = (t) => {...}
205
+ else if (calleeBinding &&
206
+ calleeBinding.path.isVariableDeclarator() &&
207
+ calleeBinding.path.node.init &&
208
+ (t.isArrowFunctionExpression(calleeBinding.path.node.init) ||
209
+ t.isFunctionExpression(calleeBinding.path.node.init))) {
210
+ const initPath = calleeBinding.path.get('init');
211
+ processFunctionIfMatches(callee.name, argIndex, calleeBinding.path.node.init, initPath, updates, errors, file);
212
+ }
213
+ // If not found locally, check if it's an imported function
214
+ else if (importMap.has(callee.name)) {
215
+ const importPath = importMap.get(callee.name);
216
+ const resolvedPath = resolveImportPath(file, importPath);
217
+ if (resolvedPath) {
218
+ findFunctionInFile(resolvedPath, callee.name, argIndex, updates, errors);
219
+ }
220
+ }
221
+ }
222
+ }
223
+ }
224
+ /**
225
+ * Processes a user-defined function that receives a translation callback as a parameter.
226
+ * Validates the function has enough parameters and traces how the translation callback
227
+ * is used within that function's body.
228
+ */
229
+ function processFunctionIfMatches(_functionName, argIndex, functionNode, functionPath, updates, errors, filePath) {
230
+ if (functionNode.params.length > argIndex) {
231
+ const param = functionNode.params[argIndex];
232
+ const paramName = extractParameterName(param);
233
+ if (paramName) {
234
+ findFunctionParameterUsage(functionPath, paramName, updates, errors, filePath);
235
+ }
236
+ }
237
+ }
238
+ /**
239
+ * Finds all usages of a translation callback parameter within a user-defined function's scope.
240
+ * Processes both direct translation calls and cases where the translation callback is passed
241
+ * to other functions (prop drilling).
110
242
  *
111
243
  * Example: In function getInfo(t) { return t('hello'); }, this finds the t('hello') call.
244
+ * Example: In function getData(t) { return getFooter(t); }, this finds and traces into getFooter.
112
245
  */
113
246
  function findFunctionParameterUsage(functionPath, parameterName, updates, errors, file) {
114
247
  // Look for the function body and find all usages of the parameter
115
248
  if (functionPath.isFunction()) {
116
249
  const functionScope = functionPath.scope;
117
- const binding = functionScope.bindings[parameterName];
118
- if (binding) {
119
- binding.referencePaths.forEach((refPath) => {
120
- processTranslationCall(refPath, updates, errors, file);
121
- });
122
- }
250
+ // Resolve all aliases of the translation callback parameter
251
+ // Example: translate -> [translate, t, a, b] for const t = translate; const a = t; const b = a;
252
+ const allParameterNames = resolveVariableAliases(functionScope, parameterName);
253
+ // Build import map for this function's scope to handle cross-file calls
254
+ const programPath = functionPath.scope.getProgramParent().path;
255
+ const importMap = buildImportMap(programPath);
256
+ // Process references for all parameter names and their aliases
257
+ allParameterNames.forEach((name) => {
258
+ const binding = functionScope.bindings[name];
259
+ if (binding) {
260
+ binding.referencePaths.forEach((refPath) => {
261
+ handleFunctionCall(refPath, updates, errors, file, importMap);
262
+ });
263
+ }
264
+ });
123
265
  }
124
266
  }
125
267
  /**
@@ -164,12 +306,31 @@ function resolveImportPath(currentFile, importPath) {
164
306
  return resolve.sync(importPath, { basedir, extensions });
165
307
  }
166
308
  catch {
309
+ // If resolution fails, try to manually replace .js/.jsx with .ts/.tsx for source files
310
+ if (importPath.endsWith('.js')) {
311
+ const tsPath = importPath.replace(/\.js$/, '.ts');
312
+ try {
313
+ return resolve.sync(tsPath, { basedir, extensions });
314
+ }
315
+ catch {
316
+ // Continue to return null
317
+ }
318
+ }
319
+ else if (importPath.endsWith('.jsx')) {
320
+ const tsxPath = importPath.replace(/\.jsx$/, '.tsx');
321
+ try {
322
+ return resolve.sync(tsxPath, { basedir, extensions });
323
+ }
324
+ catch {
325
+ // Continue to return null
326
+ }
327
+ }
167
328
  return null;
168
329
  }
169
330
  }
170
331
  /**
171
- * Searches for a specific function in a file and analyzes how a particular parameter
172
- * (at argIndex position) is used within that function for translation calls.
332
+ * Searches for a specific user-defined function in a file and analyzes how a translation callback
333
+ * parameter (at argIndex position) is used within that function.
173
334
  *
174
335
  * Handles multiple function declaration patterns:
175
336
  * - function getInfo(t) { ... }
@@ -186,25 +347,8 @@ function findFunctionInFile(filePath, functionName, argIndex, updates, errors) {
186
347
  (0, traverse_1.default)(ast, {
187
348
  // Handle function declarations: function getInfo(t) { ... }
188
349
  FunctionDeclaration(path) {
189
- if (path.node.id?.name === functionName &&
190
- path.node.params.length > argIndex) {
191
- const param = path.node.params[argIndex];
192
- if (t.isIdentifier(param)) {
193
- findFunctionParameterUsage(path, param.name, updates, errors, filePath);
194
- }
195
- }
196
- },
197
- // Handle exported function declarations: export function getInfo(t) { ... }
198
- ExportNamedDeclaration(path) {
199
- if (path.node.declaration &&
200
- t.isFunctionDeclaration(path.node.declaration)) {
201
- const func = path.node.declaration;
202
- if (func.id?.name === functionName && func.params.length > argIndex) {
203
- const param = func.params[argIndex];
204
- if (t.isIdentifier(param)) {
205
- findFunctionParameterUsage(path.get('declaration'), param.name, updates, errors, filePath);
206
- }
207
- }
350
+ if (path.node.id?.name === functionName) {
351
+ processFunctionIfMatches(functionName, argIndex, path.node, path, updates, errors, filePath);
208
352
  }
209
353
  },
210
354
  // Handle variable declarations: const getInfo = (t) => { ... }
@@ -213,13 +357,9 @@ function findFunctionInFile(filePath, functionName, argIndex, updates, errors) {
213
357
  path.node.id.name === functionName &&
214
358
  path.node.init &&
215
359
  (t.isArrowFunctionExpression(path.node.init) ||
216
- t.isFunctionExpression(path.node.init)) &&
217
- path.node.init.params.length > argIndex) {
218
- const param = path.node.init.params[argIndex];
219
- if (t.isIdentifier(param)) {
220
- const initPath = path.get('init');
221
- findFunctionParameterUsage(initPath, param.name, updates, errors, filePath);
222
- }
360
+ t.isFunctionExpression(path.node.init))) {
361
+ const initPath = path.get('init');
362
+ processFunctionIfMatches(functionName, argIndex, path.node.init, initPath, updates, errors, filePath);
223
363
  }
224
364
  },
225
365
  });
@@ -233,35 +373,17 @@ function findFunctionInFile(filePath, functionName, argIndex, updates, errors) {
233
373
  *
234
374
  * Supports complex patterns including:
235
375
  * 1. Direct calls: const t = useGT(); t('hello');
236
- * 2. Function parameters: const t = useGT(); getInfo(t); where getInfo uses t() internally
237
- * 3. Cross-file function calls: imported functions that receive t as a parameter
376
+ * 2. Translation callback prop drilling: const t = useGT(); getInfo(t); where getInfo uses t() internally
377
+ * 3. Cross-file function calls: imported functions that receive the translation callback as a parameter
238
378
  *
239
379
  * Example flow:
240
380
  * - const t = useGT();
241
381
  * - const { home } = getInfo(t); // getInfo is imported from './constants'
242
- * - This will parse constants.ts to find t() calls within getInfo function
382
+ * - This will parse constants.ts to find translation calls within getInfo function
243
383
  */
244
384
  function parseStrings(importName, path, updates, errors, file) {
245
385
  // First, collect all imports in this file to track cross-file function calls
246
- const importMap = new Map(); // functionName -> importPath
247
- path.scope.getProgramParent().path.traverse({
248
- ImportDeclaration(importPath) {
249
- if (t.isStringLiteral(importPath.node.source)) {
250
- const importSource = importPath.node.source.value;
251
- importPath.node.specifiers.forEach((spec) => {
252
- if (t.isImportSpecifier(spec) &&
253
- t.isIdentifier(spec.imported) &&
254
- t.isIdentifier(spec.local)) {
255
- importMap.set(spec.local.name, importSource);
256
- }
257
- else if (t.isImportDefaultSpecifier(spec) &&
258
- t.isIdentifier(spec.local)) {
259
- importMap.set(spec.local.name, importSource);
260
- }
261
- });
262
- }
263
- },
264
- });
386
+ const importMap = buildImportMap(path.scope.getProgramParent().path);
265
387
  const referencePaths = path.scope.bindings[importName]?.referencePaths || [];
266
388
  for (const refPath of referencePaths) {
267
389
  // Find call expressions of useGT() / await getGT()
@@ -280,40 +402,7 @@ function parseStrings(importName, path, updates, errors, file) {
280
402
  const variableScope = effectiveParent.scope;
281
403
  const tReferencePaths = variableScope.bindings[tFuncName]?.referencePaths || [];
282
404
  for (const tPath of tReferencePaths) {
283
- // Check if this is a direct call to the translation function
284
- if (tPath.parent.type === 'CallExpression' &&
285
- tPath.parent.callee === tPath.node) {
286
- processTranslationCall(tPath, updates, errors, file);
287
- }
288
- // Check if this is being passed as an argument to another function
289
- else if (tPath.parent.type === 'CallExpression' &&
290
- t.isExpression(tPath.node) &&
291
- tPath.parent.arguments.includes(tPath.node)) {
292
- // Find which parameter position this is
293
- const argIndex = tPath.parent.arguments.indexOf(tPath.node);
294
- // Try to find the function definition being called
295
- const callee = tPath.parent.callee;
296
- if (t.isIdentifier(callee)) {
297
- // Look for function declarations or function expressions with this name
298
- const calleeBinding = tPath.scope.getBinding(callee.name);
299
- if (calleeBinding && calleeBinding.path.isFunction()) {
300
- const functionPath = calleeBinding.path;
301
- const params = functionPath.node.params;
302
- if (params[argIndex] && t.isIdentifier(params[argIndex])) {
303
- const paramName = params[argIndex].name;
304
- findFunctionParameterUsage(functionPath, paramName, updates, errors, file);
305
- }
306
- }
307
- // If not found locally, check if it's an imported function
308
- else if (importMap.has(callee.name)) {
309
- const importPath = importMap.get(callee.name);
310
- const resolvedPath = resolveImportPath(file, importPath);
311
- if (resolvedPath) {
312
- findFunctionInFile(resolvedPath, callee.name, argIndex, updates, errors);
313
- }
314
- }
315
- }
316
- }
405
+ handleFunctionCall(tPath, updates, errors, file, importMap);
317
406
  }
318
407
  }
319
408
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gtx-cli",
3
- "version": "1.2.21",
3
+ "version": "1.2.23",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [
@@ -79,7 +79,7 @@
79
79
  "esbuild": "^0.25.4",
80
80
  "fast-glob": "^3.3.3",
81
81
  "form-data": "^4.0.2",
82
- "generaltranslation": "^6.2.9",
82
+ "generaltranslation": "^6.2.10",
83
83
  "open": "^10.1.1",
84
84
  "ora": "^8.2.0",
85
85
  "resolve": "^1.22.10",