package service import ( "bugulma/backend/internal/domain" "bugulma/backend/internal/localization" "context" "fmt" "strings" "time" ) // TranslationWorkflowService handles the orchestration of translation workflows type TranslationWorkflowService struct { locRepo domain.LocalizationRepository locService domain.LocalizationService entityLoader *EntityLoaderService translationService *TranslationService cacheService *TranslationCacheService } // NewTranslationWorkflowService creates a new translation workflow service func NewTranslationWorkflowService( locRepo domain.LocalizationRepository, locService domain.LocalizationService, entityLoader *EntityLoaderService, translationService *TranslationService, cacheService *TranslationCacheService, ) *TranslationWorkflowService { return &TranslationWorkflowService{ locRepo: locRepo, locService: locService, entityLoader: entityLoader, translationService: translationService, cacheService: cacheService, } } // TranslationResult represents the result of a translation operation type TranslationResult struct { Success bool Translated string Cached bool Error error EntityType string EntityID string Field string TargetLocale string } // TranslationSummary represents a summary of translation operations type TranslationSummary struct { TotalEntities int ProcessedFields int Translated int Cached int Errors int Skipped int Duration time.Duration } // ProcessEntityTranslation processes translation for all fields of an entity func (s *TranslationWorkflowService) ProcessEntityTranslation( ctx context.Context, entityType string, entity interface{}, targetLocale string, dryRun bool, ) ([]TranslationResult, error) { entityID := s.entityLoader.GetEntityID(entity) results := []TranslationResult{} // Get localizable fields for this entity type fields := localization.GetFieldsForEntityType(entityType) if len(fields) == 0 { return results, fmt.Errorf("no localizable fields found for entity type: %s", entityType) } for _, field := range fields { russianText := s.entityLoader.GetRussianContent(entity, field) if russianText == "" { continue } // Skip very short content if len(strings.TrimSpace(russianText)) < 2 { continue } result := s.processFieldTranslation(ctx, entityType, entityID, field, russianText, targetLocale, dryRun) results = append(results, result) } return results, nil } // ProcessBatchTranslation processes translation for multiple entities func (s *TranslationWorkflowService) ProcessBatchTranslation( ctx context.Context, entityType string, entityIDs []string, targetLocale string, dryRun bool, ) (*TranslationSummary, error) { startTime := time.Now() summary := &TranslationSummary{} for _, entityID := range entityIDs { entity, err := s.entityLoader.LoadEntityByID(entityType, entityID) if err != nil { summary.Errors++ continue } results, err := s.ProcessEntityTranslation(ctx, entityType, entity, targetLocale, dryRun) if err != nil { summary.Errors++ continue } summary.ProcessedFields += len(results) summary.TotalEntities++ for _, result := range results { if result.Cached { summary.Cached++ } else if result.Success { summary.Translated++ } else { summary.Errors++ } } } summary.Duration = time.Since(startTime) return summary, nil } // GetTranslationStats returns statistics for translations func (s *TranslationWorkflowService) GetTranslationStats(ctx context.Context, entityTypeFilter string) (*domain.TranslationStats, error) { entityTypes := localization.GetEntityTypesForFilter(entityTypeFilter) stats := &domain.TranslationStats{ EntityStats: make(map[string]*domain.EntityTranslationStats), } totalEntities := 0 totalFields := 0 totalTranslations := 0 for _, entityType := range entityTypes { entityStats, err := s.getEntityTranslationStats(ctx, entityType) if err != nil { continue } stats.EntityStats[entityType] = entityStats totalEntities += entityStats.TotalEntities totalFields += entityStats.TotalFields totalTranslations += entityStats.TotalTranslations } stats.TotalEntities = totalEntities stats.TotalFields = totalFields stats.TotalTranslations = totalTranslations return stats, nil } // WorkflowTranslationRequest represents a translation request in the workflow type WorkflowTranslationRequest struct { TargetLocale string EntityType string EntityID string Field string RussianText string DryRun bool } // processFieldTranslation processes translation for a single field func (s *TranslationWorkflowService) processFieldTranslation( ctx context.Context, entityType, entityID, field, russianText, targetLocale string, dryRun bool, ) TranslationResult { result := TranslationResult{ EntityType: entityType, EntityID: entityID, Field: field, TargetLocale: targetLocale, } // Check if translation already exists if existing, err := s.locService.GetLocalizedValue(entityType, entityID, field, targetLocale); err == nil && existing != "" { result.Success = true result.Cached = true result.Translated = existing return result } // Check translation cache if cached, found, _ := s.cacheService.FindCachedTranslation(ctx, entityType, field, targetLocale, russianText); found { if !dryRun { if err := s.locService.SetLocalizedValue(entityType, entityID, field, targetLocale, cached); err != nil { result.Error = fmt.Errorf("failed to save cached translation: %w", err) return result } } result.Success = true result.Cached = true result.Translated = cached return result } // Perform translation if dryRun { result.Success = true result.Translated = "[DRY RUN]" return result } translated, err := s.translationService.Translate(russianText, "ru", targetLocale) if err != nil { result.Error = fmt.Errorf("translation failed: %w", err) return result } if translated == "" { result.Error = fmt.Errorf("empty translation received") return result } if err := s.locService.SetLocalizedValue(entityType, entityID, field, targetLocale, translated); err != nil { result.Error = fmt.Errorf("failed to save translation: %w", err) return result } // Update cache s.cacheService.AddToCache(entityType, field, targetLocale, russianText, translated) result.Success = true result.Translated = translated return result } // getEntityTranslationStats gets stats for a specific entity type func (s *TranslationWorkflowService) getEntityTranslationStats(ctx context.Context, entityType string) (*domain.EntityTranslationStats, error) { entities, err := s.entityLoader.LoadEntities(entityType, localization.LoadOptions{}) if err != nil { return nil, err } fields := localization.GetFieldsForEntityType(entityType) totalFields := len(entities) * len(fields) // Count translations by locale ruCount := 0 enCount := 0 ttCount := 0 for _, entity := range entities { entityID := s.entityLoader.GetEntityID(entity) for _, field := range fields { if ruVal, _ := s.locService.GetLocalizedValue(entityType, entityID, field, "ru"); ruVal != "" { ruCount++ } if enVal, _ := s.locService.GetLocalizedValue(entityType, entityID, field, "en"); enVal != "" { enCount++ } if ttVal, _ := s.locService.GetLocalizedValue(entityType, entityID, field, "tt"); ttVal != "" { ttCount++ } } } return &domain.EntityTranslationStats{ EntityType: entityType, TotalEntities: len(entities), TotalFields: totalFields, RussianCount: ruCount, EnglishCount: enCount, TatarCount: ttCount, TotalTranslations: ruCount + enCount + ttCount, }, nil }