package service import ( "bugulma/backend/internal/domain" "context" "crypto/md5" "fmt" "strings" "sync" "time" ) // TranslationCacheEntry represents a cached translation type TranslationCacheEntry struct { EntityType string Field string TargetLocale string RussianText string Translated string UseCount int LastUsed int64 } // TranslationCacheService manages translation caching and reuse type TranslationCacheService struct { cache map[string]*TranslationCacheEntry locRepo domain.LocalizationRepository locService domain.LocalizationService mutex sync.RWMutex maxSize int } // NewTranslationCacheService creates a new translation cache service func NewTranslationCacheService(locRepo domain.LocalizationRepository, locService domain.LocalizationService) *TranslationCacheService { return &TranslationCacheService{ cache: make(map[string]*TranslationCacheEntry), locRepo: locRepo, locService: locService, maxSize: 10000, // Configurable cache size } } // FindCachedTranslation finds a cached translation for the given parameters // Returns (translation, found, error) for consistency with TranslationCache API func (s *TranslationCacheService) FindCachedTranslation(ctx context.Context, entityType, field, targetLocale, russianText string) (string, bool, error) { if russianText == "" { return "", false, nil } s.mutex.RLock() key := s.generateCacheKey(entityType, field, targetLocale, russianText) if entry, exists := s.cache[key]; exists { entry.UseCount++ entry.LastUsed = time.Now().Unix() s.mutex.RUnlock() return entry.Translated, true, nil } s.mutex.RUnlock() // Try to find in database (slower but persistent) if translation := s.findInDatabase(ctx, entityType, field, targetLocale, russianText); translation != "" { // Add to cache s.AddToCache(entityType, field, targetLocale, russianText, translation) return translation, true, nil } return "", false, nil } // FindCachedTranslationLegacy is the legacy API for backward compatibility // Deprecated: Use FindCachedTranslation with context instead func (s *TranslationCacheService) FindCachedTranslationLegacy(entityType, field, targetLocale, russianText string) string { ctx := context.Background() translation, found, _ := s.FindCachedTranslation(ctx, entityType, field, targetLocale, russianText) if found { return translation } return "" } // AddToCache adds a translation to the cache func (s *TranslationCacheService) AddToCache(entityType, field, targetLocale, russianText, translated string) { s.mutex.Lock() defer s.mutex.Unlock() s.addToCache(entityType, field, targetLocale, russianText, translated) } // addToCache internal method to add to cache (assumes mutex is held) func (s *TranslationCacheService) addToCache(entityType, field, targetLocale, russianText, translated string) { key := s.generateCacheKey(entityType, field, targetLocale, russianText) entry := &TranslationCacheEntry{ EntityType: entityType, Field: field, TargetLocale: targetLocale, RussianText: russianText, Translated: translated, UseCount: 1, LastUsed: time.Now().Unix(), } // Evict least recently used if cache is full if len(s.cache) >= s.maxSize { s.evictLRU() } s.cache[key] = entry } // findInDatabase searches the database for existing translations of the same Russian text func (s *TranslationCacheService) findInDatabase(ctx context.Context, entityType, field, targetLocale, russianText string) string { if russianText == "" { return "" } normalized := strings.TrimSpace(russianText) if normalized == "" { return "" } // Search for localizations with matching Russian source text localizations, err := s.locRepo.SearchLocalizations(ctx, normalized, "ru", 10) if err != nil { return "" } // Find a matching Russian localization and check if it has a target locale translation for _, loc := range localizations { if loc.EntityType == entityType && loc.Field == field && loc.Value == normalized { // Found matching Russian text, now check for target locale translation targetLoc, err := s.locRepo.GetByEntityAndField(ctx, loc.EntityType, loc.EntityID, loc.Field, targetLocale) if err == nil && targetLoc != nil && targetLoc.Value != "" { return targetLoc.Value } } } return "" } // generateCacheKey generates a unique key for cache entries func (s *TranslationCacheService) generateCacheKey(entityType, field, targetLocale, russianText string) string { // Create a hash of the Russian text to handle long texts textHash := fmt.Sprintf("%x", md5.Sum([]byte(russianText))) return fmt.Sprintf("%s:%s:%s:%s", entityType, field, targetLocale, textHash) } // evictLRU evicts the least recently used entry from the cache func (s *TranslationCacheService) evictLRU() { var oldestKey string var oldestTime int64 = -1 for key, entry := range s.cache { if oldestTime == -1 || entry.LastUsed < oldestTime { oldestTime = entry.LastUsed oldestKey = key } } if oldestKey != "" { delete(s.cache, oldestKey) } } // GetCacheStats returns statistics about the cache func (s *TranslationCacheService) GetCacheStats() map[string]interface{} { s.mutex.RLock() defer s.mutex.RUnlock() totalEntries := len(s.cache) totalUses := 0 avgUses := 0.0 if totalEntries > 0 { for _, entry := range s.cache { totalUses += entry.UseCount } avgUses = float64(totalUses) / float64(totalEntries) } return map[string]interface{}{ "total_entries": totalEntries, "max_size": s.maxSize, "total_uses": totalUses, "avg_uses": avgUses, "utilization": float64(totalEntries) / float64(s.maxSize) * 100, } } // ClearCache clears all entries from the cache func (s *TranslationCacheService) ClearCache() { s.mutex.Lock() defer s.mutex.Unlock() s.cache = make(map[string]*TranslationCacheEntry) } // PreloadCache preloads the cache with commonly used translations func (s *TranslationCacheService) PreloadCache(ctx context.Context, entityType, field, targetLocale string) error { if ctx == nil { ctx = context.Background() } // Get translation reuse candidates (Russian text that appears in multiple entities) candidates, err := s.locRepo.GetTranslationReuseCandidates(ctx, entityType, field, "ru") if err != nil { return fmt.Errorf("failed to get reuse candidates: %w", err) } // For each candidate, find existing translations and cache them for _, candidate := range candidates { // Find any translation of this Russian text to the target locale localizations, err := s.locRepo.SearchLocalizations(ctx, candidate.RussianValue, targetLocale, 1) if err != nil || len(localizations) == 0 { continue } // Use the first matching translation translation := localizations[0].Value if translation != "" { s.AddToCache(entityType, field, targetLocale, candidate.RussianValue, translation) } } return nil }