package service import ( "bugulma/backend/internal/domain" "context" "fmt" ) // I18nService provides a unified interface for both UI/UX and data translations // This service is DRY, robust, and easy to use type I18nService struct { locService domain.LocalizationService locRepo domain.LocalizationRepository translationSvc *TranslationService cacheService *TranslationCacheService } // NewI18nService creates a new unified i18n service func NewI18nService( locService domain.LocalizationService, locRepo domain.LocalizationRepository, translationSvc *TranslationService, cacheService *TranslationCacheService, ) *I18nService { return &I18nService{ locService: locService, locRepo: locRepo, translationSvc: translationSvc, cacheService: cacheService, } } // SupportedLocales returns all supported locales var SupportedLocales = []string{"ru", "en", "tt"} // DefaultLocale is the default locale (Russian) const DefaultLocale = "ru" // TranslateData translates data entity fields (sites, organizations, etc.) // This method retrieves existing translations only. For new translations, use TranslateDataWithSource. // This is a convenience method for read-only translation retrieval. func (s *I18nService) TranslateData(ctx context.Context, entityType, entityID, field, targetLocale string) (string, error) { if targetLocale == DefaultLocale { // Russian is the source language, no translation needed return "", fmt.Errorf("target locale cannot be source locale (ru)") } // Return error if locService is not initialized if s.locService == nil { return "", fmt.Errorf("localization service not initialized") } // Get existing translation if available translated, err := s.locService.GetLocalizedValue(entityType, entityID, field, targetLocale) if err != nil { return "", fmt.Errorf("failed to get translation: %w", err) } if translated == "" { return "", fmt.Errorf("no translation found - use TranslateDataWithSource to create new translation") } return translated, nil } // TranslateDataWithSource translates data with provided source text func (s *I18nService) TranslateDataWithSource(ctx context.Context, entityType, entityID, field, sourceText, targetLocale string) (string, error) { if targetLocale == DefaultLocale { return sourceText, nil // No translation needed } // Check cache first if cached, found, _ := s.cacheService.FindCachedTranslation(ctx, entityType, field, targetLocale, sourceText); found { // Save to localization if not already saved if existing, _ := s.getLocalizedValueWithContext(ctx, entityType, entityID, field, targetLocale); existing == "" { _ = s.setLocalizedValueWithContext(ctx, entityType, entityID, field, targetLocale, cached) } return cached, nil } // Check if translation already exists in database existing, err := s.getLocalizedValueWithContext(ctx, entityType, entityID, field, targetLocale) if err == nil && existing != "" { // Cache it s.cacheService.AddToCache(entityType, field, targetLocale, sourceText, existing) return existing, nil } // Perform translation translated, err := s.translationSvc.Translate(sourceText, DefaultLocale, targetLocale) if err != nil { return "", fmt.Errorf("translation failed: %w", err) } // Save translation if err := s.setLocalizedValueWithContext(ctx, entityType, entityID, field, targetLocale, translated); err != nil { return "", fmt.Errorf("failed to save translation: %w", err) } // Cache translation s.cacheService.AddToCache(entityType, field, targetLocale, sourceText, translated) return translated, nil } // GetDataTranslation retrieves a data translation, with fallback to source func (s *I18nService) GetDataTranslation(ctx context.Context, entityType, entityID, field, locale string, fallbackSource string) string { if locale == DefaultLocale { return fallbackSource } translated, err := s.getLocalizedValueWithContext(ctx, entityType, entityID, field, locale) if err == nil && translated != "" { return translated } return fallbackSource } // GetUITranslation retrieves a UI translation by key // UI translations are stored with entityType="ui" and entityID="ui" func (s *I18nService) GetUITranslation(ctx context.Context, key, locale string, fallback string) string { if locale == DefaultLocale { // For Russian, return fallback or key if fallback != "" { return fallback } return key } // UI translations use entityType="ui", entityID="ui", field=key translated, err := s.getLocalizedValueWithContext(ctx, "ui", "ui", key, locale) if err == nil && translated != "" { return translated } // Fallback chain: requested locale -> default locale -> key if fallback != "" { return fallback } return key } // SetUITranslation sets a UI translation func (s *I18nService) SetUITranslation(ctx context.Context, key, locale, value string) error { return s.setLocalizedValueWithContext(ctx, "ui", "ui", key, locale, value) } // GetUITranslationsForLocale retrieves all UI translations for a specific locale func (s *I18nService) GetUITranslationsForLocale(ctx context.Context, locale string) (map[string]string, error) { if !s.ValidateLocale(locale) { return nil, fmt.Errorf("invalid locale: %s", locale) } // Get all UI localizations for this locale // UI translations use entityType="ui", entityID="ui" localizations, err := s.locRepo.GetByEntityTypeAndLocale(ctx, "ui", locale) if err != nil { return nil, fmt.Errorf("failed to retrieve UI translations: %w", err) } // Filter to only "ui" entityID and build map result := make(map[string]string) for _, loc := range localizations { if loc.EntityID == "ui" { result[loc.Field] = loc.Value } } return result, nil } // BatchTranslateUI translates multiple UI keys at once func (s *I18nService) BatchTranslateUI(ctx context.Context, keys []string, sourceLocale, targetLocale string) (map[string]string, error) { if targetLocale == DefaultLocale { return nil, fmt.Errorf("target locale cannot be source locale") } results := make(map[string]string) // Get source texts sourceTexts := make(map[string]string) for _, key := range keys { // Get source text from Russian locale source, err := s.getLocalizedValueWithContext(ctx, "ui", "ui", key, sourceLocale) if err != nil || source == "" { // Try fallback - use key as source source = key } sourceTexts[key] = source } // Extract source texts in order for batch translation sourceTextSlice := make([]string, len(keys)) for i, key := range keys { sourceTextSlice[i] = sourceTexts[key] } // Translate in batch translatedTexts, err := s.translationSvc.BatchTranslate( sourceTextSlice, sourceLocale, targetLocale, ) if err != nil { return nil, fmt.Errorf("batch translation failed: %w", err) } // Map translations back to keys for i, key := range keys { if i < len(translatedTexts) { translated := translatedTexts[i] results[key] = translated // Save translation _ = s.setLocalizedValueWithContext(ctx, "ui", "ui", key, targetLocale, translated) } } return results, nil } // GetLocalizedEntity returns an entity with all fields localized for a specific locale func (s *I18nService) GetLocalizedEntity(ctx context.Context, entity domain.Localizable, locale string) error { return s.applyLocalizationToEntityWithContext(ctx, entity, locale) } // GetSupportedLocales returns all supported locales func (s *I18nService) GetSupportedLocales(ctx context.Context) ([]string, error) { if s.locService == nil { // Return default supported locales if locService is not initialized return SupportedLocales, nil } if locSvc, ok := s.locService.(*LocalizationService); ok { return locSvc.GetAllLocalesWithContext(ctx) } // Fallback to interface method return s.locService.GetAllLocales() } // ValidateLocale validates if a locale is supported func (s *I18nService) ValidateLocale(locale string) bool { for _, supported := range SupportedLocales { if supported == locale { return true } } return false } // GetTranslationStats returns statistics about translations func (s *I18nService) GetTranslationStats(ctx context.Context, entityType string) (map[string]interface{}, error) { stats := make(map[string]interface{}) // Get cache stats if s.cacheService != nil { stats["cache"] = s.cacheService.GetCacheStats() } // Get supported locales (handles nil locService gracefully) locales, err := s.GetSupportedLocales(ctx) if err == nil { stats["available_locales"] = locales // Set default stats when locService is nil if s.locService == nil { stats["total_unique_keys"] = int64(0) stats["translated_counts"] = make(map[string]int64) return stats, nil } } // Get translation counts by entity type and locale if s.locRepo != nil { counts, err := s.locRepo.GetTranslationCountsByEntity(ctx) if err == nil { stats["counts_by_entity"] = counts if entityType != "" { if entityCounts, ok := counts[entityType]; ok { stats["entity_counts"] = entityCounts } } } } // Basic stats structure stats["entity_type"] = entityType stats["supported_locales"] = SupportedLocales stats["default_locale"] = DefaultLocale return stats, nil } // Helper methods to access context-aware methods on the concrete LocalizationService implementation func (s *I18nService) getLocalizedValueWithContext(ctx context.Context, entityType, entityID, field, locale string) (string, error) { if s.locService == nil { return "", fmt.Errorf("localization service not initialized") } if locSvc, ok := s.locService.(*LocalizationService); ok { return locSvc.GetLocalizedValueWithContext(ctx, entityType, entityID, field, locale) } // Fallback to interface method (uses context.Background internally) return s.locService.GetLocalizedValue(entityType, entityID, field, locale) } func (s *I18nService) setLocalizedValueWithContext(ctx context.Context, entityType, entityID, field, locale, value string) error { if s.locService == nil { return fmt.Errorf("localization service not initialized") } if locSvc, ok := s.locService.(*LocalizationService); ok { return locSvc.SetLocalizedValueWithContext(ctx, entityType, entityID, field, locale, value) } // Fallback to interface method (uses context.Background internally) return s.locService.SetLocalizedValue(entityType, entityID, field, locale, value) } // SetDataTranslation sets a data translation (admin only) func (s *I18nService) SetDataTranslation(ctx context.Context, entityType, entityID, field, locale, value string) error { return s.setLocalizedValueWithContext(ctx, entityType, entityID, field, locale, value) } func (s *I18nService) applyLocalizationToEntityWithContext(ctx context.Context, entity domain.Localizable, locale string) error { if s.locService == nil { return fmt.Errorf("localization service not initialized") } if locSvc, ok := s.locService.(*LocalizationService); ok { return locSvc.ApplyLocalizationToEntityWithContext(ctx, entity, locale) } // Fallback to interface method (uses context.Background internally) return s.locService.ApplyLocalizationToEntity(entity, locale) }