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

371 lines
13 KiB
Go

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
}