mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
330 lines
11 KiB
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)
|
|
}
|