@@ -444,6 +444,65 @@ describe("bucket loaders", () => {
444444 expect ( fs . writeFile ) . toHaveBeenCalledWith ( "i18n/es.json" , expectedOutput , { encoding : "utf-8" , flag : "w" } ) ;
445445 } ) ;
446446
447+ it ( "should respect locked keys during cache restoration" , async ( ) => {
448+ setupFileMocks ( ) ;
449+
450+ const input = {
451+ "button.title" : "Submit" ,
452+ "button.description" : "Extra field not in payload" ,
453+ "locked.key" : "Should not change" ,
454+ nested : {
455+ locked : "This is locked" ,
456+ unlocked : "This can change" ,
457+ extra : "This should be removed in cache restore" ,
458+ } ,
459+ } ;
460+ const payload = {
461+ "button.title" : "Enviar" ,
462+ "locked.key" : "This should not be applied" ,
463+ "nested/locked" : "This should not be applied either" ,
464+ "nested/unlocked" : "Este puede cambiar" ,
465+ } ;
466+
467+ mockFileOperations ( JSON . stringify ( input ) ) ;
468+
469+ const jsonLoader = createBucketLoader (
470+ "json" ,
471+ "i18n/[locale].json" ,
472+ { isCacheRestore : true , defaultLocale : "en" } ,
473+ [ "locked.key" , "nested/locked" ] ,
474+ ) ;
475+
476+ jsonLoader . setDefaultLocale ( "en" ) ;
477+ await jsonLoader . pull ( "en" ) ;
478+
479+ await jsonLoader . push ( "es" , payload ) ;
480+
481+ expect ( fs . writeFile ) . toHaveBeenCalled ( ) ;
482+ const writeFileCall = ( fs . writeFile as any ) . mock . calls [ 0 ] ;
483+ const writtenContent = JSON . parse ( writeFileCall [ 1 ] ) ;
484+
485+ // During cache restoration, only keys in the payload should be included
486+ // but locked keys should still be preserved
487+ expect ( Object . keys ( writtenContent ) ) . toContain ( "button.title" ) ;
488+ expect ( Object . keys ( writtenContent ) ) . toContain ( "locked.key" ) ;
489+ expect ( writtenContent [ "locked.key" ] ) . toBe ( "Should not change" ) ;
490+
491+ // Fields not in the payload should be removed in cache restoration
492+ expect ( Object . keys ( writtenContent ) ) . not . toContain ( "button.description" ) ;
493+
494+ // Nested keys should follow the same pattern
495+ expect ( writtenContent . nested ) . toHaveProperty ( "unlocked" , "Este puede cambiar" ) ;
496+ expect ( writtenContent . nested ) . toHaveProperty ( "locked" , "This is locked" ) ;
497+ expect ( writtenContent . nested ) . not . toHaveProperty ( "extra" ) ;
498+
499+ // Only locked keys and payload keys should be present
500+ expect ( Object . keys ( writtenContent ) ) . toEqual ( expect . arrayContaining ( [ "button.title" , "locked.key" , "nested" ] ) ) ;
501+ expect ( Object . keys ( writtenContent ) ) . toHaveLength ( 3 ) ;
502+ expect ( Object . keys ( writtenContent . nested ) ) . toEqual ( expect . arrayContaining ( [ "locked" , "unlocked" ] ) ) ;
503+ expect ( Object . keys ( writtenContent . nested ) ) . toHaveLength ( 2 ) ;
504+ } ) ;
505+
447506 it ( "should load and save json data for paths with multiple locales" , async ( ) => {
448507 setupFileMocks ( ) ;
449508
@@ -507,6 +566,186 @@ describe("bucket loaders", () => {
507566 } ) ;
508567 } ) ;
509568
569+ describe ( "locked keys functionality" , ( ) => {
570+ it ( "should respect locked keys for JSON format" , async ( ) => {
571+ setupFileMocks ( ) ;
572+
573+ const input = {
574+ "button.title" : "Submit" ,
575+ "button.description" : "Submit description" ,
576+ "locked.key" : "Should not change" ,
577+ nested : {
578+ locked : "This is locked" ,
579+ unlocked : "This can change" ,
580+ } ,
581+ } ;
582+ const payload = {
583+ "button.title" : "Enviar" ,
584+ "button.description" : "Descripción de envío" ,
585+ "locked.key" : "This should not be applied" ,
586+ "nested/locked" : "This should not be applied either" ,
587+ "nested/unlocked" : "Este puede cambiar" ,
588+ } ;
589+
590+ mockFileOperations ( JSON . stringify ( input ) ) ;
591+
592+ const jsonLoader = createBucketLoader (
593+ "json" ,
594+ "i18n/[locale].json" ,
595+ { isCacheRestore : false , defaultLocale : "en" } ,
596+ [ "locked.key" , "nested/locked" ] ,
597+ ) ;
598+
599+ jsonLoader . setDefaultLocale ( "en" ) ;
600+ await jsonLoader . pull ( "en" ) ;
601+
602+ await jsonLoader . push ( "es" , payload ) ;
603+
604+ expect ( fs . writeFile ) . toHaveBeenCalled ( ) ;
605+ const writeFileCall = ( fs . writeFile as any ) . mock . calls [ 0 ] ;
606+ const writtenContent = JSON . parse ( writeFileCall [ 1 ] ) ;
607+
608+ // Check that locked keys retain their original values
609+ expect ( writtenContent [ "locked.key" ] ) . toBe ( "Should not change" ) ;
610+ expect ( writtenContent . nested . locked ) . toBe ( "This is locked" ) ;
611+
612+ // Check that unlocked keys are updated
613+ expect ( writtenContent [ "button.title" ] ) . toBe ( "Enviar" ) ;
614+ expect ( writtenContent [ "button.description" ] ) . toBe ( "Descripción de envío" ) ;
615+ expect ( writtenContent . nested . unlocked ) . toBe ( "Este puede cambiar" ) ;
616+ } ) ;
617+
618+ it ( "should respect locked keys during cache restoration" , async ( ) => {
619+ setupFileMocks ( ) ;
620+
621+ const input = {
622+ "button.title" : "Submit" ,
623+ "locked.key" : "Should not change" ,
624+ nested : {
625+ locked : "This is locked" ,
626+ unlocked : "This can change" ,
627+ } ,
628+ } ;
629+ const payload = {
630+ "button.title" : "Enviar" ,
631+ "locked.key" : "This should not be applied" ,
632+ "nested/locked" : "This should not be applied either" ,
633+ "nested/unlocked" : "Este puede cambiar" ,
634+ } ;
635+
636+ mockFileOperations ( JSON . stringify ( input ) ) ;
637+
638+ const jsonLoader = createBucketLoader (
639+ "json" ,
640+ "i18n/[locale].json" ,
641+ { isCacheRestore : true , defaultLocale : "en" } ,
642+ [ "locked.key" , "nested/locked" ] ,
643+ ) ;
644+
645+ jsonLoader . setDefaultLocale ( "en" ) ;
646+ await jsonLoader . pull ( "en" ) ;
647+
648+ await jsonLoader . push ( "es" , payload ) ;
649+
650+ expect ( fs . writeFile ) . toHaveBeenCalled ( ) ;
651+ const writeFileCall = ( fs . writeFile as any ) . mock . calls [ 0 ] ;
652+ const writtenContent = JSON . parse ( writeFileCall [ 1 ] ) ;
653+
654+ expect ( Object . keys ( writtenContent ) ) . toContain ( "button.title" ) ;
655+ expect ( writtenContent [ "locked.key" ] ) . toBe ( "Should not change" ) ;
656+ expect ( writtenContent . nested ) . toHaveProperty ( "unlocked" , "Este puede cambiar" ) ;
657+ expect ( writtenContent . nested ) . toHaveProperty ( "locked" , "This is locked" ) ;
658+ } ) ;
659+
660+ it ( "should handle deeply nested locked keys" , async ( ) => {
661+ setupFileMocks ( ) ;
662+
663+ const input = {
664+ level1 : {
665+ level2 : {
666+ level3 : {
667+ locked : "This is locked deep" ,
668+ unlocked : "This can change" ,
669+ } ,
670+ } ,
671+ } ,
672+ } ;
673+ const payload = {
674+ "level1/level2/level3/locked" : "This should not be applied" ,
675+ "level1/level2/level3/unlocked" : "This should change" ,
676+ } ;
677+
678+ mockFileOperations ( JSON . stringify ( input ) ) ;
679+
680+ const jsonLoader = createBucketLoader (
681+ "json" ,
682+ "i18n/[locale].json" ,
683+ { isCacheRestore : false , defaultLocale : "en" } ,
684+ [ "level1/level2/level3/locked" ] ,
685+ ) ;
686+
687+ jsonLoader . setDefaultLocale ( "en" ) ;
688+ await jsonLoader . pull ( "en" ) ;
689+
690+ await jsonLoader . push ( "es" , payload ) ;
691+
692+ expect ( fs . writeFile ) . toHaveBeenCalled ( ) ;
693+ const writeFileCall = ( fs . writeFile as any ) . mock . calls [ 0 ] ;
694+ const writtenContent = JSON . parse ( writeFileCall [ 1 ] ) ;
695+
696+ // Check that deeply nested locked key retains its original value
697+ expect ( writtenContent . level1 . level2 . level3 . locked ) . toBe ( "This is locked deep" ) ;
698+
699+ // Check that unlocked key is updated
700+ expect ( writtenContent . level1 . level2 . level3 . unlocked ) . toBe ( "This should change" ) ;
701+ } ) ;
702+
703+ it ( "should lock keys that are arrays" , async ( ) => {
704+ setupFileMocks ( ) ;
705+
706+ const input = {
707+ messages : [ "first" , "second" , "third" ] ,
708+ unlocked : [ "can" , "be" , "changed" ] ,
709+ } ;
710+ const payload = {
711+ "messages/0" : "should not change" ,
712+ "messages/1" : "should not change either" ,
713+ "messages/2" : "should definitely not change" ,
714+ "unlocked/0" : "should" ,
715+ "unlocked/1" : "definitely" ,
716+ "unlocked/2" : "change" ,
717+ } ;
718+
719+ mockFileOperations ( JSON . stringify ( input ) ) ;
720+
721+ const jsonLoader = createBucketLoader (
722+ "json" ,
723+ "i18n/[locale].json" ,
724+ { isCacheRestore : false , defaultLocale : "en" } ,
725+ [ "messages/0" , "messages/1" , "messages/2" ] ,
726+ ) ;
727+
728+ jsonLoader . setDefaultLocale ( "en" ) ;
729+ await jsonLoader . pull ( "en" ) ;
730+
731+ await jsonLoader . push ( "es" , payload ) ;
732+
733+ expect ( fs . writeFile ) . toHaveBeenCalled ( ) ;
734+ const writeFileCall = ( fs . writeFile as any ) . mock . calls [ 0 ] ;
735+ const writtenContent = JSON . parse ( writeFileCall [ 1 ] ) ;
736+
737+ // Check that locked array elements retain their original values
738+ expect ( writtenContent . messages [ 0 ] ) . toBe ( "first" ) ;
739+ expect ( writtenContent . messages [ 1 ] ) . toBe ( "second" ) ;
740+ expect ( writtenContent . messages [ 2 ] ) . toBe ( "third" ) ;
741+
742+ // Check that unlocked array elements are updated
743+ expect ( writtenContent . unlocked [ 0 ] ) . toBe ( "should" ) ;
744+ expect ( writtenContent . unlocked [ 1 ] ) . toBe ( "definitely" ) ;
745+ expect ( writtenContent . unlocked [ 2 ] ) . toBe ( "change" ) ;
746+ } ) ;
747+ } ) ;
748+
510749 describe ( "markdown bucket loader" , ( ) => {
511750 it ( "should load markdown data" , async ( ) => {
512751 setupFileMocks ( ) ;
0 commit comments