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

289 lines
7.7 KiB
Go

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
}