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

330 lines
11 KiB
Go

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)
}