11/**
22 * useChatTranslation — Issue #617
33 * Hook for real-time per-message translation with locale switching.
4+ *
5+ * Issue #1095: Added LRU eviction to prevent unbounded cache growth.
6+ * The cache is capped at MAX_TRANSLATION_CACHE entries; when the cap is
7+ * reached, the oldest entries are evicted to maintain a bounded footprint.
48 */
59
6- import { useCallback , useState } from 'react' ;
10+ import { useCallback , useRef , useState } from 'react' ;
711import { AppLocale } from '../i18n' ;
812import { ChatTranslationService } from '../services/ChatTranslationService' ;
913
14+ /** Maximum number of cached message translations before LRU eviction. */
15+ const MAX_TRANSLATION_CACHE = 200 ;
16+
1017export interface MessageTranslation {
1118 translatedText : string ;
1219 isLoading : boolean ;
@@ -16,6 +23,38 @@ export interface MessageTranslation {
1623
1724export function useChatTranslation ( targetLocale : AppLocale ) {
1825 const [ translations , setTranslations ] = useState < Record < string , MessageTranslation > > ( { } ) ;
26+ const accessOrder = useRef < string [ ] > ( [ ] ) ;
27+
28+ /**
29+ * Evict oldest entries when the cache exceeds MAX_TRANSLATION_CACHE.
30+ * Uses a simple FIFO strategy based on insertion order (accessOrder ref).
31+ */
32+ const evictIfNeeded = useCallback ( ( current : Record < string , MessageTranslation > ) => {
33+ const keys = Object . keys ( current ) ;
34+ if ( keys . length <= MAX_TRANSLATION_CACHE ) return current ;
35+
36+ // Evict oldest entries that are not currently visible
37+ const toRemove : string [ ] = [ ] ;
38+ const visibleKeys = new Set (
39+ accessOrder . current . filter ( ( k ) => current [ k ] ?. isVisible ) ,
40+ ) ;
41+
42+ for ( const key of accessOrder . current ) {
43+ if ( keys . length - toRemove . length <= MAX_TRANSLATION_CACHE ) break ;
44+ if ( ! visibleKeys . has ( key ) ) {
45+ toRemove . push ( key ) ;
46+ }
47+ }
48+
49+ // Update access order
50+ accessOrder . current = accessOrder . current . filter ( ( k ) => ! toRemove . includes ( k ) ) ;
51+
52+ const next = { ...current } ;
53+ for ( const key of toRemove ) {
54+ delete next [ key ] ;
55+ }
56+ return next ;
57+ } , [ ] ) ;
1958
2059 const toggleTranslation = useCallback (
2160 async ( messageId : string , originalText : string , sourceLocale : AppLocale = 'en' ) => {
@@ -32,26 +71,37 @@ export function useChatTranslation(targetLocale: AppLocale) {
3271
3372 // If already translated (cached), just show it
3473 if ( current ?. translatedText && ! current . error ) {
74+ // Move to end of access order (LRU update)
75+ accessOrder . current = accessOrder . current . filter ( ( id ) => id !== messageId ) ;
76+ accessOrder . current . push ( messageId ) ;
3577 setTranslations ( ( prev ) => ( {
3678 ...prev ,
3779 [ messageId ] : { ...prev [ messageId ] , isVisible : true } ,
3880 } ) ) ;
3981 return ;
4082 }
4183
84+ // Track access order for LRU
85+ if ( ! accessOrder . current . includes ( messageId ) ) {
86+ accessOrder . current . push ( messageId ) ;
87+ }
88+
4289 // Start loading
43- setTranslations ( ( prev ) => ( {
44- ...prev ,
45- [ messageId ] : { translatedText : '' , isLoading : true , error : null , isVisible : true } ,
46- } ) ) ;
90+ setTranslations ( ( prev ) => {
91+ const updated = {
92+ ...prev ,
93+ [ messageId ] : { translatedText : '' , isLoading : true , error : null , isVisible : true } ,
94+ } ;
95+ return evictIfNeeded ( updated ) ;
96+ } ) ;
4797
4898 try {
4999 const result = await ChatTranslationService . translate (
50100 originalText ,
51101 targetLocale ,
52102 sourceLocale ,
53103 ) ;
54- setTranslations ( ( prev ) => ( {
104+ setTranslations ( ( prev ) => evictIfNeeded ( {
55105 ...prev ,
56106 [ messageId ] : {
57107 translatedText : result . translatedText ,
@@ -61,7 +111,7 @@ export function useChatTranslation(targetLocale: AppLocale) {
61111 } ,
62112 } ) ) ;
63113 } catch {
64- setTranslations ( ( prev ) => ( {
114+ setTranslations ( ( prev ) => evictIfNeeded ( {
65115 ...prev ,
66116 [ messageId ] : {
67117 translatedText : '' ,
@@ -72,10 +122,11 @@ export function useChatTranslation(targetLocale: AppLocale) {
72122 } ) ) ;
73123 }
74124 } ,
75- [ targetLocale , translations ] ,
125+ [ targetLocale , translations , evictIfNeeded ] ,
76126 ) ;
77127
78128 const clearTranslations = useCallback ( ( ) => {
129+ accessOrder . current = [ ] ;
79130 setTranslations ( { } ) ;
80131 } , [ ] ) ;
81132
0 commit comments