@@ -56,9 +56,16 @@ export const ComposeTranslateModal = ({ payload }: SheetProps<SheetNames.COMPOSE
5656 const [ progress , setProgress ] = useState < [ number , number ] > ( [ 0 , 0 ] ) ;
5757 const [ failed , setFailed ] = useState ( false ) ;
5858
59- // Sheets stay mounted; a closed sheet cancels the request chain so it stops
60- // hitting the translation service in the background.
61- const closedRef = useRef ( false ) ;
59+ // Identifies the current translation run. A run is superseded by closing the sheet, by
60+ // unmount, by a re-show carrying a new payload, by changing the language pair, and by
61+ // starting another translation. translateMarkdown pages the service request by request and
62+ // only consults its cancel callback between pages, so a run always has to be assumed still
63+ // in flight: everything it writes back is gated on still being the current run.
64+ //
65+ // A plain "closed" flag was not enough. It was cleared again on re-show, and the result
66+ // handler did not consult it at all, so a run that resolved after the user switched target
67+ // language applied its result to the new pair, defeating the reset below.
68+ const runIdRef = useRef ( 0 ) ;
6269
6370 const sample = useMemo (
6471 ( ) =>
@@ -72,10 +79,19 @@ export const ComposeTranslateModal = ({ payload }: SheetProps<SheetNames.COMPOSE
7279 ) ;
7380 const tooShort = sample . trim ( ) . length < MIN_TRANSLATE_CHARS ;
7481
75- // Reset state and re-detect the source language on every open (payload is a
76- // fresh object per SheetManager.show).
82+ // Sheets unmount on hide, so the run has to be retired here as well: onClose is not
83+ // guaranteed to be the route the sheet leaves by, and a run left current keeps paging the
84+ // translation service for a screen the user has already dismissed.
85+ useEffect (
86+ ( ) => ( ) => {
87+ runIdRef . current += 1 ;
88+ } ,
89+ [ ] ,
90+ ) ;
91+
92+ // Sheets mount on show, so this resets state and re-detects the source language on every open.
7793 useEffect ( ( ) => {
78- closedRef . current = false ;
94+ runIdRef . current += 1 ;
7995 setTranslated ( '' ) ;
8096 setFailed ( false ) ;
8197 setTranslating ( false ) ;
@@ -111,8 +127,16 @@ export const ComposeTranslateModal = ({ payload }: SheetProps<SheetNames.COMPOSE
111127 setTarget ( reader && reader !== 'en' && LIBRETRANSLATE_TARGETS . has ( reader ) ? reader : 'es' ) ;
112128 } , [ source , appLang ] ) ;
113129
114- // A result translated into a previous language pair must never be applied.
130+ // A result translated into a previous language pair must never be applied. Retiring the run
131+ // is what enforces that: clearing the state alone loses the race against a run still in
132+ // flight, which would write its result back a moment later.
133+ //
134+ // Clearing `translating` is part of retiring it. The run being cancelled can no longer do it
135+ // itself, because every write it makes is now gated on still being current, and a stuck
136+ // `translating` leaves the spinner up with both action buttons disabled.
115137 useEffect ( ( ) => {
138+ runIdRef . current += 1 ;
139+ setTranslating ( false ) ;
116140 setTranslated ( '' ) ;
117141 setFailed ( false ) ;
118142 } , [ source , target ] ) ;
@@ -131,6 +155,10 @@ export const ComposeTranslateModal = ({ payload }: SheetProps<SheetNames.COMPOSE
131155 ) ;
132156
133157 const _translate = async ( ) => {
158+ runIdRef . current += 1 ;
159+ const runId = runIdRef . current ;
160+ const isCurrent = ( ) => runIdRef . current === runId ;
161+
134162 setTranslating ( true ) ;
135163 setFailed ( false ) ;
136164 setTranslated ( '' ) ;
@@ -140,17 +168,23 @@ export const ComposeTranslateModal = ({ payload }: SheetProps<SheetNames.COMPOSE
140168 body ,
141169 source ,
142170 target ,
143- ( done , total ) => setProgress ( [ done , total ] ) ,
144- ( ) => closedRef . current ,
171+ ( done , total ) => {
172+ if ( isCurrent ( ) ) {
173+ setProgress ( [ done , total ] ) ;
174+ }
175+ } ,
176+ ( ) => ! isCurrent ( ) ,
145177 ) ;
146- setTranslated ( result ) ;
178+ if ( isCurrent ( ) ) {
179+ setTranslated ( result ) ;
180+ }
147181 } catch ( error ) {
148182 console . log ( 'translate error : ' , error ) ;
149- if ( ! closedRef . current ) {
183+ if ( isCurrent ( ) ) {
150184 setFailed ( true ) ;
151185 }
152186 } finally {
153- if ( ! closedRef . current ) {
187+ if ( isCurrent ( ) ) {
154188 setTranslating ( false ) ;
155189 }
156190 }
@@ -173,7 +207,7 @@ export const ComposeTranslateModal = ({ payload }: SheetProps<SheetNames.COMPOSE
173207 } ;
174208
175209 const _handleOnSheetClose = ( ) => {
176- closedRef . current = true ;
210+ runIdRef . current += 1 ;
177211 setTranslating ( false ) ;
178212 } ;
179213
0 commit comments