mirror of
https://github.com/SamyRai/turash.git
synced 2025-12-26 23:01:33 +00:00
315 lines
11 KiB
Go
315 lines
11 KiB
Go
package handler
|
|
|
|
import (
|
|
"bugulma/backend/internal/domain"
|
|
"bugulma/backend/internal/service"
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
type inMemoryLocalizationService struct {
|
|
data map[string]string
|
|
}
|
|
|
|
func newInMemoryLocalizationService() *inMemoryLocalizationService {
|
|
return &inMemoryLocalizationService{data: map[string]string{}}
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) key(entityType, entityID, field, locale string) string {
|
|
return entityType + ":" + entityID + ":" + field + ":" + locale
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) GetLocalizedValue(entityType, entityID, field, locale string) (string, error) {
|
|
return s.data[s.key(entityType, entityID, field, locale)], nil
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) SetLocalizedValue(entityType, entityID, field, locale, value string) error {
|
|
s.data[s.key(entityType, entityID, field, locale)] = value
|
|
return nil
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) GetAllLocalizedValues(entityType, entityID string) (map[string]map[string]string, error) {
|
|
return map[string]map[string]string{}, nil
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) GetLocalizedEntity(entityType, entityID, locale string) (map[string]string, error) {
|
|
return map[string]string{}, nil
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) GetSupportedLocalesForEntity(entityType, entityID string) ([]string, error) {
|
|
return []string{"ru", "en", "tt"}, nil
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) DeleteLocalizedValue(entityType, entityID, field, locale string) error {
|
|
delete(s.data, s.key(entityType, entityID, field, locale))
|
|
return nil
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) BulkSetLocalizedValues(entityType, entityID string, values map[string]map[string]string) error {
|
|
for field, locales := range values {
|
|
for locale, value := range locales {
|
|
s.data[s.key(entityType, entityID, field, locale)] = value
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) GetAllLocales() ([]string, error) {
|
|
return []string{"ru", "en", "tt"}, nil
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) SearchLocalizations(query, locale string, limit int) ([]*domain.Localization, error) {
|
|
return []*domain.Localization{}, nil
|
|
}
|
|
|
|
func (s *inMemoryLocalizationService) ApplyLocalizationToEntity(entity domain.Localizable, locale string) error {
|
|
return nil
|
|
}
|
|
|
|
type inMemoryLocalizationRepo struct {
|
|
entityFields map[string]map[string]map[string]string // entityType -> entityID -> field -> ruValue
|
|
locValues map[string]string // entityType:entityID:field:locale -> value
|
|
}
|
|
|
|
func newInMemoryLocalizationRepo() *inMemoryLocalizationRepo {
|
|
return &inMemoryLocalizationRepo{
|
|
entityFields: map[string]map[string]map[string]string{},
|
|
locValues: map[string]string{},
|
|
}
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) locKey(entityType, entityID, field, locale string) string {
|
|
return entityType + ":" + entityID + ":" + field + ":" + locale
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) Create(ctx context.Context, loc *domain.Localization) error {
|
|
r.locValues[r.locKey(loc.EntityType, loc.EntityID, loc.Field, loc.Locale)] = loc.Value
|
|
return nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) GetByEntityAndField(ctx context.Context, entityType, entityID, field, locale string) (*domain.Localization, error) {
|
|
if v, ok := r.locValues[r.locKey(entityType, entityID, field, locale)]; ok {
|
|
return &domain.Localization{EntityType: entityType, EntityID: entityID, Field: field, Locale: locale, Value: v}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) GetAllByEntity(ctx context.Context, entityType, entityID string) ([]*domain.Localization, error) {
|
|
return []*domain.Localization{}, nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) Update(ctx context.Context, loc *domain.Localization) error {
|
|
r.locValues[r.locKey(loc.EntityType, loc.EntityID, loc.Field, loc.Locale)] = loc.Value
|
|
return nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) Delete(ctx context.Context, id string) error { return nil }
|
|
|
|
func (r *inMemoryLocalizationRepo) GetByEntityTypeAndLocale(ctx context.Context, entityType, locale string) ([]*domain.Localization, error) {
|
|
return []*domain.Localization{}, nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) GetAllLocales(ctx context.Context) ([]string, error) {
|
|
return []string{"ru", "en", "tt"}, nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) GetSupportedLocalesForEntity(ctx context.Context, entityType, entityID string) ([]string, error) {
|
|
return []string{"ru", "en", "tt"}, nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) BulkCreate(ctx context.Context, localizations []*domain.Localization) error {
|
|
for _, loc := range localizations {
|
|
_ = r.Create(ctx, loc)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) BulkDelete(ctx context.Context, ids []string) error { return nil }
|
|
|
|
func (r *inMemoryLocalizationRepo) SearchLocalizations(ctx context.Context, query string, locale string, limit int) ([]*domain.Localization, error) {
|
|
return []*domain.Localization{}, nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) GetTranslationReuseCandidates(ctx context.Context, entityType, field, locale string) ([]domain.ReuseCandidate, error) {
|
|
return []domain.ReuseCandidate{}, nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) GetEntitiesNeedingTranslation(ctx context.Context, entityType, field, targetLocale string, limit int) ([]string, error) {
|
|
ids := []string{}
|
|
entities := r.entityFields[entityType]
|
|
for entityID, fields := range entities {
|
|
ruValue := fields[field]
|
|
if ruValue == "" {
|
|
continue
|
|
}
|
|
if r.locValues[r.locKey(entityType, entityID, field, targetLocale)] == "" {
|
|
ids = append(ids, entityID)
|
|
if limit > 0 && len(ids) >= limit {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
return ids, nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) FindExistingTranslationByRussianText(ctx context.Context, entityType, field, targetLocale, russianText string) (string, error) {
|
|
return "", nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) GetTranslationCountsByEntity(ctx context.Context) (map[string]map[string]int, error) {
|
|
return map[string]map[string]int{}, nil
|
|
}
|
|
|
|
func (r *inMemoryLocalizationRepo) GetEntityFieldValue(ctx context.Context, entityType, entityID, field string) (string, error) {
|
|
if r.entityFields[entityType] == nil || r.entityFields[entityType][entityID] == nil {
|
|
return "", nil
|
|
}
|
|
return r.entityFields[entityType][entityID][field], nil
|
|
}
|
|
|
|
func TestI18nData_GetMissingTranslations_InvalidLocale(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
repo := newInMemoryLocalizationRepo()
|
|
locSvc := newInMemoryLocalizationService()
|
|
cacheSvc := service.NewTranslationCacheService(repo, locSvc)
|
|
translationSvc := service.NewTranslationService("http://localhost:0", "")
|
|
i18nSvc := service.NewI18nService(locSvc, repo, translationSvc, cacheSvc)
|
|
|
|
h := NewI18nHandler(i18nSvc)
|
|
r := gin.New()
|
|
r.GET("/api/v1/admin/i18n/data/:entityType/missing", h.GetMissingTranslations)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/i18n/data/organization/missing?locale=xx", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusBadRequest {
|
|
t.Fatalf("expected 400, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestI18nData_GetMissingTranslations_Success(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
repo := newInMemoryLocalizationRepo()
|
|
repo.entityFields["organization"] = map[string]map[string]string{
|
|
"org-1": {"name": "Имя 1", "description": "Описание 1"},
|
|
"org-2": {"name": "Имя 2", "description": "Описание 2"},
|
|
}
|
|
// Pretend org-1 name already translated
|
|
repo.locValues[repo.locKey("organization", "org-1", "name", "en")] = "Name 1"
|
|
|
|
locSvc := newInMemoryLocalizationService()
|
|
cacheSvc := service.NewTranslationCacheService(repo, locSvc)
|
|
translationSvc := service.NewTranslationService("http://localhost:0", "")
|
|
i18nSvc := service.NewI18nService(locSvc, repo, translationSvc, cacheSvc)
|
|
|
|
h := NewI18nHandler(i18nSvc)
|
|
r := gin.New()
|
|
r.GET("/api/v1/admin/i18n/data/:entityType/missing", h.GetMissingTranslations)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodGet, "/api/v1/admin/i18n/data/organization/missing?locale=en&fields=name,description&limit=100", nil)
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Total int `json:"total"`
|
|
Counts map[string]int `json:"counts"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
// name missing only for org-2, description missing for both
|
|
if resp.Counts["name"] != 1 {
|
|
t.Fatalf("expected name missing=1, got %d", resp.Counts["name"])
|
|
}
|
|
if resp.Counts["description"] != 2 {
|
|
t.Fatalf("expected description missing=2, got %d", resp.Counts["description"])
|
|
}
|
|
if resp.Total != 3 {
|
|
t.Fatalf("expected total=3, got %d", resp.Total)
|
|
}
|
|
}
|
|
|
|
func TestI18nData_BulkTranslateData_Success(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
// Fake Ollama server
|
|
ollama := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path != "/api/generate" {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "application/json")
|
|
_ = json.NewEncoder(w).Encode(service.TranslationResponse{
|
|
Model: "test",
|
|
Response: "TRANSLATED",
|
|
Done: true,
|
|
})
|
|
}))
|
|
defer ollama.Close()
|
|
|
|
repo := newInMemoryLocalizationRepo()
|
|
repo.entityFields["organization"] = map[string]map[string]string{
|
|
"org-1": {"name": "Имя 1"},
|
|
}
|
|
|
|
locSvc := newInMemoryLocalizationService()
|
|
cacheSvc := service.NewTranslationCacheService(repo, locSvc)
|
|
translationSvc := service.NewTranslationService(ollama.URL, "")
|
|
i18nSvc := service.NewI18nService(locSvc, repo, translationSvc, cacheSvc)
|
|
|
|
h := NewI18nHandler(i18nSvc)
|
|
r := gin.New()
|
|
r.POST("/api/v1/admin/i18n/data/bulk-translate", h.BulkTranslateData)
|
|
|
|
payload := map[string]any{
|
|
"entityType": "organization",
|
|
"entityIDs": []string{"org-1"},
|
|
"targetLocale": "en",
|
|
"fields": []string{"name"},
|
|
}
|
|
b, _ := json.Marshal(payload)
|
|
|
|
w := httptest.NewRecorder()
|
|
req := httptest.NewRequest(http.MethodPost, "/api/v1/admin/i18n/data/bulk-translate", bytes.NewReader(b))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp struct {
|
|
Translated int `json:"translated"`
|
|
Results map[string]map[string]string `json:"results"`
|
|
}
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("failed to parse response: %v", err)
|
|
}
|
|
|
|
if resp.Translated != 1 {
|
|
t.Fatalf("expected translated=1, got %d", resp.Translated)
|
|
}
|
|
if resp.Results["org-1"]["name"] != "TRANSLATED" {
|
|
t.Fatalf("expected result TRANSLATED, got %q", resp.Results["org-1"]["name"])
|
|
}
|
|
|
|
// Also persisted via LocalizationService
|
|
if v, _ := locSvc.GetLocalizedValue("organization", "org-1", "name", "en"); v != "TRANSLATED" {
|
|
t.Fatalf("expected persisted TRANSLATED, got %q", v)
|
|
}
|
|
}
|