@@ -604,4 +604,249 @@ Line 2
604604 expect ( pushed ) . toContain ( "- %d user\\'s item" ) ;
605605 expect ( pushed ) . not . toContain ( "- %d user\\\\'s item" ) ;
606606 } ) ;
607+
608+ // Tests for Issue Fixes
609+
610+ it ( "should preserve whitespace in array items during pull and push" , async ( ) => {
611+ const input = `
612+ <resources>
613+ <string-array name="mixed_items">
614+ <item> Item with spaces </item>
615+ <item> </item>
616+ </string-array>
617+ </resources>
618+ ` . trim ( ) ;
619+
620+ const androidLoader = createAndroidLoader ( ) . setDefaultLocale ( "en" ) ;
621+ const pulled = await androidLoader . pull ( "en" , input ) ;
622+
623+ expect ( pulled . mixed_items ) . toEqual ( [ " Item with spaces " , " " ] ) ;
624+
625+ const pushed = await androidLoader . push ( "en" , {
626+ mixed_items : [ " Elemento con espacios " , " " ] ,
627+ } ) ;
628+
629+ expect ( pushed ) . toContain ( "<item> Elemento con espacios </item>" ) ;
630+ expect ( pushed ) . toContain ( "<item> </item>" ) ;
631+ } ) ;
632+
633+ it ( "should retain CDATA wrappers for translated strings" , async ( ) => {
634+ const input = `
635+ <resources>
636+ <string name="cdata_example"><![CDATA[Special <tag> ]]></string>
637+ </resources>
638+ ` . trim ( ) ;
639+
640+ const androidLoader = createAndroidLoader ( ) . setDefaultLocale ( "en" ) ;
641+ await androidLoader . pull ( "en" , input ) ;
642+
643+ const pushed = await androidLoader . push ( "es" , {
644+ cdata_example : "Especial <tag> " ,
645+ } ) ;
646+
647+ expect ( pushed ) . toContain (
648+ '<string name="cdata_example"><![CDATA[Especial <tag> ]]></string>' ,
649+ ) ;
650+ } ) ;
651+
652+ it ( "should preserve resource ordering after push" , async ( ) => {
653+ const input = `
654+ <resources>
655+ <string name="first">First</string>
656+ <string-array name="colors">
657+ <item>Red</item>
658+ <item>Green</item>
659+ </string-array>
660+ <plurals name="messages">
661+ <item quantity="one">%d message</item>
662+ <item quantity="other">%d messages</item>
663+ </plurals>
664+ <bool name="show_tutorial">true</bool>
665+ <integer name="retry_count">3</integer>
666+ </resources>
667+ ` . trim ( ) ;
668+
669+ const androidLoader = createAndroidLoader ( ) . setDefaultLocale ( "en" ) ;
670+ const roundTrip = await androidLoader . pull ( "en" , input ) ;
671+ const pushed = await androidLoader . push ( "en" , roundTrip ) ;
672+
673+ const order = Array . from (
674+ pushed . matchAll (
675+ / < ( s t r i n g | s t r i n g - a r r a y | p l u r a l s | b o o l | i n t e g e r ) \s + n a m e = " ( [ ^ " ] + ) " / g,
676+ ) ,
677+ ) . map ( ( [ , , name ] ) => name ) ;
678+
679+ expect ( order ) . toEqual ( [
680+ "first" ,
681+ "colors" ,
682+ "messages" ,
683+ "show_tutorial" ,
684+ "retry_count" ,
685+ ] ) ;
686+ } ) ;
687+
688+ it ( "should preserve XML declaration from source file" , async ( ) => {
689+ const input = `<?xml version="1.0" encoding="utf-8"?>
690+ <resources>
691+ <string name="test">Test</string>
692+ </resources>` ;
693+
694+ const androidLoader = createAndroidLoader ( ) . setDefaultLocale ( "en" ) ;
695+ await androidLoader . pull ( "en" , input ) ;
696+
697+ const result = await androidLoader . push ( "es" , { test : "Prueba" } ) ;
698+
699+ expect ( result ) . toMatch ( / ^ < \? x m l v e r s i o n = " 1 \. 0 " e n c o d i n g = " u t f - 8 " \? > / ) ;
700+ } ) ;
701+
702+ it ( 'should preserve translatable="false" items in target locale' , async ( ) => {
703+ const input = `
704+ <resources>
705+ <string name="app_name">My App</string>
706+ <string name="api_url" translatable="false">https://api.example.com</string>
707+ <string name="debug_key" translatable="false">DEBUG_KEY</string>
708+ <string-array name="colors">
709+ <item>Red</item>
710+ </string-array>
711+ <string-array name="urls" translatable="false">
712+ <item>https://example.com</item>
713+ </string-array>
714+ <plurals name="items">
715+ <item quantity="one">%d item</item>
716+ <item quantity="other">%d items</item>
717+ </plurals>
718+ <plurals name="bytes" translatable="false">
719+ <item quantity="one">%d byte</item>
720+ <item quantity="other">%d bytes</item>
721+ </plurals>
722+ <bool name="show_tutorial">true</bool>
723+ <bool name="is_debug" translatable="false">false</bool>
724+ <integer name="timeout">30</integer>
725+ <integer name="version" translatable="false">42</integer>
726+ </resources>
727+ ` . trim ( ) ;
728+
729+ const androidLoader = createAndroidLoader ( ) . setDefaultLocale ( "en" ) ;
730+ await androidLoader . pull ( "en" , input ) ;
731+
732+ const result = await androidLoader . push ( "es" , {
733+ app_name : "Mi Aplicación" ,
734+ colors : [ "Rojo" ] ,
735+ items : { one : "%d elemento" , other : "%d elementos" } ,
736+ show_tutorial : true ,
737+ timeout : 30 ,
738+ } ) ;
739+
740+ // Check that translatable="false" items are included
741+ expect ( result ) . toContain ( 'name="api_url"' ) ;
742+ expect ( result ) . toContain ( "https://api.example.com" ) ;
743+ expect ( result ) . toContain ( 'translatable="false"' ) ;
744+ expect ( result ) . toContain ( 'name="debug_key"' ) ;
745+ expect ( result ) . toContain ( "DEBUG_KEY" ) ;
746+ expect ( result ) . toContain ( 'name="urls"' ) ;
747+ expect ( result ) . toContain ( "https://example.com" ) ;
748+ expect ( result ) . toContain ( 'name="bytes"' ) ;
749+ expect ( result ) . toContain ( 'name="is_debug"' ) ;
750+ expect ( result ) . toContain ( 'name="version"' ) ;
751+ expect ( result ) . toContain ( ">42<" ) ;
752+
753+ // Check that translatable items are translated
754+ expect ( result ) . toContain ( "Mi Aplicación" ) ;
755+ expect ( result ) . toContain ( "Rojo" ) ;
756+ expect ( result ) . toContain ( "elemento" ) ;
757+ } ) ;
758+
759+ it ( "should use 4-space indentation by default" , async ( ) => {
760+ const input = `<?xml version="1.0" encoding="utf-8"?>
761+ <resources>
762+ <string name="test">Test</string>
763+ <string name="another">Another</string>
764+ </resources>` ;
765+
766+ const androidLoader = createAndroidLoader ( ) . setDefaultLocale ( "en" ) ;
767+ await androidLoader . pull ( "en" , input ) ;
768+
769+ const result = await androidLoader . push ( "es" , {
770+ test : "Prueba" ,
771+ another : "Otro" ,
772+ } ) ;
773+
774+ // Check for 4-space indentation (default)
775+ // Note: Users should use formatters (Prettier/Biome) for custom indentation
776+ expect ( result ) . toContain ( '\n <string name="test">' ) ;
777+ expect ( result ) . toContain ( '\n <string name="another">' ) ;
778+ } ) ;
779+
780+ it ( "should preserve XML declaration encoding from source file" , async ( ) => {
781+ const inputUtf8 = `<?xml version="1.0" encoding="utf-8"?>
782+ <resources>
783+ <string name="test">Test</string>
784+ </resources>` ;
785+
786+ const inputUpperUTF8 = `<?xml version="1.0" encoding="UTF-8"?>
787+ <resources>
788+ <string name="test">Test</string>
789+ </resources>` ;
790+
791+ const inputISO = `<?xml version="1.0" encoding="ISO-8859-1"?>
792+ <resources>
793+ <string name="test">Test</string>
794+ </resources>` ;
795+
796+ const androidLoader = createAndroidLoader ( ) . setDefaultLocale ( "en" ) ;
797+
798+ // Test lowercase utf-8
799+ await androidLoader . pull ( "en" , inputUtf8 ) ;
800+ let result = await androidLoader . push ( "es" , { test : "Prueba" } ) ;
801+ expect ( result ) . toMatch ( / ^ < \? x m l v e r s i o n = " 1 \. 0 " e n c o d i n g = " u t f - 8 " \? > / ) ;
802+
803+ // Test uppercase UTF-8
804+ await androidLoader . pull ( "en" , inputUpperUTF8 ) ;
805+ result = await androidLoader . push ( "es" , { test : "Prueba" } ) ;
806+ expect ( result ) . toMatch ( / ^ < \? x m l v e r s i o n = " 1 \. 0 " e n c o d i n g = " U T F - 8 " \? > / ) ;
807+
808+ // Test ISO-8859-1
809+ await androidLoader . pull ( "en" , inputISO ) ;
810+ result = await androidLoader . push ( "es" , { test : "Prueba" } ) ;
811+ expect ( result ) . toMatch ( / ^ < \? x m l v e r s i o n = " 1 \. 0 " e n c o d i n g = " I S O - 8 8 5 9 - 1 " \? > / ) ;
812+ } ) ;
813+
814+ it ( "should preserve XML version from source file" , async ( ) => {
815+ const inputV10 = `<?xml version="1.0" encoding="utf-8"?>
816+ <resources>
817+ <string name="test">Test</string>
818+ </resources>` ;
819+
820+ const inputV11 = `<?xml version="1.1" encoding="utf-8"?>
821+ <resources>
822+ <string name="test">Test</string>
823+ </resources>` ;
824+
825+ const androidLoader = createAndroidLoader ( ) . setDefaultLocale ( "en" ) ;
826+
827+ // Test version 1.0
828+ await androidLoader . pull ( "en" , inputV10 ) ;
829+ let result = await androidLoader . push ( "es" , { test : "Prueba" } ) ;
830+ expect ( result ) . toMatch ( / ^ < \? x m l v e r s i o n = " 1 \. 0 " / ) ;
831+
832+ // Test version 1.1
833+ await androidLoader . pull ( "en" , inputV11 ) ;
834+ result = await androidLoader . push ( "es" , { test : "Prueba" } ) ;
835+ expect ( result ) . toMatch ( / ^ < \? x m l v e r s i o n = " 1 \. 1 " / ) ;
836+ } ) ;
837+
838+ it ( "should omit XML declaration when source has none" , async ( ) => {
839+ const inputNoDeclaration = `<resources>
840+ <string name="test">Test</string>
841+ </resources>` ;
842+
843+ const androidLoader = createAndroidLoader ( ) . setDefaultLocale ( "en" ) ;
844+ await androidLoader . pull ( "en" , inputNoDeclaration ) ;
845+
846+ const result = await androidLoader . push ( "es" , { test : "Prueba" } ) ;
847+
848+ // Should start immediately with the root element (no declaration)
849+ expect ( result ) . not . toMatch ( / ^ < \? x m l / ) ;
850+ expect ( result . trim ( ) . startsWith ( "<resources>" ) ) . toBe ( true ) ;
851+ } ) ;
607852} ) ;
0 commit comments