@@ -161,11 +161,7 @@ function translateAttributes(
161161 node . loc ?. start . line ,
162162 node . loc ?. start . column ,
163163 ) ;
164- state . newEntries . push ( entry ) ;
165-
166- const hashes = state . componentHashes . get ( component . name ) || [ ] ;
167- hashes . push ( entry . hash ) ;
168- state . componentHashes . set ( component . name , hashes ) ;
164+ registerEntry ( entry , state , component . name ) ;
169165
170166 attr . value = constructTranslationCall ( entry . hash , text ) ;
171167 }
@@ -263,7 +259,6 @@ interface SerializedJSX {
263259 components : Map < string , t . JSXElement > ;
264260}
265261
266- // TODO (AleksandrSl 28/11/2025): Check whitespace logic, the rest seems reasonable
267262/**
268263 * Serialize JSX children to a translation string with placeholders
269264 */
@@ -397,87 +392,117 @@ function transformVoidElement(
397392 translateAttributes ( node , state ) ;
398393}
399394
395+ type TranslationScope =
396+ | { kind : "mixed" ; text : string ; args : any }
397+ | { kind : "text" ; text : string ; textNodeIndex : number } ;
398+
399+ function getTranslationScope (
400+ node : t . JSXElement | t . JSXFragment ,
401+ state : VisitorsInternalState ,
402+ ) : TranslationScope | null {
403+ if ( hasMixedContent ( node ) ) {
404+ const serialized = serializeJSXChildren ( node . children , state ) ;
405+ const text = serialized . text . trim ( ) ;
406+ if ( text . length === 0 ) return null ;
407+
408+ return {
409+ kind : "mixed" ,
410+ text,
411+ args : {
412+ variables : serialized . variables ,
413+ expressions : serialized . expressions ,
414+ components : serialized . components ,
415+ } ,
416+ } ;
417+ }
418+
419+ // Non-mixed: allow exactly one meaningful JSXText + optional void elements + whitespace
420+ const textNodeIndex = node . children . findIndex (
421+ ( child ) => child . type === "JSXText" && child . value . trim ( ) . length > 0 ,
422+ ) ;
423+ if ( textNodeIndex === - 1 ) return null ;
424+
425+ const allowed = node . children . every (
426+ ( child ) =>
427+ child . type === "JSXText" ||
428+ ( child . type === "JSXElement" && isVoidElement ( child ) ) ,
429+ ) ;
430+ if ( ! allowed ) return null ;
431+
432+ const textNode = node . children [ textNodeIndex ] as t . JSXText ;
433+ const text = normalizeWhitespace ( textNode . value ) ;
434+ if ( text . length === 0 ) return null ;
435+
436+ return { kind : "text" , text, textNodeIndex } ;
437+ }
438+
439+ function registerEntry (
440+ entry : ReturnType < typeof createTranslationEntry > ,
441+ state : VisitorsInternalState ,
442+ componentName : string ,
443+ ) {
444+ state . newEntries . push ( entry ) ;
445+
446+ const hashes = state . componentHashes . get ( componentName ) ?? [ ] ;
447+ hashes . push ( entry . hash ) ;
448+ state . componentHashes . set ( componentName , hashes ) ;
449+ }
450+
451+ function rewriteChildren (
452+ path : NodePath < t . JSXElement | t . JSXFragment > ,
453+ state : VisitorsInternalState ,
454+ translationScope : TranslationScope ,
455+ entryHash : string ,
456+ ) {
457+ if ( translationScope . kind === "mixed" ) {
458+ path . node . children = [
459+ constructTranslationCall (
460+ entryHash ,
461+ translationScope . text ,
462+ translationScope . args ,
463+ ) ,
464+ ] ;
465+ return ;
466+ }
467+
468+ const { textNodeIndex, text } = translationScope ;
469+
470+ path . node . children = path . node . children . map ( ( child , index ) => {
471+ if ( index === textNodeIndex ) {
472+ return constructTranslationCall ( entryHash , text ) ;
473+ }
474+ if ( child . type === "JSXElement" ) {
475+ transformVoidElement ( child , state ) ;
476+ }
477+ return child ;
478+ } ) ;
479+ }
480+
400481function processJSXElement (
401482 path : NodePath < t . JSXElement | t . JSXFragment > ,
402483 state : VisitorsInternalState ,
403484) : void {
404485 const component = state . componentsStack . at ( - 1 ) ;
405486 if ( ! component ) return ;
406487
407- let type = undefined ;
408- let textNode ;
409- let textNodeIndex : number | undefined ;
410- if ( hasMixedContent ( path . node ) ) {
411- type = "mixed" ;
412- } else {
413- // If there were several text elements we will be in the mixed content above
414- textNodeIndex = path . node . children . findIndex (
415- ( child ) => child . type === "JSXText" && child . value . trim ( ) . length > 0 ,
416- ) ;
417- if (
418- textNodeIndex !== - 1 &&
419- path . node . children . every (
420- ( child ) =>
421- child . type === "JSXText" ||
422- ( child . type === "JSXElement" && isVoidElement ( child ) ) ,
423- )
424- ) {
425- type = "text" ;
426- textNode = path . node . children [ textNodeIndex ] as t . JSXText ;
427- }
428- }
429-
430- if ( ! type ) {
431- return ;
432- }
488+ const scope = getTranslationScope ( path . node , state ) ;
489+ if ( ! scope ) return ;
433490
434491 const overrides = processOverrideAttributes ( path ) ;
435492
436- let text ;
437- let args ;
438- if ( type === "mixed" ) {
439- const serialized = serializeJSXChildren ( path . node . children , state ) ;
440- text = serialized . text . trim ( ) ;
441- args = {
442- variables : serialized . variables ,
443- expressions : serialized . expressions ,
444- components : serialized . components ,
445- } ;
446- } else {
447- text = normalizeWhitespace ( textNode ! . value ) ;
448- }
449-
450- if ( text . length == 0 ) return ;
451-
452493 const entry = createTranslationEntry (
453494 "content" ,
454- text ,
495+ scope . text ,
455496 { componentName : component . name } ,
456497 state . filePath ,
457498 path . node . loc ?. start . line ,
458499 path . node . loc ?. start . column ,
459500 overrides ,
460501 ) ;
461- state . newEntries . push ( entry ) ;
462502
463- const hashes = state . componentHashes . get ( component . name ) || [ ] ;
464- hashes . push ( entry . hash ) ;
465- state . componentHashes . set ( component . name , hashes ) ;
503+ registerEntry ( entry , state , component . name ) ;
504+ rewriteChildren ( path , state , scope , entry . hash ) ;
466505
467- if ( type === "mixed" ) {
468- path . node . children = [ constructTranslationCall ( entry . hash , text , args ) ] ;
469- } else {
470- path . node . children = path . node . children . map ( ( it , index ) => {
471- if ( index === textNodeIndex ) {
472- return constructTranslationCall ( entry . hash , text ) ;
473- } else if ( it . type === "JSXElement" ) {
474- transformVoidElement ( it , state ) ;
475- return it ;
476- } else {
477- return it ;
478- }
479- } ) ;
480- }
481506 path . skip ( ) ;
482507}
483508
0 commit comments