gt 2.14.3 → 2.14.4

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,15 @@
1
1
  # gtx-cli
2
2
 
3
+ ## 2.14.4
4
+
5
+ ### Patch Changes
6
+
7
+ - [#1158](https://github.com/generaltranslation/gt/pull/1158) [`5b85ccd`](https://github.com/generaltranslation/gt/commit/5b85ccd80b93b91eae9c873b258a13b6a57443c8) Thanks [@ErnestM1234](https://github.com/ErnestM1234)! - add auto injection for jsx translation
8
+
9
+ - Updated dependencies [[`5b85ccd`](https://github.com/generaltranslation/gt/commit/5b85ccd80b93b91eae9c873b258a13b6a57443c8)]:
10
+ - generaltranslation@8.2.2
11
+ - @generaltranslation/python-extractor@0.2.6
12
+
3
13
  ## 2.14.3
4
14
 
5
15
  ### Patch Changes
@@ -4,8 +4,9 @@
4
4
  * @property {boolean} includeSourceCodeContext - Include surrounding source code lines as context for translations.
5
5
  */
6
6
  export const GT_PARSING_FLAGS_DEFAULT = {
7
- autoDerive: true,
7
+ autoDerive: false,
8
8
  includeSourceCodeContext: false,
9
+ enableAutoJsxInjection: false,
9
10
  };
10
11
  /**
11
12
  * Default parsing flags for all files
@@ -5,6 +5,7 @@ export declare const warnMissingReturnSync: (file: string, functionName: string,
5
5
  export declare const warnHasUnwrappedExpressionSync: (file: string, unwrappedExpressions: string[], id?: string, location?: string) => string;
6
6
  export declare const warnFailedToConstructJsxTreeSync: (file: string, code: string, location?: string) => string;
7
7
  export declare const warnNestedTComponent: (file: string, location?: string) => string;
8
+ export declare const warnNestedInternalTComponent: (file: string, location?: string) => string;
8
9
  export declare const warnNonStaticExpressionSync: (file: string, attrName: string, value: string, location?: string) => string;
9
10
  export declare const warnInvalidMaxCharsSync: (file: string, value: string, location?: string) => string;
10
11
  export declare const warnInvalidFormatSync: (file: string, value: string, location?: string) => string;
@@ -14,6 +14,7 @@ export const warnMissingReturnSync = (file, functionName, location) => withLocat
14
14
  export const warnHasUnwrappedExpressionSync = (file, unwrappedExpressions, id, location) => withLocation(file, `${colorizeComponent('<T>')} component${id ? ` with id ${colorizeIdString(id)}` : ''} has children that could change at runtime. Use a variable component like ${colorizeComponent('<Var>')} to ensure this content is translated.\n${colorizeContent(unwrappedExpressions.join('\n'))}`, location);
15
15
  export const warnFailedToConstructJsxTreeSync = (file, code, location) => withLocation(file, `Failed to construct JsxTree! Call expression is not a valid createElement call: ${colorizeContent(code)}`, location);
16
16
  export const warnNestedTComponent = (file, location) => withLocation(file, `Found nested <T> component. <T> components cannot be directly nested.`, location);
17
+ export const warnNestedInternalTComponent = (file, location) => withLocation(file, `DEBUG: Found nested <GtInternalTranslateJsx> component. <GtInternalTranslateJsx> components cannot be directly nested.`, location);
17
18
  export const warnNonStaticExpressionSync = (file, attrName, value, location) => withLocation(file, `Found non-static expression for attribute ${colorizeIdString(attrName)}: ${colorizeContent(value)}. Change "${colorizeIdString(attrName)}" to ensure this content is translated.`, location);
18
19
  export const warnInvalidMaxCharsSync = (file, value, location) => withLocation(file, `Found invalid maxChars value: ${colorizeContent(value)}. Change the value to a valid number to ensure this content is translated.`, location);
19
20
  export const warnInvalidFormatSync = (file, value, location) => withLocation(file, `Found invalid $format value: ${colorizeContent(value)}. Must be one of: 'ICU', 'STRING', 'I18NEXT'.`, location);
@@ -1 +1 @@
1
- export declare const PACKAGE_VERSION = "2.14.3";
1
+ export declare const PACKAGE_VERSION = "2.14.4";
@@ -1,2 +1,2 @@
1
1
  // This file is auto-generated. Do not edit manually.
2
- export const PACKAGE_VERSION = '2.14.3';
2
+ export const PACKAGE_VERSION = '2.14.4';
@@ -18,6 +18,9 @@ export declare const TRANSLATION_COMPONENT = "T";
18
18
  export declare const STATIC_COMPONENT = "Static";
19
19
  export declare const DERIVE_COMPONENT = "Derive";
20
20
  export declare const BRANCH_COMPONENT = "Branch";
21
+ export declare const DEFAULT_GT_IMPORT_SOURCE = "gt-react/browser";
22
+ export declare const INTERNAL_TRANSLATION_COMPONENT = "GtInternalTranslateJsx";
23
+ export declare const INTERNAL_VAR_COMPONENT = "GtInternalVar";
21
24
  export declare const VAR_COMPONENT = "Var";
22
25
  export declare const DATETIME_COMPONENT = "DateTime";
23
26
  export declare const RELATIVE_TIME_COMPONENT = "RelativeTime";
@@ -31,4 +34,8 @@ export declare const VARIABLE_COMPONENTS: string[];
31
34
  export declare const GT_ATTRIBUTES_WITH_SUGAR: readonly ["$id", "$context", "$maxChars", "$format"];
32
35
  export declare const GT_ATTRIBUTES: readonly ["id", "context", "maxChars", "$id", "$context", "$maxChars", "$format"];
33
36
  export declare const DATA_ATTR_PREFIX: "data-";
37
+ /** Branch control props — not translatable content. */
38
+ export declare const BRANCH_CONTROL_PROPS: Set<string>;
39
+ /** Plural control props — not translatable content. */
40
+ export declare const PLURAL_CONTROL_PROPS: Set<string>;
34
41
  export declare const T_GLOBAL_REGISTRATION_FUNCTION_MARKER = "_gt_internal_t_global_registration_marker";
@@ -18,6 +18,9 @@ export const TRANSLATION_COMPONENT = 'T';
18
18
  export const STATIC_COMPONENT = 'Static';
19
19
  export const DERIVE_COMPONENT = 'Derive';
20
20
  export const BRANCH_COMPONENT = 'Branch';
21
+ export const DEFAULT_GT_IMPORT_SOURCE = 'gt-react/browser';
22
+ export const INTERNAL_TRANSLATION_COMPONENT = 'GtInternalTranslateJsx';
23
+ export const INTERNAL_VAR_COMPONENT = 'GtInternalVar';
21
24
  // Variable components
22
25
  export const VAR_COMPONENT = 'Var';
23
26
  export const DATETIME_COMPONENT = 'DateTime';
@@ -45,6 +48,8 @@ export const GT_TRANSLATION_FUNCS = [
45
48
  CURRENCY_COMPONENT,
46
49
  NUM_COMPONENT,
47
50
  BRANCH_COMPONENT,
51
+ INTERNAL_TRANSLATION_COMPONENT,
52
+ INTERNAL_VAR_COMPONENT,
48
53
  PLURAL_COMPONENT,
49
54
  ];
50
55
  // GT String translation functions
@@ -66,6 +71,7 @@ export const VARIABLE_COMPONENTS = [
66
71
  NUM_COMPONENT,
67
72
  STATIC_COMPONENT,
68
73
  DERIVE_COMPONENT,
74
+ INTERNAL_VAR_COMPONENT,
69
75
  ];
70
76
  export const GT_ATTRIBUTES_WITH_SUGAR = [
71
77
  '$id',
@@ -81,5 +87,9 @@ export const GT_ATTRIBUTES = [
81
87
  ];
82
88
  // Data attribute prefix injected by build tools
83
89
  export const DATA_ATTR_PREFIX = 'data-';
90
+ /** Branch control props — not translatable content. */
91
+ export const BRANCH_CONTROL_PROPS = new Set(['branch']);
92
+ /** Plural control props — not translatable content. */
93
+ export const PLURAL_CONTROL_PROPS = new Set(['n', 'locales']);
84
94
  // demarcation for global t macro
85
95
  export const T_GLOBAL_REGISTRATION_FUNCTION_MARKER = '_gt_internal_t_global_registration_marker';
@@ -1,5 +1,5 @@
1
1
  import traverseModule from '@babel/traverse';
2
- import { GT_TRANSLATION_FUNCS, INLINE_TRANSLATION_HOOK, INLINE_TRANSLATION_HOOK_ASYNC, INLINE_MESSAGE_HOOK, INLINE_MESSAGE_HOOK_ASYNC, MSG_REGISTRATION_FUNCTION, T_REGISTRATION_FUNCTION, TRANSLATION_COMPONENT, T_GLOBAL_REGISTRATION_FUNCTION, T_GLOBAL_REGISTRATION_FUNCTION_MARKER, } from '../../jsx/utils/constants.js';
2
+ import { GT_TRANSLATION_FUNCS, INLINE_TRANSLATION_HOOK, INLINE_TRANSLATION_HOOK_ASYNC, INLINE_MESSAGE_HOOK, INLINE_MESSAGE_HOOK_ASYNC, MSG_REGISTRATION_FUNCTION, T_REGISTRATION_FUNCTION, TRANSLATION_COMPONENT, INTERNAL_TRANSLATION_COMPONENT, T_GLOBAL_REGISTRATION_FUNCTION, T_GLOBAL_REGISTRATION_FUNCTION_MARKER, } from '../../jsx/utils/constants.js';
3
3
  import { extractImportName } from './parseAst.js';
4
4
  import * as t from '@babel/types';
5
5
  // Handle CommonJS/ESM interop
@@ -45,7 +45,8 @@ export function getPathsAndAliases(ast, pkgs) {
45
45
  originalName: name.original,
46
46
  });
47
47
  }
48
- else if (name.original === TRANSLATION_COMPONENT) {
48
+ else if (name.original === TRANSLATION_COMPONENT ||
49
+ name.original === INTERNAL_TRANSLATION_COMPONENT) {
49
50
  translationComponentPaths.push({
50
51
  localName: name.local,
51
52
  path,
@@ -82,7 +83,8 @@ export function getPathsAndAliases(ast, pkgs) {
82
83
  originalName: name.original,
83
84
  });
84
85
  }
85
- else if (name.original === TRANSLATION_COMPONENT) {
86
+ else if (name.original === TRANSLATION_COMPONENT ||
87
+ name.original === INTERNAL_TRANSLATION_COMPONENT) {
86
88
  translationComponentPaths.push({
87
89
  localName: name.local,
88
90
  path: parentPath,
@@ -6,4 +6,4 @@ import { MultipliedTreeNode } from './types.js';
6
6
  * @param startingIndex - The starting index for GT IDs
7
7
  * @returns The tree with GT identifiers added
8
8
  */
9
- export default function addGTIdentifierToSyntaxTree(tree: MultipliedTreeNode, startingIndex?: number): JsxChildren;
9
+ export default function addGTIdentifierToSyntaxTree(tree: MultipliedTreeNode, startingIndex?: number, gtVariableNames?: Set<string>): JsxChildren;
@@ -1,7 +1,7 @@
1
1
  import { HTML_CONTENT_PROPS, } from 'generaltranslation/types';
2
2
  import { defaultVariableNames, getVariableName, minifyVariableType, } from '../../../utils/getVariableName.js';
3
3
  import { isAcceptedPluralForm } from 'generaltranslation/internal';
4
- import { DATA_ATTR_PREFIX } from '../constants.js';
4
+ import { DATA_ATTR_PREFIX, BRANCH_COMPONENT, PLURAL_COMPONENT, } from '../constants.js';
5
5
  /**
6
6
  * Construct the data-_gt prop
7
7
  * @param type - The type of the element
@@ -18,7 +18,7 @@ function constructGTProp(type, props, id) {
18
18
  return acc;
19
19
  }, {});
20
20
  // Plural
21
- if (type === 'Plural') {
21
+ if (type === PLURAL_COMPONENT) {
22
22
  const pluralBranches = Object.entries(props).reduce((acc, [branchName, branch]) => {
23
23
  if (isAcceptedPluralForm(branchName)) {
24
24
  acc[branchName] = addGTIdentifierToSyntaxTree(branch, id);
@@ -32,7 +32,7 @@ function constructGTProp(type, props, id) {
32
32
  }
33
33
  // Branch
34
34
  }
35
- else if (type === 'Branch') {
35
+ else if (type === BRANCH_COMPONENT) {
36
36
  // eslint-disable-next-line @typescript-eslint/no-unused-vars, no-unused-vars
37
37
  const { children, branch, ...allBranches } = props;
38
38
  // Filter out data-* attributes injected by build tools
@@ -57,7 +57,7 @@ function constructGTProp(type, props, id) {
57
57
  * @param startingIndex - The starting index for GT IDs
58
58
  * @returns The tree with GT identifiers added
59
59
  */
60
- export default function addGTIdentifierToSyntaxTree(tree, startingIndex = 0) {
60
+ export default function addGTIdentifierToSyntaxTree(tree, startingIndex = 0, gtVariableNames) {
61
61
  // Edge case: boolean or null, just return the tree
62
62
  if (typeof tree === 'boolean' || tree === null) {
63
63
  return tree;
@@ -79,8 +79,11 @@ export default function addGTIdentifierToSyntaxTree(tree, startingIndex = 0) {
79
79
  if (type === 'React.Fragment') {
80
80
  type = '';
81
81
  }
82
- // Variables
83
- if (Object.keys(defaultVariableNames).includes(type)) {
82
+ // Variables — only treat as GT variable if confirmed as GT import
83
+ const isGTVariable = gtVariableNames
84
+ ? gtVariableNames.has(type)
85
+ : Object.keys(defaultVariableNames).includes(type);
86
+ if (isGTVariable) {
84
87
  const variableType = minifyVariableType(type);
85
88
  const variableName = getVariableName(props, type, indexObject.index);
86
89
  return {
@@ -91,9 +94,21 @@ export default function addGTIdentifierToSyntaxTree(tree, startingIndex = 0) {
91
94
  }
92
95
  // Construct the data-_gt prop
93
96
  const generaltranslation = constructGTProp(type, (props || {}), indexObject.index);
94
- // Save current index and recurse
97
+ // Save current index and recurse.
98
+ // For Branch/Plural, children use an independent index counter
99
+ // (same as branch props) so they don't inflate sibling indices.
100
+ // This matches the compiler's id.copy() behavior.
95
101
  const currentIndex = indexObject.index;
96
- const children = handleChildren(props?.children === undefined ? null : props?.children);
102
+ const isBranching = type === BRANCH_COMPONENT || type === PLURAL_COMPONENT;
103
+ let children;
104
+ if (isBranching) {
105
+ const savedIndex = indexObject.index;
106
+ children = handleChildren(props?.children === undefined ? null : props?.children);
107
+ indexObject.index = savedIndex;
108
+ }
109
+ else {
110
+ children = handleChildren(props?.children === undefined ? null : props?.children);
111
+ }
97
112
  // Enforce boolean rendering behavior
98
113
  // <T>{true}</T> -> true <- this is a boolean value, not a string
99
114
  // <T>{false}</T> -> nothing
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Auto JSX Insertion for CLI extraction.
3
+ *
4
+ * Inserts <T> and <Var> JSX elements into the AST so the extraction pipeline
5
+ * can process them as if the user wrote them. This operates on raw JSX syntax
6
+ * (JSXElement, JSXText, JSXExpressionContainer) — NOT compiled jsx() calls.
7
+ *
8
+ * Rules follow JSX_INSERTION_RULES.md from the compiler package.
9
+ */
10
+ import * as t from '@babel/types';
11
+ /** Check if a node was auto-inserted */
12
+ export declare function isAutoInserted(node: t.Node): boolean;
13
+ /**
14
+ * Ensure GtInternalTranslateJsx and GtInternalVar are imported in the AST.
15
+ * Always adds: import { GtInternalTranslateJsx, GtInternalVar } from 'gt-react/browser';
16
+ * These are distinct from user T/Var so there's no ambiguity.
17
+ *
18
+ * Updates importAliases in-place.
19
+ */
20
+ export declare function ensureTAndVarImported(ast: t.File, importAliases: Record<string, string>): void;
21
+ /**
22
+ * Traverse the AST and insert GtInternalTranslateJsx and GtInternalVar JSX elements following
23
+ * the insertion rules. Uses deliberate children traversal.
24
+ *
25
+ * Every inserted node gets node._autoInserted = true.
26
+ */
27
+ export declare function autoInsertJsxComponents(ast: t.File, importAliases: Record<string, string>): void;
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Auto JSX Insertion for CLI extraction.
3
+ *
4
+ * Inserts <T> and <Var> JSX elements into the AST so the extraction pipeline
5
+ * can process them as if the user wrote them. This operates on raw JSX syntax
6
+ * (JSXElement, JSXText, JSXExpressionContainer) — NOT compiled jsx() calls.
7
+ *
8
+ * Rules follow JSX_INSERTION_RULES.md from the compiler package.
9
+ */
10
+ import * as t from '@babel/types';
11
+ import traverseModule from '@babel/traverse';
12
+ const traverse = traverseModule.default || traverseModule;
13
+ import { isStaticExpression } from '../../evaluateJsx.js';
14
+ import { TRANSLATION_COMPONENT, INTERNAL_TRANSLATION_COMPONENT, INTERNAL_VAR_COMPONENT, VARIABLE_COMPONENTS, BRANCH_COMPONENT, PLURAL_COMPONENT, DEFAULT_GT_IMPORT_SOURCE, DERIVE_COMPONENT, STATIC_COMPONENT, BRANCH_CONTROL_PROPS, PLURAL_CONTROL_PROPS, } from '../constants.js';
15
+ /** Tracks which AST nodes were auto-inserted by this module */
16
+ const autoInsertedNodes = new WeakSet();
17
+ /** Check if a node was auto-inserted */
18
+ export function isAutoInserted(node) {
19
+ return autoInsertedNodes.has(node);
20
+ }
21
+ // ===== Public API ===== //
22
+ /**
23
+ * Ensure GtInternalTranslateJsx and GtInternalVar are imported in the AST.
24
+ * Always adds: import { GtInternalTranslateJsx, GtInternalVar } from 'gt-react/browser';
25
+ * These are distinct from user T/Var so there's no ambiguity.
26
+ *
27
+ * Updates importAliases in-place.
28
+ */
29
+ export function ensureTAndVarImported(ast, importAliases) {
30
+ // Check if internal components are already imported
31
+ const hasInternalT = Object.values(importAliases).includes(INTERNAL_TRANSLATION_COMPONENT);
32
+ const hasInternalVar = Object.values(importAliases).includes(INTERNAL_VAR_COMPONENT);
33
+ if (hasInternalT && hasInternalVar)
34
+ return;
35
+ const specifiers = [];
36
+ if (!hasInternalT) {
37
+ specifiers.push(t.importSpecifier(t.identifier(INTERNAL_TRANSLATION_COMPONENT), t.identifier(INTERNAL_TRANSLATION_COMPONENT)));
38
+ importAliases[INTERNAL_TRANSLATION_COMPONENT] =
39
+ INTERNAL_TRANSLATION_COMPONENT;
40
+ }
41
+ if (!hasInternalVar) {
42
+ specifiers.push(t.importSpecifier(t.identifier(INTERNAL_VAR_COMPONENT), t.identifier(INTERNAL_VAR_COMPONENT)));
43
+ importAliases[INTERNAL_VAR_COMPONENT] = INTERNAL_VAR_COMPONENT;
44
+ }
45
+ const importDecl = t.importDeclaration(specifiers, t.stringLiteral(DEFAULT_GT_IMPORT_SOURCE));
46
+ traverse(ast, {
47
+ Program(path) {
48
+ path.unshiftContainer('body', importDecl);
49
+ path.stop();
50
+ },
51
+ });
52
+ }
53
+ /**
54
+ * Traverse the AST and insert GtInternalTranslateJsx and GtInternalVar JSX elements following
55
+ * the insertion rules. Uses deliberate children traversal.
56
+ *
57
+ * Every inserted node gets node._autoInserted = true.
58
+ */
59
+ export function autoInsertJsxComponents(ast, importAliases) {
60
+ const processedNodes = new WeakSet();
61
+ const tLocalName = getLocalName(importAliases, INTERNAL_TRANSLATION_COMPONENT);
62
+ const varLocalName = getLocalName(importAliases, INTERNAL_VAR_COMPONENT);
63
+ traverse(ast, {
64
+ JSXElement(path) {
65
+ if (processedNodes.has(path.node))
66
+ return;
67
+ processJsxElement({
68
+ path,
69
+ insideAutoT: false,
70
+ importAliases,
71
+ processedNodes,
72
+ tLocalName,
73
+ varLocalName,
74
+ });
75
+ },
76
+ JSXFragment(path) {
77
+ if (processedNodes.has(path.node))
78
+ return;
79
+ processJsxFragment({
80
+ path,
81
+ insideAutoT: false,
82
+ importAliases,
83
+ processedNodes,
84
+ tLocalName,
85
+ varLocalName,
86
+ });
87
+ },
88
+ });
89
+ }
90
+ function processJsxElement({ path, insideAutoT, importAliases, processedNodes, tLocalName, varLocalName, }) {
91
+ processedNodes.add(path.node);
92
+ // Get component type
93
+ const typeName = getElementTypeName(path.node);
94
+ const canonicalName = typeName ? importAliases[typeName] : undefined;
95
+ // User T → mark all descendants, hands off
96
+ if (canonicalName === TRANSLATION_COMPONENT) {
97
+ markDescendantJsx(path, processedNodes);
98
+ return;
99
+ }
100
+ // Branch/Plural/Derive/Static → opaque, process props for dynamic Var
101
+ // Must be checked BEFORE user variable check because Derive/Static are in VARIABLE_COMPONENTS
102
+ if (canonicalName === BRANCH_COMPONENT ||
103
+ canonicalName === PLURAL_COMPONENT ||
104
+ canonicalName === DERIVE_COMPONENT ||
105
+ canonicalName === STATIC_COMPONENT) {
106
+ if (!insideAutoT) {
107
+ // Root-level opaque component — wrap in _T
108
+ const tWrapper = createTWrapper([path.node], tLocalName);
109
+ processedNodes.add(tWrapper);
110
+ path.replaceWith(tWrapper);
111
+ }
112
+ processOpaqueComponentProps({
113
+ path,
114
+ insideAutoT: true,
115
+ importAliases,
116
+ processedNodes,
117
+ tLocalName,
118
+ varLocalName,
119
+ canonicalName,
120
+ });
121
+ return;
122
+ }
123
+ // User Var/Num/Currency/DateTime → mark descendants, hands off
124
+ if (canonicalName && VARIABLE_COMPONENTS.includes(canonicalName)) {
125
+ markDescendantJsx(path, processedNodes);
126
+ return;
127
+ }
128
+ // Process children
129
+ processElementChildren({
130
+ path,
131
+ insideAutoT,
132
+ importAliases,
133
+ processedNodes,
134
+ tLocalName,
135
+ varLocalName,
136
+ });
137
+ }
138
+ function processJsxFragment({ path, insideAutoT, importAliases, processedNodes, tLocalName, varLocalName, }) {
139
+ processedNodes.add(path.node);
140
+ // Fragments are treated like regular elements
141
+ processFragmentChildren({
142
+ path,
143
+ insideAutoT,
144
+ importAliases,
145
+ processedNodes,
146
+ tLocalName,
147
+ varLocalName,
148
+ });
149
+ }
150
+ // ===== Children processing ===== //
151
+ function processElementChildren({ path, insideAutoT, importAliases, processedNodes, tLocalName, varLocalName, }) {
152
+ const children = path.node.children;
153
+ const ctx = {
154
+ insideAutoT,
155
+ importAliases,
156
+ processedNodes,
157
+ tLocalName,
158
+ varLocalName,
159
+ };
160
+ const hasText = hasTranslatableText(children);
161
+ const hasOpaque = hasOpaqueGTChild(children, importAliases);
162
+ const shouldClaimT = !insideAutoT && (hasText || hasOpaque);
163
+ if (shouldClaimT) {
164
+ // Process children: wrap dynamic expressions in Var, recurse into child elements
165
+ const childPaths = path.get('children');
166
+ for (const childPath of childPaths) {
167
+ processChild(childPath, { ...ctx, insideAutoT: true });
168
+ }
169
+ // Wrap all children in <T>
170
+ wrapChildrenInT(path, tLocalName, processedNodes);
171
+ }
172
+ else if (insideAutoT) {
173
+ // Inside a T region: wrap dynamic expressions, recurse
174
+ const childPaths = path.get('children');
175
+ for (const childPath of childPaths) {
176
+ processChild(childPath, ctx);
177
+ }
178
+ }
179
+ else {
180
+ // No text, no opaque, not inside T: just recurse into child elements
181
+ const childPaths = path.get('children');
182
+ for (const childPath of childPaths) {
183
+ if (childPath.isJSXElement() && !processedNodes.has(childPath.node)) {
184
+ processJsxElement({ path: childPath, ...ctx });
185
+ }
186
+ else if (childPath.isJSXFragment() &&
187
+ !processedNodes.has(childPath.node)) {
188
+ processJsxFragment({ path: childPath, ...ctx });
189
+ }
190
+ }
191
+ }
192
+ }
193
+ function processFragmentChildren({ path, insideAutoT, importAliases, processedNodes, tLocalName, varLocalName, }) {
194
+ const children = path.node.children;
195
+ const ctx = {
196
+ insideAutoT,
197
+ importAliases,
198
+ processedNodes,
199
+ tLocalName,
200
+ varLocalName,
201
+ };
202
+ const hasText = hasTranslatableText(children);
203
+ const hasOpaque = hasOpaqueGTChild(children, importAliases);
204
+ const shouldClaimT = !insideAutoT && (hasText || hasOpaque);
205
+ if (shouldClaimT) {
206
+ const childPaths = path.get('children');
207
+ for (const childPath of childPaths) {
208
+ processChild(childPath, { ...ctx, insideAutoT: true });
209
+ }
210
+ wrapFragmentChildrenInT(path, tLocalName, processedNodes);
211
+ }
212
+ else if (insideAutoT) {
213
+ const childPaths = path.get('children');
214
+ for (const childPath of childPaths) {
215
+ processChild(childPath, ctx);
216
+ }
217
+ }
218
+ else {
219
+ const childPaths = path.get('children');
220
+ for (const childPath of childPaths) {
221
+ if (childPath.isJSXElement() && !processedNodes.has(childPath.node)) {
222
+ processJsxElement({ path: childPath, ...ctx });
223
+ }
224
+ else if (childPath.isJSXFragment() &&
225
+ !processedNodes.has(childPath.node)) {
226
+ processJsxFragment({ path: childPath, ...ctx });
227
+ }
228
+ }
229
+ }
230
+ }
231
+ function processChild(childPath, ctx) {
232
+ if (childPath.isJSXElement() && !ctx.processedNodes.has(childPath.node)) {
233
+ processJsxElement({ path: childPath, ...ctx });
234
+ }
235
+ else if (childPath.isJSXFragment() &&
236
+ !ctx.processedNodes.has(childPath.node)) {
237
+ processJsxFragment({ path: childPath, ...ctx });
238
+ }
239
+ else if (childPath.isJSXExpressionContainer() &&
240
+ ctx.insideAutoT &&
241
+ needsVarWrapping(childPath.node)) {
242
+ // Wrap dynamic expression in <Var>
243
+ const varWrapper = createVarWrapper(childPath.node, ctx.varLocalName, ctx.processedNodes);
244
+ childPath.replaceWith(varWrapper);
245
+ ctx.processedNodes.add(varWrapper);
246
+ }
247
+ }
248
+ // ===== Opaque component props ===== //
249
+ function processOpaqueComponentProps({ path, insideAutoT, importAliases, processedNodes, tLocalName, varLocalName, canonicalName, }) {
250
+ // Branch/Plural children (fallback content) — process element-by-element before marking
251
+ if (insideAutoT &&
252
+ (canonicalName === BRANCH_COMPONENT || canonicalName === PLURAL_COMPONENT)) {
253
+ const childPaths = path.get('children');
254
+ for (const childPath of childPaths) {
255
+ processChild(childPath, {
256
+ insideAutoT: true,
257
+ importAliases,
258
+ processedNodes,
259
+ tLocalName,
260
+ varLocalName,
261
+ });
262
+ }
263
+ }
264
+ if (!insideAutoT) {
265
+ markDescendantJsx(path, processedNodes);
266
+ return;
267
+ }
268
+ const ctx = {
269
+ insideAutoT: true,
270
+ importAliases,
271
+ processedNodes,
272
+ tLocalName,
273
+ varLocalName,
274
+ };
275
+ // Wrap dynamic prop values in <Var>, skipping control props.
276
+ // This must happen BEFORE markDescendantJsx so that JSX inside auto-inserted
277
+ // _Var wrappers stays unmarked for the top-level visitor to process independently.
278
+ const attrs = path.get('openingElement').get('attributes');
279
+ for (const attrPath of attrs) {
280
+ if (!attrPath.isJSXAttribute())
281
+ continue;
282
+ // Determine prop name and skip control props
283
+ const nameNode = attrPath.node.name;
284
+ const propName = t.isJSXIdentifier(nameNode) ? nameNode.name : null;
285
+ if (isControlProp(canonicalName, propName))
286
+ continue;
287
+ const valuePath = attrPath.get('value');
288
+ if (!valuePath.isJSXExpressionContainer())
289
+ continue;
290
+ const expr = valuePath.node.expression;
291
+ // Content prop with JSX value — recurse into children for Var-wrapping
292
+ if (t.isJSXElement(expr) || t.isJSXFragment(expr)) {
293
+ const exprPath = valuePath.get('expression');
294
+ if (exprPath.isJSXElement() || exprPath.isJSXFragment()) {
295
+ const childPaths = exprPath.get('children');
296
+ for (const childPath of childPaths) {
297
+ processChild(childPath, ctx);
298
+ }
299
+ }
300
+ continue;
301
+ }
302
+ // Non-JSX dynamic value — wrap in Var
303
+ if (needsVarWrapping(valuePath.node)) {
304
+ const varWrapper = createVarWrapper(valuePath.node, varLocalName, processedNodes);
305
+ valuePath.replaceWith(varWrapper);
306
+ processedNodes.add(varWrapper);
307
+ }
308
+ }
309
+ // Mark remaining descendant JSX as processed, but skip auto-inserted nodes
310
+ // so the top-level visitor can still discover JSX inside _Var wrappers.
311
+ // For Derive/Static, only mark props — leave children unmarked so the
312
+ // top-level visitor can independently process them (e.g. Branch/Plural
313
+ // inside Derive should get their own _T, matching compiler behavior).
314
+ if (canonicalName === DERIVE_COMPONENT ||
315
+ canonicalName === STATIC_COMPONENT) {
316
+ for (const attrPath of path.get('openingElement').get('attributes')) {
317
+ attrPath.traverse({
318
+ JSXElement(childPath) {
319
+ processedNodes.add(childPath.node);
320
+ },
321
+ JSXFragment(childPath) {
322
+ processedNodes.add(childPath.node);
323
+ },
324
+ });
325
+ }
326
+ }
327
+ else {
328
+ markDescendantJsx(path, processedNodes);
329
+ }
330
+ }
331
+ function isControlProp(canonicalName, propName) {
332
+ if (!propName)
333
+ return false;
334
+ if (canonicalName === BRANCH_COMPONENT) {
335
+ return BRANCH_CONTROL_PROPS.has(propName) || propName.startsWith('data-');
336
+ }
337
+ if (canonicalName === PLURAL_COMPONENT) {
338
+ return PLURAL_CONTROL_PROPS.has(propName);
339
+ }
340
+ return false;
341
+ }
342
+ // ===== Helper functions ===== //
343
+ function getElementTypeName(element) {
344
+ const name = element.openingElement.name;
345
+ if (t.isJSXIdentifier(name))
346
+ return name.name;
347
+ return null;
348
+ }
349
+ function getLocalName(importAliases, canonicalName) {
350
+ const entry = Object.entries(importAliases).find(([, v]) => v === canonicalName);
351
+ return entry ? entry[0] : canonicalName;
352
+ }
353
+ function hasTranslatableText(children) {
354
+ return children.some((child) => t.isJSXText(child) && child.value.trim().length > 0);
355
+ }
356
+ function hasOpaqueGTChild(children, importAliases) {
357
+ return children.some((child) => {
358
+ if (!t.isJSXElement(child))
359
+ return false;
360
+ const typeName = getElementTypeName(child);
361
+ if (!typeName)
362
+ return false;
363
+ const canonical = importAliases[typeName];
364
+ return (canonical === BRANCH_COMPONENT ||
365
+ canonical === PLURAL_COMPONENT ||
366
+ canonical === DERIVE_COMPONENT ||
367
+ canonical === STATIC_COMPONENT);
368
+ });
369
+ }
370
+ function needsVarWrapping(container) {
371
+ const expr = container.expression;
372
+ if (t.isJSXEmptyExpression(expr))
373
+ return false;
374
+ // Use isStaticExpression to check — if static, no wrapping needed
375
+ const analysis = isStaticExpression(expr, true);
376
+ if (analysis.isStatic)
377
+ return false;
378
+ // JSX elements/fragments inside expressions are not dynamic — they're valid children
379
+ if (t.isJSXElement(expr) || t.isJSXFragment(expr))
380
+ return false;
381
+ return true;
382
+ }
383
+ // ===== AST construction ===== //
384
+ function createTWrapper(children, tName) {
385
+ const element = t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(tName), []), t.jsxClosingElement(t.jsxIdentifier(tName)), children);
386
+ autoInsertedNodes.add(element);
387
+ return element;
388
+ }
389
+ function createVarWrapper(child, varName, processedNodes) {
390
+ const varElement = t.jsxElement(t.jsxOpeningElement(t.jsxIdentifier(varName), []), t.jsxClosingElement(t.jsxIdentifier(varName)), [child]);
391
+ autoInsertedNodes.add(varElement);
392
+ processedNodes.add(varElement);
393
+ return t.jsxExpressionContainer(varElement);
394
+ }
395
+ function wrapChildrenInT(elementPath, tName, processedNodes) {
396
+ const children = [...elementPath.node.children];
397
+ const tWrapper = createTWrapper(children, tName);
398
+ processedNodes.add(tWrapper);
399
+ elementPath.node.children = [tWrapper];
400
+ }
401
+ function wrapFragmentChildrenInT(fragmentPath, tName, processedNodes) {
402
+ const children = [...fragmentPath.node.children];
403
+ const tWrapper = createTWrapper(children, tName);
404
+ processedNodes.add(tWrapper);
405
+ fragmentPath.node.children = [tWrapper];
406
+ }
407
+ // ===== Marking descendants as processed ===== //
408
+ function markDescendantJsx(path, processedNodes) {
409
+ path.traverse({
410
+ JSXElement(childPath) {
411
+ // Don't mark JSX inside auto-inserted _Var — the top-level visitor
412
+ // needs to find and process those independently
413
+ if (autoInsertedNodes.has(childPath.node)) {
414
+ childPath.skip();
415
+ return;
416
+ }
417
+ processedNodes.add(childPath.node);
418
+ },
419
+ JSXFragment(childPath) {
420
+ if (autoInsertedNodes.has(childPath.node)) {
421
+ childPath.skip();
422
+ return;
423
+ }
424
+ processedNodes.add(childPath.node);
425
+ },
426
+ });
427
+ }
@@ -11,6 +11,7 @@ type ConfigOptions = {
11
11
  pkgs: GTLibrary[];
12
12
  file: string;
13
13
  includeSourceCodeContext?: boolean;
14
+ enableAutoJsxInjection?: boolean;
14
15
  };
15
16
  /**
16
17
  * Collectors for errors, warnings, and unwrapped expressions.
@@ -6,10 +6,10 @@ import * as t from '@babel/types';
6
6
  import fs from 'node:fs';
7
7
  import { parse } from '@babel/parser';
8
8
  import addGTIdentifierToSyntaxTree from './addGTIdentifierToSyntaxTree.js';
9
- import { warnHasUnwrappedExpressionSync, warnNestedTComponent, warnFunctionNotFoundSync, warnMissingReturnSync, warnDuplicateFunctionDefinitionSync, warnInvalidDeriveInitSync, warnRecursiveFunctionCallSync, warnDataAttrOnBranch, } from '../../../../console/index.js';
9
+ import { warnHasUnwrappedExpressionSync, warnNestedTComponent, warnFunctionNotFoundSync, warnMissingReturnSync, warnDuplicateFunctionDefinitionSync, warnInvalidDeriveInitSync, warnRecursiveFunctionCallSync, warnDataAttrOnBranch, warnNestedInternalTComponent, } from '../../../../console/index.js';
10
10
  import { isAcceptedPluralForm } from 'generaltranslation/internal';
11
11
  import { isStaticExpression } from '../../evaluateJsx.js';
12
- import { DATA_ATTR_PREFIX, STATIC_COMPONENT, DERIVE_COMPONENT, TRANSLATION_COMPONENT, VARIABLE_COMPONENTS, } from '../constants.js';
12
+ import { DATA_ATTR_PREFIX, STATIC_COMPONENT, DERIVE_COMPONENT, TRANSLATION_COMPONENT, INTERNAL_TRANSLATION_COMPONENT, VARIABLE_COMPONENTS, } from '../constants.js';
13
13
  import { HTML_CONTENT_PROPS } from 'generaltranslation/types';
14
14
  import { resolveImportPath } from '../resolveImportPath.js';
15
15
  import traverseModule from '@babel/traverse';
@@ -20,6 +20,7 @@ import { handleChildrenWhitespace } from './handleChildrenWhitespace.js';
20
20
  import { isElementNode } from './types.js';
21
21
  import { multiplyJsxTree } from './multiplication/multiplyJsxTree.js';
22
22
  import { removeNullChildrenFields } from './removeNullChildrenFields.js';
23
+ import { ensureTAndVarImported, autoInsertJsxComponents, } from './autoInsertion.js';
23
24
  import path from 'node:path';
24
25
  import { extractSourceCode } from '../extractSourceCode.js';
25
26
  import { SURROUNDING_LINE_COUNT } from '../../../../utils/constants.js';
@@ -145,9 +146,53 @@ function buildJSXTree({ node, helperPath, scopeNode, insideT, inDerive, config,
145
146
  }
146
147
  // Convert from alias to original name
147
148
  const componentType = config.importAliases[typeName ?? ''];
148
- if (componentType === TRANSLATION_COMPONENT && insideT) {
149
+ // When enableAutoJsxInjection is on and we're inside a Derive context,
150
+ // any auto-inserted T component will be stripped at runtime by
151
+ // removeInjectedT. Unwrap it transparently — process the T's children
152
+ // as if the T wasn't there. Check this BEFORE the nested-T warning
153
+ // so we don't emit spurious errors for expected auto-inserted nesting.
154
+ if (componentType === INTERNAL_TRANSLATION_COMPONENT &&
155
+ inDerive &&
156
+ config.enableAutoJsxInjection) {
157
+ const childResults = [];
158
+ const helperChildren = helperPath.get('children');
159
+ for (let i = 0; i < element.children.length; i++) {
160
+ const child = element.children[i];
161
+ const helperChild = helperChildren[i];
162
+ const result = buildJSXTree({
163
+ node: child,
164
+ helperPath: helperChild,
165
+ scopeNode,
166
+ insideT: true,
167
+ inDerive: true,
168
+ config,
169
+ state,
170
+ output,
171
+ });
172
+ if (result !== null) {
173
+ if (Array.isArray(result)) {
174
+ childResults.push(...result);
175
+ }
176
+ else {
177
+ childResults.push(result);
178
+ }
179
+ }
180
+ }
181
+ if (childResults.length === 0)
182
+ return null;
183
+ if (childResults.length === 1)
184
+ return childResults[0];
185
+ // Return array — callers flatten this into parent's children
186
+ return childResults;
187
+ }
188
+ if ((componentType === TRANSLATION_COMPONENT ||
189
+ componentType === INTERNAL_TRANSLATION_COMPONENT) &&
190
+ insideT) {
149
191
  // Add warning: Nested <T> components are allowed, but they are advised against
150
192
  output.warnings.add(warnNestedTComponent(config.file, `${element.loc?.start?.line}:${element.loc?.start?.column}`));
193
+ if (componentType === INTERNAL_TRANSLATION_COMPONENT) {
194
+ output.errors.push(warnNestedInternalTComponent(config.file, `${element.loc?.start?.line}:${element.loc?.start?.column}`));
195
+ }
151
196
  }
152
197
  // If this JSXElement is one of the recognized variable components,
153
198
  const elementIsVariable = VARIABLE_COMPONENTS.includes(componentType);
@@ -240,7 +285,13 @@ function buildJSXTree({ node, helperPath, scopeNode, insideT, inDerive, config,
240
285
  state,
241
286
  output,
242
287
  });
243
- childrenArray.push(result);
288
+ // Flatten array results from _T transparency unwrap inside Derive
289
+ if (Array.isArray(result)) {
290
+ childrenArray.push(...result);
291
+ }
292
+ else {
293
+ childrenArray.push(result);
294
+ }
244
295
  }
245
296
  if (childrenArray.length) {
246
297
  results.props.children = childrenArray;
@@ -256,16 +307,22 @@ function buildJSXTree({ node, helperPath, scopeNode, insideT, inDerive, config,
256
307
  };
257
308
  }
258
309
  const children = element.children
259
- .map((child, index) => buildJSXTree({
260
- node: child,
261
- insideT: true,
262
- inDerive: inDerive,
263
- scopeNode,
264
- helperPath: helperPath.get('children')[index],
265
- config,
266
- state,
267
- output,
268
- }))
310
+ .flatMap((child, index) => {
311
+ const result = buildJSXTree({
312
+ node: child,
313
+ insideT: true,
314
+ inDerive: inDerive,
315
+ scopeNode,
316
+ helperPath: helperPath.get('children')[index],
317
+ config,
318
+ state,
319
+ output,
320
+ });
321
+ // Flatten array results from _T transparency unwrap inside Derive
322
+ if (Array.isArray(result))
323
+ return result;
324
+ return [result];
325
+ })
269
326
  .filter((child) => child !== null && child !== '');
270
327
  if (children.length === 1) {
271
328
  props.children = children[0];
@@ -284,16 +341,22 @@ function buildJSXTree({ node, helperPath, scopeNode, insideT, inDerive, config,
284
341
  // If it's a JSX fragment
285
342
  else if (t.isJSXFragment(node)) {
286
343
  const children = node.children
287
- .map((child, index) => buildJSXTree({
288
- node: child,
289
- insideT: true,
290
- inDerive: inDerive,
291
- scopeNode,
292
- helperPath: helperPath.get('children')[index],
293
- config,
294
- state,
295
- output,
296
- }))
344
+ .flatMap((child, index) => {
345
+ const result = buildJSXTree({
346
+ node: child,
347
+ insideT: true,
348
+ inDerive: inDerive,
349
+ scopeNode,
350
+ helperPath: helperPath.get('children')[index],
351
+ config,
352
+ state,
353
+ output,
354
+ });
355
+ // Flatten array results from _T transparency unwrap inside Derive
356
+ if (Array.isArray(result))
357
+ return result;
358
+ return [result];
359
+ })
297
360
  .filter((child) => child !== null && child !== '');
298
361
  const props = {};
299
362
  if (children.length === 1) {
@@ -405,7 +468,9 @@ function parseJSXElement({ node, originalName, scopeNode, updates, config, state
405
468
  const name = openingElement.name;
406
469
  // Only proceed if it's <T> ...
407
470
  // TODO: i don't think this condition is needed anymore
408
- if (!(name.type === 'JSXIdentifier' && originalName === TRANSLATION_COMPONENT)) {
471
+ if (!(name.type === 'JSXIdentifier' &&
472
+ (originalName === TRANSLATION_COMPONENT ||
473
+ originalName === INTERNAL_TRANSLATION_COMPONENT))) {
409
474
  return;
410
475
  }
411
476
  const componentErrors = [];
@@ -477,7 +542,15 @@ function parseJSXElement({ node, originalName, scopeNode, updates, config, state
477
542
  // TODO: do this in parallel
478
543
  const minifiedTress = [];
479
544
  for (const multipliedTree of multipliedTrees) {
480
- const minifiedTree = addGTIdentifierToSyntaxTree(multipliedTree);
545
+ // Build set of confirmed GT variable names from importAliases.
546
+ // Only pass when enableAutoJsxInjection is on, to avoid breaking
547
+ // existing behavior where importAliases may be incomplete.
548
+ const gtVariableNames = config.enableAutoJsxInjection
549
+ ? new Set(Object.values(config.importAliases).filter((name) => VARIABLE_COMPONENTS.includes(name) &&
550
+ name !== DERIVE_COMPONENT &&
551
+ name !== STATIC_COMPONENT))
552
+ : undefined;
553
+ const minifiedTree = addGTIdentifierToSyntaxTree(multipliedTree, 0, gtVariableNames);
481
554
  minifiedTress.push(Array.isArray(minifiedTree) && minifiedTree.length === 1
482
555
  ? minifiedTree[0]
483
556
  : minifiedTree);
@@ -620,7 +693,19 @@ function processFunctionInFile({ config, state, output, filePath, functionName,
620
693
  sourceType: 'module',
621
694
  plugins: ['jsx', 'typescript'],
622
695
  });
623
- const { importAliases } = getPathsAndAliases(ast, config.pkgs);
696
+ const pathsResult = getPathsAndAliases(ast, config.pkgs);
697
+ const importAliases = { ...pathsResult.importAliases };
698
+ // Merge translation component names into importAliases so
699
+ // autoInsertJsxComponents can recognize user T/Var and skip them
700
+ for (const { localName, originalName, } of pathsResult.translationComponentPaths) {
701
+ importAliases[localName] = originalName;
702
+ }
703
+ // Auto-inject T/Var into the cross-file AST when enabled,
704
+ // so that Derive extraction sees the same structure as same-file
705
+ if (config.enableAutoJsxInjection) {
706
+ ensureTAndVarImported(ast, importAliases);
707
+ autoInsertJsxComponents(ast, importAliases);
708
+ }
624
709
  // Collect all imports in this file to track cross-file function calls
625
710
  let importedFunctionsMap = new Map();
626
711
  traverse(ast, {
@@ -644,6 +729,7 @@ function processFunctionInFile({ config, state, output, filePath, functionName,
644
729
  parsingOptions: config.parsingOptions,
645
730
  pkgs: config.pkgs,
646
731
  file: filePath,
732
+ enableAutoJsxInjection: config.enableAutoJsxInjection,
647
733
  },
648
734
  state: {
649
735
  ...state,
@@ -669,6 +755,7 @@ function processFunctionInFile({ config, state, output, filePath, functionName,
669
755
  parsingOptions: config.parsingOptions,
670
756
  pkgs: config.pkgs,
671
757
  file: filePath,
758
+ enableAutoJsxInjection: config.enableAutoJsxInjection,
672
759
  },
673
760
  state: {
674
761
  ...state,
@@ -716,6 +803,7 @@ function processFunctionInFile({ config, state, output, filePath, functionName,
716
803
  parsingOptions: config.parsingOptions,
717
804
  pkgs: config.pkgs,
718
805
  file: filePath,
806
+ enableAutoJsxInjection: config.enableAutoJsxInjection,
719
807
  },
720
808
  state: {
721
809
  ...state,
@@ -760,13 +848,19 @@ function processFunctionDeclarationNodePath({ config, state, output, path, }) {
760
848
  if (!returnNodePath.isExpression()) {
761
849
  return;
762
850
  }
763
- result.branches.push(processDeriveExpression({
851
+ const deriveResult = processDeriveExpression({
764
852
  config,
765
853
  state,
766
854
  output,
767
855
  expressionNodePath: returnNodePath,
768
856
  scopeNode: returnPath,
769
- }));
857
+ });
858
+ if (Array.isArray(deriveResult)) {
859
+ result.branches.push(...deriveResult);
860
+ }
861
+ else {
862
+ result.branches.push(deriveResult);
863
+ }
770
864
  },
771
865
  });
772
866
  if (result.branches.length === 0) {
@@ -794,13 +888,19 @@ function processVariableDeclarationNodePath({ config, state, output, functionNam
794
888
  const bodyNodePath = arrowFunctionPath.get('body');
795
889
  if (bodyNodePath.isExpression()) {
796
890
  // process expression return
797
- result.branches.push(processDeriveExpression({
891
+ const deriveResult = processDeriveExpression({
798
892
  config,
799
893
  state,
800
894
  output,
801
895
  expressionNodePath: bodyNodePath,
802
896
  scopeNode: arrowFunctionPath,
803
- }));
897
+ });
898
+ if (Array.isArray(deriveResult)) {
899
+ result.branches.push(...deriveResult);
900
+ }
901
+ else {
902
+ result.branches.push(deriveResult);
903
+ }
804
904
  }
805
905
  else {
806
906
  // search for a return statement
@@ -813,13 +913,19 @@ function processVariableDeclarationNodePath({ config, state, output, functionNam
813
913
  if (!returnNodePath.isExpression()) {
814
914
  return;
815
915
  }
816
- result.branches.push(processDeriveExpression({
916
+ const deriveResult = processDeriveExpression({
817
917
  config,
818
918
  state,
819
919
  output,
820
920
  expressionNodePath: returnNodePath,
821
921
  scopeNode: returnPath,
822
- }));
922
+ });
923
+ if (Array.isArray(deriveResult)) {
924
+ result.branches.push(...deriveResult);
925
+ }
926
+ else {
927
+ result.branches.push(deriveResult);
928
+ }
823
929
  },
824
930
  });
825
931
  }
@@ -904,13 +1010,16 @@ function processDeriveExpression({ config, state, output, expressionNodePath, sc
904
1010
  const alternateNodePath = expressionNodePath.get('alternate');
905
1011
  const result = {
906
1012
  nodeType: 'multiplication',
907
- branches: [consequentNodePath, alternateNodePath].map((expressionNodePath) => processDeriveExpression({
908
- config,
909
- state,
910
- output,
911
- scopeNode,
912
- expressionNodePath,
913
- })),
1013
+ branches: [consequentNodePath, alternateNodePath].flatMap((expressionNodePath) => {
1014
+ const r = processDeriveExpression({
1015
+ config,
1016
+ state,
1017
+ output,
1018
+ scopeNode,
1019
+ expressionNodePath,
1020
+ });
1021
+ return Array.isArray(r) ? r : [r];
1022
+ }),
914
1023
  };
915
1024
  return result;
916
1025
  }
@@ -8,6 +8,10 @@ import { DEFAULT_SRC_PATTERNS } from '../../config/generateSettings.js';
8
8
  import { getPathsAndAliases } from '../jsx/utils/getPathsAndAliases.js';
9
9
  import { GT_LIBRARIES_UPSTREAM, REACT_LIBRARIES, } from '../../types/libraries.js';
10
10
  import { calculateHashes, dedupeUpdates, linkDeriveUpdates, } from '../../extraction/postProcess.js';
11
+ import { ensureTAndVarImported, autoInsertJsxComponents, } from '../jsx/utils/jsxParsing/autoInsertion.js';
12
+ import { INTERNAL_TRANSLATION_COMPONENT } from '../jsx/utils/constants.js';
13
+ import traverseModule from '@babel/traverse';
14
+ const traverse = traverseModule.default || traverseModule;
11
15
  export async function createInlineUpdates(pkg, validate, filePatterns, parsingFlags, parsingOptions) {
12
16
  const updates = [];
13
17
  const errors = [];
@@ -46,7 +50,7 @@ export async function createInlineUpdates(pkg, validate, filePatterns, parsingFl
46
50
  autoDeriveMethod: parsingFlags.autoDerive ? 'AUTO' : 'DISABLED',
47
51
  }, { updates, errors, warnings });
48
52
  }
49
- // Parse <T> components
53
+ // Parse <T> components — PASS 1: user-written T
50
54
  if (REACT_LIBRARIES.includes(pkg)) {
51
55
  for (const { localName, path } of translationComponentPaths) {
52
56
  parseTranslationComponent({
@@ -68,6 +72,55 @@ export async function createInlineUpdates(pkg, validate, filePatterns, parsingFl
68
72
  },
69
73
  });
70
74
  }
75
+ // PASS 2: Auto-inject GtInternalTranslateJsx and GtInternalVar and extract (flag-gated)
76
+ if (parsingFlags.enableAutoJsxInjection) {
77
+ // Add translation component names to importAliases so autoInsertJsxComponents
78
+ // recognizes user T as hands-off (getPathsAndAliases separates them out)
79
+ for (const { localName, originalName } of translationComponentPaths) {
80
+ importAliases[localName] = originalName;
81
+ }
82
+ // Ensure GtInternalTranslateJsx and GtInternalVar are imported in the AST
83
+ ensureTAndVarImported(ast, importAliases);
84
+ // Insert T/Var into the AST
85
+ autoInsertJsxComponents(ast, importAliases);
86
+ // Refresh scope to pick up new T references
87
+ traverse(ast, {
88
+ Program(programPath) {
89
+ programPath.scope.crawl();
90
+ },
91
+ });
92
+ // Re-collect with updated AST
93
+ const refreshed = getPathsAndAliases(ast, pkgs);
94
+ // Add translation component names to refreshed aliases so parseJsx
95
+ // can recognize GtInternalTranslateJsx inside Derive for transparent unwrap
96
+ for (const { localName: tLocalName, originalName: tOrigName, } of refreshed.translationComponentPaths) {
97
+ refreshed.importAliases[tLocalName] = tOrigName;
98
+ }
99
+ // Extract only from auto-injected GtInternalTranslateJsx — never re-extract user T
100
+ for (const { localName, path, originalName, } of refreshed.translationComponentPaths) {
101
+ if (originalName !== INTERNAL_TRANSLATION_COMPONENT)
102
+ continue;
103
+ parseTranslationComponent({
104
+ originalName: localName,
105
+ localName,
106
+ path,
107
+ updates,
108
+ config: {
109
+ importAliases: refreshed.importAliases,
110
+ parsingOptions,
111
+ pkgs,
112
+ file,
113
+ includeSourceCodeContext: parsingFlags.includeSourceCodeContext,
114
+ enableAutoJsxInjection: true,
115
+ },
116
+ output: {
117
+ errors,
118
+ warnings,
119
+ unwrappedExpressions: [],
120
+ },
121
+ });
122
+ }
123
+ }
71
124
  }
72
125
  }
73
126
  // Post processing steps:
@@ -4,8 +4,10 @@ import { VariableType } from 'generaltranslation/types';
4
4
  */
5
5
  export declare const defaultVariableNames: {
6
6
  readonly Var: "value";
7
+ readonly GtInternalVar: "value";
7
8
  readonly Num: "n";
8
9
  readonly DateTime: "date";
10
+ readonly RelativeTime: "time";
9
11
  readonly Currency: "cost";
10
12
  };
11
13
  /**
@@ -3,14 +3,18 @@
3
3
  */
4
4
  export const defaultVariableNames = {
5
5
  Var: 'value',
6
+ GtInternalVar: 'value',
6
7
  Num: 'n',
7
8
  DateTime: 'date',
9
+ RelativeTime: 'time',
8
10
  Currency: 'cost',
9
11
  };
10
12
  const minifyVariableTypeMap = {
11
13
  Var: 'v',
14
+ GtInternalVar: 'v',
12
15
  Num: 'n',
13
16
  DateTime: 'd',
17
+ RelativeTime: 'rt',
14
18
  Currency: 'c',
15
19
  };
16
20
  /**
@@ -32,10 +32,12 @@ export type BaseParsingFlags = Record<string, unknown>;
32
32
  *
33
33
  * @property {boolean} autoDerive - Whether to enable auto-derive for the t() function. (true -> 'AUTO', false -> 'DISABLED' {@link ParsingConfig['autoDeriveMethod']})
34
34
  * @property {boolean} includeSourceCodeContext - Include surrounding source code lines as context for translations.
35
+ * @property {boolean} enableAutoJsxInjection - Whether to enable auto-jsx injection for the internal <_T> and <_Var> components.
35
36
  */
36
37
  export type GTParsingFlags = BaseParsingFlags & {
37
38
  autoDerive: boolean;
38
39
  includeSourceCodeContext: boolean;
40
+ enableAutoJsxInjection: boolean;
39
41
  };
40
42
  /**
41
43
  * Flags for parsing content with each filetype having its own flags
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gt",
3
- "version": "2.14.3",
3
+ "version": "2.14.4",
4
4
  "main": "dist/index.js",
5
5
  "bin": "dist/main.js",
6
6
  "files": [
@@ -110,8 +110,8 @@
110
110
  "unified": "^11.0.5",
111
111
  "unist-util-visit": "^5.0.0",
112
112
  "yaml": "^2.8.0",
113
- "@generaltranslation/python-extractor": "0.2.5",
114
- "generaltranslation": "8.2.1",
113
+ "@generaltranslation/python-extractor": "0.2.6",
114
+ "generaltranslation": "8.2.2",
115
115
  "gt-remark": "1.0.7"
116
116
  },
117
117
  "devDependencies": {