mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
431 lines
14 KiB
Go
431 lines
14 KiB
Go
package repository
|
|
|
|
import (
|
|
"bugulma/backend/internal/domain"
|
|
"bugulma/backend/internal/localization"
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
// LocalizationRepository implements the domain.LocalizationRepository interface
|
|
type LocalizationRepository struct {
|
|
db *gorm.DB
|
|
}
|
|
|
|
// NewLocalizationRepository creates a new localization repository
|
|
func NewLocalizationRepository(db *gorm.DB) domain.LocalizationRepository {
|
|
return &LocalizationRepository{db: db}
|
|
}
|
|
|
|
// Create creates a new localization record
|
|
func (r *LocalizationRepository) Create(ctx context.Context, loc *domain.Localization) error {
|
|
if loc == nil {
|
|
return fmt.Errorf("localization cannot be nil")
|
|
}
|
|
|
|
// Validate required fields
|
|
if loc.EntityType == "" {
|
|
return fmt.Errorf("entity_type is required")
|
|
}
|
|
if loc.EntityID == "" {
|
|
return fmt.Errorf("entity_id is required")
|
|
}
|
|
if loc.Field == "" {
|
|
return fmt.Errorf("field is required")
|
|
}
|
|
if loc.Locale == "" {
|
|
return fmt.Errorf("locale is required")
|
|
}
|
|
if loc.Value == "" {
|
|
return fmt.Errorf("value cannot be empty")
|
|
}
|
|
|
|
// Generate composite ID if not provided
|
|
if loc.ID == "" {
|
|
loc.ID = fmt.Sprintf("%s_%s_%s_%s", loc.EntityType, loc.EntityID, loc.Field, loc.Locale)
|
|
}
|
|
|
|
return r.db.WithContext(ctx).Create(loc).Error
|
|
}
|
|
|
|
// GetByEntityAndField retrieves a localization by entity type, entity ID, field, and locale
|
|
func (r *LocalizationRepository) GetByEntityAndField(ctx context.Context, entityType, entityID, field, locale string) (*domain.Localization, error) {
|
|
if entityType == "" || entityID == "" || field == "" || locale == "" {
|
|
return nil, fmt.Errorf("all parameters (entityType, entityID, field, locale) are required")
|
|
}
|
|
|
|
var loc domain.Localization
|
|
err := r.db.WithContext(ctx).Where("entity_type = ? AND entity_id = ? AND field = ? AND locale = ?",
|
|
entityType, entityID, field, locale).First(&loc).Error
|
|
|
|
if err == gorm.ErrRecordNotFound {
|
|
return nil, nil // Return nil instead of error for not found
|
|
}
|
|
|
|
return &loc, err
|
|
}
|
|
|
|
// GetAllByEntity retrieves all localizations for a specific entity
|
|
func (r *LocalizationRepository) GetAllByEntity(ctx context.Context, entityType, entityID string) ([]*domain.Localization, error) {
|
|
if entityType == "" || entityID == "" {
|
|
return nil, fmt.Errorf("entityType and entityID are required")
|
|
}
|
|
|
|
var localizations []*domain.Localization
|
|
err := r.db.WithContext(ctx).Where("entity_type = ? AND entity_id = ?",
|
|
entityType, entityID).Order("field, locale").Find(&localizations).Error
|
|
|
|
return localizations, err
|
|
}
|
|
|
|
// Update updates an existing localization record
|
|
func (r *LocalizationRepository) Update(ctx context.Context, loc *domain.Localization) error {
|
|
if loc == nil {
|
|
return fmt.Errorf("localization cannot be nil")
|
|
}
|
|
if loc.ID == "" {
|
|
return fmt.Errorf("localization ID is required for update")
|
|
}
|
|
|
|
// Validate required fields
|
|
if loc.EntityType == "" {
|
|
return fmt.Errorf("entity_type is required")
|
|
}
|
|
if loc.EntityID == "" {
|
|
return fmt.Errorf("entity_id is required")
|
|
}
|
|
if loc.Field == "" {
|
|
return fmt.Errorf("field is required")
|
|
}
|
|
if loc.Locale == "" {
|
|
return fmt.Errorf("locale is required")
|
|
}
|
|
|
|
return r.db.WithContext(ctx).Save(loc).Error
|
|
}
|
|
|
|
// Delete deletes a localization record by ID
|
|
func (r *LocalizationRepository) Delete(ctx context.Context, id string) error {
|
|
if id == "" {
|
|
return fmt.Errorf("localization ID is required")
|
|
}
|
|
|
|
return r.db.WithContext(ctx).Delete(&domain.Localization{}, "id = ?", id).Error
|
|
}
|
|
|
|
// GetByEntityTypeAndLocale retrieves all localizations for entities of a specific type and locale
|
|
func (r *LocalizationRepository) GetByEntityTypeAndLocale(ctx context.Context, entityType, locale string) ([]*domain.Localization, error) {
|
|
if entityType == "" || locale == "" {
|
|
return nil, fmt.Errorf("entityType and locale are required")
|
|
}
|
|
|
|
var localizations []*domain.Localization
|
|
err := r.db.WithContext(ctx).Where("entity_type = ? AND locale = ?",
|
|
entityType, locale).Order("entity_id, field").Find(&localizations).Error
|
|
|
|
return localizations, err
|
|
}
|
|
|
|
// GetAllLocales returns all available locales in the system
|
|
func (r *LocalizationRepository) GetAllLocales(ctx context.Context) ([]string, error) {
|
|
var locales []string
|
|
err := r.db.WithContext(ctx).Model(&domain.Localization{}).
|
|
Distinct("locale").
|
|
Pluck("locale", &locales).Error
|
|
|
|
return locales, err
|
|
}
|
|
|
|
// GetSupportedLocalesForEntity returns locales that have translations for a specific entity
|
|
func (r *LocalizationRepository) GetSupportedLocalesForEntity(ctx context.Context, entityType, entityID string) ([]string, error) {
|
|
if entityType == "" || entityID == "" {
|
|
return nil, fmt.Errorf("entityType and entityID are required")
|
|
}
|
|
|
|
var locales []string
|
|
err := r.db.WithContext(ctx).Model(&domain.Localization{}).
|
|
Where("entity_type = ? AND entity_id = ?", entityType, entityID).
|
|
Distinct("locale").
|
|
Pluck("locale", &locales).Error
|
|
|
|
return locales, err
|
|
}
|
|
|
|
// BulkCreate creates multiple localization records in a transaction
|
|
func (r *LocalizationRepository) BulkCreate(ctx context.Context, localizations []*domain.Localization) error {
|
|
if len(localizations) == 0 {
|
|
return nil
|
|
}
|
|
|
|
// Validate all localizations before starting transaction
|
|
for i, loc := range localizations {
|
|
if loc == nil {
|
|
return fmt.Errorf("localization at index %d cannot be nil", i)
|
|
}
|
|
if loc.EntityType == "" || loc.EntityID == "" || loc.Field == "" || loc.Locale == "" {
|
|
return fmt.Errorf("localization at index %d has missing required fields", i)
|
|
}
|
|
// Generate composite ID if not provided
|
|
if loc.ID == "" {
|
|
loc.ID = fmt.Sprintf("%s_%s_%s_%s", loc.EntityType, loc.EntityID, loc.Field, loc.Locale)
|
|
}
|
|
}
|
|
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
for _, loc := range localizations {
|
|
if err := tx.Create(loc).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// BulkDelete deletes multiple localization records by their IDs in a transaction
|
|
func (r *LocalizationRepository) BulkDelete(ctx context.Context, ids []string) error {
|
|
if len(ids) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
|
return tx.Where("id IN ?", ids).Delete(&domain.Localization{}).Error
|
|
})
|
|
}
|
|
|
|
// SearchLocalizations searches localizations by value content
|
|
func (r *LocalizationRepository) SearchLocalizations(ctx context.Context, query string, locale string, limit int) ([]*domain.Localization, error) {
|
|
if query == "" {
|
|
return nil, fmt.Errorf("search query cannot be empty")
|
|
}
|
|
|
|
var localizations []*domain.Localization
|
|
db := r.db.WithContext(ctx)
|
|
|
|
if locale != "" {
|
|
db = db.Where("locale = ?", locale)
|
|
}
|
|
|
|
err := db.Where("value ILIKE ?", "%"+query+"%").
|
|
Order("entity_type, entity_id, field").
|
|
Limit(limit).
|
|
Find(&localizations).Error
|
|
|
|
return localizations, err
|
|
}
|
|
|
|
// GetTranslationCountsByEntity returns translation counts grouped by entity type and locale
|
|
func (r *LocalizationRepository) GetTranslationCountsByEntity(ctx context.Context) (map[string]map[string]int, error) {
|
|
var results []struct {
|
|
EntityType string
|
|
Locale string
|
|
Count int
|
|
}
|
|
|
|
err := r.db.WithContext(ctx).Model(&domain.Localization{}).
|
|
Select("entity_type, locale, COUNT(*) as count").
|
|
Group("entity_type, locale").
|
|
Order("entity_type, locale").
|
|
Scan(&results).Error
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
counts := make(map[string]map[string]int)
|
|
for _, result := range results {
|
|
if counts[result.EntityType] == nil {
|
|
counts[result.EntityType] = make(map[string]int)
|
|
}
|
|
counts[result.EntityType][result.Locale] = result.Count
|
|
}
|
|
|
|
return counts, nil
|
|
}
|
|
|
|
// GetUntranslatedFields returns fields that exist in entities but don't have translations
|
|
func (r *LocalizationRepository) GetUntranslatedFields(ctx context.Context, entityType, targetLocale string, entityIDs []string) ([]UntranslatedFieldInfo, error) {
|
|
// This would require joining with the actual entity tables
|
|
// For now, return empty - this is a complex query that would need to be implemented
|
|
// based on the specific entity schema
|
|
return []UntranslatedFieldInfo{}, nil
|
|
}
|
|
|
|
// UntranslatedFieldInfo represents information about a field that needs translation
|
|
type UntranslatedFieldInfo struct {
|
|
EntityID string
|
|
Field string
|
|
RussianValue string
|
|
}
|
|
|
|
// GetTranslationReuseCandidates finds Russian text that appears in multiple entities
|
|
// This can help identify good candidates for caching
|
|
func (r *LocalizationRepository) GetTranslationReuseCandidates(ctx context.Context, entityType, field, locale string) ([]domain.ReuseCandidate, error) {
|
|
var results []struct {
|
|
RussianValue string
|
|
Count int
|
|
}
|
|
|
|
// Find Russian values that appear in multiple entities of the same type
|
|
err := r.db.WithContext(ctx).Model(&domain.Localization{}).
|
|
Select("value as russian_value, COUNT(DISTINCT entity_id) as count").
|
|
Where("entity_type = ? AND field = ? AND locale = 'ru'", entityType, field).
|
|
Group("value").
|
|
Having("COUNT(DISTINCT entity_id) > 1").
|
|
Order("count DESC").
|
|
Limit(50).
|
|
Scan(&results).Error
|
|
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
candidates := make([]domain.ReuseCandidate, len(results))
|
|
for i, result := range results {
|
|
candidates[i] = domain.ReuseCandidate{
|
|
RussianValue: result.RussianValue,
|
|
EntityCount: result.Count,
|
|
}
|
|
}
|
|
|
|
return candidates, nil
|
|
}
|
|
|
|
// GetEntitiesNeedingTranslation returns entity IDs that need translation for specific fields
|
|
// It queries entity tables to find entities with Russian content that don't have translations
|
|
func (r *LocalizationRepository) GetEntitiesNeedingTranslation(ctx context.Context, entityType, field, targetLocale string, limit int) ([]string, error) {
|
|
if entityType == "" || field == "" || targetLocale == "" {
|
|
return nil, fmt.Errorf("entityType, field, and targetLocale are required")
|
|
}
|
|
|
|
if limit <= 0 {
|
|
limit = 100
|
|
}
|
|
if limit > 1000 {
|
|
limit = 1000
|
|
}
|
|
|
|
// Get entity descriptor from registry to know table name
|
|
desc, exists := localization.GetEntityDescriptor(entityType)
|
|
if !exists {
|
|
return nil, fmt.Errorf("unknown entity type: %s", entityType)
|
|
}
|
|
|
|
// Build query to find entities that:
|
|
// 1. Have non-empty Russian content in the specified field
|
|
// 2. Don't have a translation in the target locale
|
|
// This uses a LEFT JOIN to find missing translations
|
|
// We need to use GORM's Raw with proper parameterization
|
|
// The field name needs to be validated against the entity descriptor
|
|
fieldValid := false
|
|
for _, f := range desc.Fields {
|
|
if f == field {
|
|
fieldValid = true
|
|
break
|
|
}
|
|
}
|
|
if !fieldValid {
|
|
return nil, fmt.Errorf("field '%s' is not valid for entity type '%s'", field, entityType)
|
|
}
|
|
|
|
// Use parameterized query with proper escaping for table/column names
|
|
// Note: Table and column names cannot be parameterized, so we validate them above
|
|
// GORM uses ? placeholders for parameters
|
|
query := fmt.Sprintf(`
|
|
SELECT DISTINCT e.%s as entity_id
|
|
FROM %s e
|
|
LEFT JOIN localizations l
|
|
ON l.entity_type = ?
|
|
AND l.entity_id = e.%s
|
|
AND l.field = ?
|
|
AND l.locale = ?
|
|
WHERE e.%s IS NOT NULL
|
|
AND e.%s != ''
|
|
AND l.id IS NULL
|
|
LIMIT ?
|
|
`, desc.IDField, desc.TableName, desc.IDField, field, field)
|
|
|
|
var entityIDs []string
|
|
err := r.db.WithContext(ctx).Raw(query, entityType, field, targetLocale, limit).Pluck("entity_id", &entityIDs).Error
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query entities needing translation: %w", err)
|
|
}
|
|
|
|
return entityIDs, nil
|
|
}
|
|
|
|
// GetEntityFieldValue retrieves the raw value of a field from the entity table for a given entity ID
|
|
func (r *LocalizationRepository) GetEntityFieldValue(ctx context.Context, entityType, entityID, field string) (string, error) {
|
|
if entityType == "" || entityID == "" || field == "" {
|
|
return "", fmt.Errorf("entityType, entityID and field are required")
|
|
}
|
|
|
|
// Get entity descriptor to know table and id field
|
|
desc, exists := localization.GetEntityDescriptor(entityType)
|
|
if !exists {
|
|
return "", fmt.Errorf("unknown entity type: %s", entityType)
|
|
}
|
|
|
|
// Validate field
|
|
fieldValid := false
|
|
for _, f := range desc.Fields {
|
|
if f == field {
|
|
fieldValid = true
|
|
break
|
|
}
|
|
}
|
|
if !fieldValid {
|
|
return "", fmt.Errorf("field '%s' is not valid for entity type '%s'", field, entityType)
|
|
}
|
|
|
|
query := fmt.Sprintf(`SELECT e.%s FROM %s e WHERE e.%s = ? LIMIT 1`, field, desc.TableName, desc.IDField)
|
|
var value string
|
|
if err := r.db.WithContext(ctx).Raw(query, entityID).Scan(&value).Error; err != nil {
|
|
return "", fmt.Errorf("failed to get field value: %w", err)
|
|
}
|
|
|
|
return value, nil
|
|
}
|
|
|
|
// FindExistingTranslationByRussianText finds an existing translation by matching Russian source text
|
|
// It looks for entities with the same Russian text in the same field that already have a translation
|
|
// Returns the translation value if found, empty string if not found
|
|
func (r *LocalizationRepository) FindExistingTranslationByRussianText(ctx context.Context, entityType, field, targetLocale, russianText string) (string, error) {
|
|
if entityType == "" || field == "" || targetLocale == "" || russianText == "" {
|
|
return "", nil
|
|
}
|
|
|
|
// Normalize the Russian text for matching
|
|
normalized := strings.TrimSpace(russianText)
|
|
if normalized == "" {
|
|
return "", nil
|
|
}
|
|
|
|
// Query: Find a localization where:
|
|
// 1. The entity has a Russian localization with matching text
|
|
// 2. The same entity has a translation in the target locale
|
|
var translation string
|
|
err := r.db.WithContext(ctx).Raw(`
|
|
SELECT l_target.value
|
|
FROM localizations l_ru
|
|
INNER JOIN localizations l_target
|
|
ON l_ru.entity_type = l_target.entity_type
|
|
AND l_ru.entity_id = l_target.entity_id
|
|
AND l_ru.field = l_target.field
|
|
WHERE l_ru.entity_type = ?
|
|
AND l_ru.field = ?
|
|
AND l_ru.locale = 'ru'
|
|
AND l_ru.value = ?
|
|
AND l_target.locale = ?
|
|
LIMIT 1
|
|
`, entityType, field, normalized, targetLocale).Scan(&translation).Error
|
|
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to find existing translation: %w", err)
|
|
}
|
|
|
|
return translation, nil
|
|
}
|