package service import ( "bugulma/backend/internal/domain" "bugulma/backend/internal/localization" "context" "fmt" "time" ) // Note: SupportedLocales and DefaultLocale are defined in i18n_service.go // They are shared constants used across all i18n-related services // LocalizationService implements the domain.LocalizationService interface type LocalizationService struct { repo domain.LocalizationRepository } // NewLocalizationService creates a new localization service func NewLocalizationService(repo domain.LocalizationRepository) domain.LocalizationService { return &LocalizationService{repo: repo} } // GetLocalizedValue retrieves a localized value for a specific entity field and locale func (s *LocalizationService) GetLocalizedValue(entityType, entityID, field, locale string) (string, error) { return s.GetLocalizedValueWithContext(context.Background(), entityType, entityID, field, locale) } // GetLocalizedValueWithContext retrieves a localized value with context support func (s *LocalizationService) GetLocalizedValueWithContext(ctx context.Context, entityType, entityID, field, locale string) (string, error) { if entityType == "" || entityID == "" || field == "" || locale == "" { return "", fmt.Errorf("all parameters (entityType, entityID, field, locale) are required") } // Validate locale if !s.isValidLocale(locale) { return "", fmt.Errorf("invalid locale: %s. Supported locales: ru, en, tt", locale) } loc, err := s.repo.GetByEntityAndField(ctx, entityType, entityID, field, locale) if err != nil { return "", fmt.Errorf("failed to retrieve localization: %w", err) } if loc == nil { return "", nil // No localization found } return loc.Value, nil } // SetLocalizedValue sets or updates a localized value for a specific entity field and locale func (s *LocalizationService) SetLocalizedValue(entityType, entityID, field, locale, value string) error { return s.SetLocalizedValueWithContext(context.Background(), entityType, entityID, field, locale, value) } // SetLocalizedValueWithContext sets or updates a localized value with context support func (s *LocalizationService) SetLocalizedValueWithContext(ctx context.Context, entityType, entityID, field, locale, value string) error { if entityType == "" || entityID == "" || field == "" || locale == "" { return fmt.Errorf("all parameters (entityType, entityID, field, locale) are required") } // Validate locale if !s.isValidLocale(locale) { return fmt.Errorf("invalid locale: %s. Supported locales: ru, en, tt", locale) } // Validate entity type and field combination if !s.isValidEntityField(entityType, field) { return fmt.Errorf("invalid field '%s' for entity type '%s'", field, entityType) } // Check if localization already exists existing, err := s.repo.GetByEntityAndField(ctx, entityType, entityID, field, locale) if err != nil { return fmt.Errorf("failed to check existing localization: %w", err) } now := time.Now() if existing != nil { // Update existing existing.Value = value existing.UpdatedAt = now return s.repo.Update(ctx, existing) } else { // Create new loc := &domain.Localization{ EntityType: entityType, EntityID: entityID, Field: field, Locale: locale, Value: value, CreatedAt: now, UpdatedAt: now, } return s.repo.Create(ctx, loc) } } // GetAllLocalizedValues retrieves all localized values for a specific entity func (s *LocalizationService) GetAllLocalizedValues(entityType, entityID string) (map[string]map[string]string, error) { return s.GetAllLocalizedValuesWithContext(context.Background(), entityType, entityID) } // GetAllLocalizedValuesWithContext retrieves all localized values with context support func (s *LocalizationService) GetAllLocalizedValuesWithContext(ctx context.Context, entityType, entityID string) (map[string]map[string]string, error) { if entityType == "" || entityID == "" { return nil, fmt.Errorf("entityType and entityID are required") } localizations, err := s.repo.GetAllByEntity(ctx, entityType, entityID) if err != nil { return nil, fmt.Errorf("failed to retrieve localizations: %w", err) } result := make(map[string]map[string]string) for _, loc := range localizations { if result[loc.Field] == nil { result[loc.Field] = make(map[string]string) } result[loc.Field][loc.Locale] = loc.Value } return result, nil } // GetLocalizedEntity retrieves an entity with all its fields localized for a specific locale func (s *LocalizationService) GetLocalizedEntity(entityType, entityID, locale string) (map[string]string, error) { return s.GetLocalizedEntityWithContext(context.Background(), entityType, entityID, locale) } // GetLocalizedEntityWithContext retrieves an entity with all its fields localized with context support func (s *LocalizationService) GetLocalizedEntityWithContext(ctx context.Context, entityType, entityID, locale string) (map[string]string, error) { if entityType == "" || entityID == "" || locale == "" { return nil, fmt.Errorf("all parameters (entityType, entityID, locale) are required") } if !s.isValidLocale(locale) { return nil, fmt.Errorf("invalid locale: %s. Supported locales: ru, en, tt", locale) } localizations, err := s.repo.GetAllByEntity(ctx, entityType, entityID) if err != nil { return nil, fmt.Errorf("failed to retrieve localizations: %w", err) } result := make(map[string]string) for _, loc := range localizations { if loc.Locale == locale { result[loc.Field] = loc.Value } } return result, nil } // GetSupportedLocalesForEntity returns all locales that have translations for a specific entity func (s *LocalizationService) GetSupportedLocalesForEntity(entityType, entityID string) ([]string, error) { return s.GetSupportedLocalesForEntityWithContext(context.Background(), entityType, entityID) } // GetSupportedLocalesForEntityWithContext returns all locales with context support func (s *LocalizationService) GetSupportedLocalesForEntityWithContext(ctx context.Context, entityType, entityID string) ([]string, error) { if entityType == "" || entityID == "" { return nil, fmt.Errorf("entityType and entityID are required") } return s.repo.GetSupportedLocalesForEntity(ctx, entityType, entityID) } // DeleteLocalizedValue deletes a specific localization func (s *LocalizationService) DeleteLocalizedValue(entityType, entityID, field, locale string) error { return s.DeleteLocalizedValueWithContext(context.Background(), entityType, entityID, field, locale) } // DeleteLocalizedValueWithContext deletes a specific localization with context support func (s *LocalizationService) DeleteLocalizedValueWithContext(ctx context.Context, entityType, entityID, field, locale string) error { if entityType == "" || entityID == "" || field == "" || locale == "" { return fmt.Errorf("all parameters (entityType, entityID, field, locale) are required") } loc, err := s.repo.GetByEntityAndField(ctx, entityType, entityID, field, locale) if err != nil { return fmt.Errorf("failed to find localization: %w", err) } if loc == nil { return fmt.Errorf("localization not found") } return s.repo.Delete(ctx, loc.ID) } // BulkSetLocalizedValues sets multiple localized values in a single operation func (s *LocalizationService) BulkSetLocalizedValues(entityType, entityID string, values map[string]map[string]string) error { return s.BulkSetLocalizedValuesWithContext(context.Background(), entityType, entityID, values) } // BulkSetLocalizedValuesWithContext sets multiple localized values with context support func (s *LocalizationService) BulkSetLocalizedValuesWithContext(ctx context.Context, entityType, entityID string, values map[string]map[string]string) error { if entityType == "" || entityID == "" { return fmt.Errorf("entityType and entityID are required") } if len(values) == 0 { return nil } var localizations []*domain.Localization now := time.Now() for field, localeValues := range values { for locale, value := range localeValues { if !s.isValidLocale(locale) { return fmt.Errorf("invalid locale: %s for field %s", locale, field) } if !s.isValidEntityField(entityType, field) { return fmt.Errorf("invalid field '%s' for entity type '%s'", field, entityType) } loc := &domain.Localization{ EntityType: entityType, EntityID: entityID, Field: field, Locale: locale, Value: value, CreatedAt: now, UpdatedAt: now, } localizations = append(localizations, loc) } } // First delete existing localizations for these fields/locales for _, loc := range localizations { existing, _ := s.repo.GetByEntityAndField(ctx, loc.EntityType, loc.EntityID, loc.Field, loc.Locale) if existing != nil { s.repo.Delete(ctx, existing.ID) // Ignore errors, continue with creation } } return s.repo.BulkCreate(ctx, localizations) } // GetAllLocales returns all available locales in the system func (s *LocalizationService) GetAllLocales() ([]string, error) { return s.GetAllLocalesWithContext(context.Background()) } // GetAllLocalesWithContext returns all available locales with context support func (s *LocalizationService) GetAllLocalesWithContext(ctx context.Context) ([]string, error) { return s.repo.GetAllLocales(ctx) } // SearchLocalizations searches for localizations containing specific text func (s *LocalizationService) SearchLocalizations(query, locale string, limit int) ([]*domain.Localization, error) { return s.SearchLocalizationsWithContext(context.Background(), query, locale, limit) } // SearchLocalizationsWithContext searches for localizations with context support func (s *LocalizationService) SearchLocalizationsWithContext(ctx context.Context, query, locale string, limit int) ([]*domain.Localization, error) { if query == "" { return nil, fmt.Errorf("search query cannot be empty") } if locale != "" && !s.isValidLocale(locale) { return nil, fmt.Errorf("invalid locale: %s", locale) } if limit <= 0 { limit = 50 // Default limit } if limit > 1000 { limit = 1000 // Max limit } return s.repo.SearchLocalizations(ctx, query, locale, limit) } // Helper methods for validation func (s *LocalizationService) isValidLocale(locale string) bool { // Use the centralized locale constants from I18nService for _, supported := range SupportedLocales { if supported == locale { return true } } return false } func (s *LocalizationService) isValidEntityField(entityType, field string) bool { // Use registry to get valid fields for the entity type fields := localization.GetFieldsForEntityType(entityType) for _, f := range fields { if f == field { return true } } return false } // ApplyLocalizationToEntity applies localization to a localizable entity for a specific locale func (s *LocalizationService) ApplyLocalizationToEntity(entity domain.Localizable, locale string) error { return s.ApplyLocalizationToEntityWithContext(context.Background(), entity, locale) } // ApplyLocalizationToEntityWithContext applies localization with context support func (s *LocalizationService) ApplyLocalizationToEntityWithContext(ctx context.Context, entity domain.Localizable, locale string) error { if entity == nil { return fmt.Errorf("entity cannot be nil") } if locale == "ru" { // Russian is the primary language, no localization needed return nil } if !s.isValidLocale(locale) { return fmt.Errorf("invalid locale: %s", locale) } localizations, err := s.GetAllLocalizedValuesWithContext(ctx, entity.GetEntityType(), entity.GetEntityID()) if err != nil { return fmt.Errorf("failed to get localizations: %w", err) } // Apply localizations based on entity type switch e := entity.(type) { case *domain.Site: if name, ok := localizations["name"][locale]; ok && name != "" { e.Name = name } if notes, ok := localizations["notes"][locale]; ok && notes != "" { e.Notes = notes } if builderOwner, ok := localizations["builder_owner"][locale]; ok && builderOwner != "" { e.BuilderOwner = builderOwner } if architect, ok := localizations["architect"][locale]; ok && architect != "" { e.Architect = architect } if originalPurpose, ok := localizations["original_purpose"][locale]; ok && originalPurpose != "" { e.OriginalPurpose = originalPurpose } if currentUse, ok := localizations["current_use"][locale]; ok && currentUse != "" { e.CurrentUse = currentUse } if style, ok := localizations["style"][locale]; ok && style != "" { e.Style = style } if materials, ok := localizations["materials"][locale]; ok && materials != "" { e.Materials = materials } case *domain.Organization: if name, ok := localizations["name"][locale]; ok && name != "" { e.Name = name } if description, ok := localizations["description"][locale]; ok && description != "" { e.Description = description } case *domain.User: if name, ok := localizations["name"][locale]; ok && name != "" { e.Name = name } } return nil }