turash/bugulma/backend/internal/service/translation_cache_service.go

227 lines
6.8 KiB
Go

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
}