@@ -289,8 +289,9 @@ export default function StoreListingPage() {
289289 originalCopyrightRef . current = cr ;
290290 }
291291
292- // Track original locale → localization ID mapping for diffing saves
292+ // Track original locale → localization ID mapping and data for diffing saves
293293 const originalLocaleIdsRef = useRef < Record < string , string > > ( { } ) ;
294+ const originalLocaleDataRef = useRef < Record < string , LocaleFields > > ( { } ) ;
294295
295296 // Reset locale data when localizations change (during render)
296297 const [ prevLocalizations , setPrevLocalizations ] = useState ( localizations ) ;
@@ -311,13 +312,14 @@ export default function StoreListingPage() {
311312 setDirty ( false ) ;
312313 }
313314
314- // Snapshot original locale → ID mapping for save diffing
315+ // Snapshot original locale IDs and data for save diffing
315316 useEffect ( ( ) => {
316317 const ids : Record < string , string > = { } ;
317318 for ( const loc of localizations ) {
318319 ids [ loc . attributes . locale ] = loc . id ;
319320 }
320321 originalLocaleIdsRef . current = ids ;
322+ originalLocaleDataRef . current = buildLocaleData ( localizations ) ;
321323 } , [ localizations ] ) ;
322324
323325 // Validate field limits across all locales
@@ -363,44 +365,57 @@ export default function StoreListingPage() {
363365 const promises : Promise < void > [ ] = [ ] ;
364366 const allSyncErrors : SyncError [ ] = [ ] ;
365367
366- // When the version is read-only (live), only promotional text is
367- // editable – send just that field to avoid ASC rejecting locked fields.
368- // For the first-ever version, strip whatsNew – ASC rejects it.
369- const locPayload = readOnly
370- ? Object . fromEntries (
371- Object . entries ( localeData ) . map ( ( [ locale , fields ] ) => [
372- locale ,
373- { promotionalText : fields . promotionalText } ,
374- ] ) ,
375- )
376- : isFirstVersion
377- ? Object . fromEntries (
378- Object . entries ( localeData ) . map ( ( [ locale , fields ] ) => [
379- locale ,
380- Object . fromEntries ( Object . entries ( fields ) . filter ( ( [ k ] ) => k !== "whatsNew" ) ) ,
381- ] ) ,
382- )
383- : localeData ;
384-
385- // Save localizations
368+ // Only send locales that actually changed (or are new/deleted)
369+ const changedLocales : Record < string , LocaleFields | Record < string , string > > = { } ;
370+ const changedLocaleIds : Record < string , string > = { } ;
371+ const orig = originalLocaleDataRef . current ;
372+
373+ for ( const [ locale , fields ] of Object . entries ( localeData ) ) {
374+ const origFields = orig [ locale ] ;
375+ // New locale or any field differs → include it
376+ if ( ! origFields || Object . keys ( fields ) . some ( ( k ) => fields [ k as keyof LocaleFields ] !== origFields [ k as keyof LocaleFields ] ) ) {
377+ changedLocaleIds [ locale ] = originalLocaleIdsRef . current [ locale ] ;
378+ // When read-only, only promotional text is editable.
379+ // For the first-ever version, strip whatsNew – ASC rejects it.
380+ if ( readOnly ) {
381+ changedLocales [ locale ] = { promotionalText : fields . promotionalText } ;
382+ } else if ( isFirstVersion ) {
383+ changedLocales [ locale ] = Object . fromEntries (
384+ Object . entries ( fields ) . filter ( ( [ k ] ) => k !== "whatsNew" ) ,
385+ ) ;
386+ } else {
387+ changedLocales [ locale ] = fields ;
388+ }
389+ }
390+ }
391+ // Detect deleted locales
392+ for ( const locale of Object . keys ( originalLocaleIdsRef . current ) ) {
393+ if ( ! localeData [ locale ] ) {
394+ changedLocaleIds [ locale ] = originalLocaleIdsRef . current [ locale ] ;
395+ }
396+ }
397+
398+ // Save localizations (skip if nothing changed)
386399 let locCreatedIds : Record < string , string > = { } ;
387- promises . push (
388- fetch ( `/api/apps/${ appId } /versions/${ versionId } /localizations` , {
389- method : "PUT" ,
390- headers : { "Content-Type" : "application/json" } ,
391- body : JSON . stringify ( {
392- locales : locPayload ,
393- originalLocaleIds : originalLocaleIdsRef . current ,
400+ if ( Object . keys ( changedLocales ) . length > 0 || Object . keys ( changedLocaleIds ) . length > Object . keys ( changedLocales ) . length ) {
401+ promises . push (
402+ fetch ( `/api/apps/${ appId } /versions/${ versionId } /localizations` , {
403+ method : "PUT" ,
404+ headers : { "Content-Type" : "application/json" } ,
405+ body : JSON . stringify ( {
406+ locales : changedLocales ,
407+ originalLocaleIds : changedLocaleIds ,
408+ } ) ,
409+ } ) . then ( async ( res ) => {
410+ const data = await res . json ( ) ;
411+ if ( ! res . ok && ! data . errors ) throw new Error ( data . error ?? "Save failed" ) ;
412+ if ( data . errors ?. length > 0 ) {
413+ allSyncErrors . push ( ...( data . errors as SyncError [ ] ) ) ;
414+ }
415+ locCreatedIds = data . createdIds ?? { } ;
394416 } ) ,
395- } ) . then ( async ( res ) => {
396- const data = await res . json ( ) ;
397- if ( ! res . ok && ! data . errors ) throw new Error ( data . error ?? "Save failed" ) ;
398- if ( data . errors ?. length > 0 ) {
399- allSyncErrors . push ( ...( data . errors as SyncError [ ] ) ) ;
400- }
401- locCreatedIds = data . createdIds ?? { } ;
402- } ) ,
403- ) ;
417+ ) ;
418+ }
404419
405420 // Release settings are locked on live versions
406421 if ( ! readOnly ) {
@@ -478,7 +493,7 @@ export default function StoreListingPage() {
478493
479494 toast . success ( readOnly ? "Promotional text saved" : "Store listing saved" ) ;
480495
481- // Update original snapshot with real IDs from created locales
496+ // Update original snapshots so subsequent saves only send new diffs
482497 const ids = { ...originalLocaleIdsRef . current } ;
483498 for ( const [ locale , id ] of Object . entries ( locCreatedIds ) ) {
484499 ids [ locale ] = id ;
@@ -487,6 +502,7 @@ export default function StoreListingPage() {
487502 if ( ! localeData [ locale ] ) delete ids [ locale ] ;
488503 }
489504 originalLocaleIdsRef . current = ids ;
505+ originalLocaleDataRef . current = { ...localeData } ;
490506
491507 // Update cached version with release settings + build
492508 if ( ! readOnly && selectedVersion ) {
0 commit comments