turash/bugulma/backend/internal/handler/i18n_handler.go

445 lines
14 KiB
Go

package handler
import (
"bugulma/backend/internal/service"
"fmt"
"net/http"
"github.com/gin-gonic/gin"
)
// I18nHandler handles i18n API requests
type I18nHandler struct {
i18nService *service.I18nService
}
// NewI18nHandler creates a new i18n handler
func NewI18nHandler(i18nService *service.I18nService) *I18nHandler {
return &I18nHandler{
i18nService: i18nService,
}
}
// GetUITranslations returns UI translations for a locale
// @Summary Get UI translations
// @Tags i18n
// @Produce json
// @Param locale path string true "Locale code (en, tt)"
// @Success 200 {object} map[string]string
// @Router /api/i18n/ui/{locale} [get]
func (h *I18nHandler) GetUITranslations(c *gin.Context) {
locale := c.Param("locale")
if !h.i18nService.ValidateLocale(locale) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid locale"})
return
}
ctx := c.Request.Context()
// Get all UI translations for this locale from the database
// UI translations are stored with entityType="ui", entityID="ui"
localizations, err := h.i18nService.GetUITranslationsForLocale(ctx, locale)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to load UI translations: %v", err)})
return
}
c.JSON(http.StatusOK, gin.H{
"locale": locale,
"translations": localizations,
})
}
// GetDataTranslation returns a data translation
// @Summary Get data translation
// @Tags i18n
// @Produce json
// @Param entityType path string true "Entity type"
// @Param entityID path string true "Entity ID"
// @Param field path string true "Field name"
// @Param locale query string true "Locale code"
// @Success 200 {object} map[string]string
// @Router /api/i18n/data/{entityType}/{entityID}/{field} [get]
func (h *I18nHandler) GetDataTranslation(c *gin.Context) {
entityType := c.Param("entityType")
entityID := c.Param("entityID")
field := c.Param("field")
locale := c.Query("locale")
if locale == "" {
locale = service.DefaultLocale
}
if !h.i18nService.ValidateLocale(locale) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid locale"})
return
}
// GetDataTranslation returns only a string (no error), so we need to handle it differently
// For now, we'll need the source text as fallback - this should come from the entity
// In a real scenario, you'd fetch the entity first to get the source text
translated := h.i18nService.GetDataTranslation(
c.Request.Context(),
entityType,
entityID,
field,
locale,
"", // Fallback would need to be provided from entity source
)
c.JSON(http.StatusOK, gin.H{
"entity_type": entityType,
"entity_id": entityID,
"field": field,
"locale": locale,
"value": translated,
})
}
// GetSupportedLocales returns all supported locales
// @Summary Get supported locales
// @Tags i18n
// @Produce json
// @Success 200 {object} map[string]interface{}
// @Router /api/i18n/locales [get]
func (h *I18nHandler) GetSupportedLocales(c *gin.Context) {
locales, err := h.i18nService.GetSupportedLocales(c.Request.Context())
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"locales": locales,
"default_locale": service.DefaultLocale,
"supported": service.SupportedLocales,
})
}
// GetTranslationStats returns translation statistics
// @Summary Get translation statistics
// @Tags i18n
// @Produce json
// @Param entityType query string false "Entity type filter"
// @Success 200 {object} map[string]interface{}
// @Router /api/i18n/stats [get]
func (h *I18nHandler) GetTranslationStats(c *gin.Context) {
entityType := c.Query("entityType")
stats, err := h.i18nService.GetTranslationStats(c.Request.Context(), entityType)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, stats)
}
// Admin endpoints for i18n management
// UpdateUITranslation updates a UI translation (admin only)
// @Summary Update UI translation
// @Tags admin
// @Accept json
// @Produce json
// @Param locale path string true "Locale code"
// @Param key path string true "Translation key"
// @Param request body UpdateUITranslationRequest true "Translation value"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/i18n/ui/:locale/:key [put]
func (h *I18nHandler) UpdateUITranslation(c *gin.Context) {
locale := c.Param("locale")
key := c.Param("key")
if !h.i18nService.ValidateLocale(locale) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid locale"})
return
}
var req UpdateUITranslationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.i18nService.SetUITranslation(c.Request.Context(), key, locale, req.Value); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Translation updated"})
}
type UpdateUITranslationRequest struct {
Value string `json:"value" binding:"required"`
}
// BulkUpdateUITranslations updates multiple UI translations (admin only)
// @Summary Bulk update UI translations
// @Tags admin
// @Accept json
// @Produce json
// @Param request body BulkUpdateUITranslationsRequest true "Bulk update request"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/i18n/ui/bulk-update [post]
func (h *I18nHandler) BulkUpdateUITranslations(c *gin.Context) {
var req BulkUpdateUITranslationsRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
updates := make(map[string]map[string]string) // locale -> key -> value
for _, update := range req.Updates {
if updates[update.Locale] == nil {
updates[update.Locale] = make(map[string]string)
}
updates[update.Locale][update.Key] = update.Value
}
for locale, keys := range updates {
if !h.i18nService.ValidateLocale(locale) {
c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("Invalid locale: %s", locale)})
return
}
for key, value := range keys {
if err := h.i18nService.SetUITranslation(c.Request.Context(), key, locale, value); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": fmt.Sprintf("Failed to update %s:%s: %v", locale, key, err)})
return
}
}
}
c.JSON(http.StatusOK, gin.H{"message": "Translations updated"})
}
type BulkUpdateUITranslationsRequest struct {
Updates []TranslationUpdate `json:"updates" binding:"required"`
}
type TranslationUpdate struct {
Locale string `json:"locale" binding:"required"`
Key string `json:"key" binding:"required"`
Value string `json:"value" binding:"required"`
}
// AutoTranslateMissing auto-translates missing UI keys (admin only)
// @Summary Auto-translate missing keys
// @Tags admin
// @Accept json
// @Produce json
// @Param request body AutoTranslateRequest true "Auto-translate request"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/admin/i18n/ui/auto-translate [post]
func (h *I18nHandler) AutoTranslateMissing(c *gin.Context) {
var req AutoTranslateRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if !h.i18nService.ValidateLocale(req.TargetLocale) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid target locale"})
return
}
// Get all UI keys for source locale
sourceTranslations, err := h.i18nService.GetUITranslationsForLocale(c.Request.Context(), req.SourceLocale)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Get existing target translations
targetTranslations, err := h.i18nService.GetUITranslationsForLocale(c.Request.Context(), req.TargetLocale)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Find missing keys
var keysToTranslate []string
for key := range sourceTranslations {
if _, exists := targetTranslations[key]; !exists {
keysToTranslate = append(keysToTranslate, key)
}
}
if len(keysToTranslate) == 0 {
c.JSON(http.StatusOK, gin.H{"message": "No missing translations", "translated": 0})
return
}
// Batch translate
translated, err := h.i18nService.BatchTranslateUI(c.Request.Context(), keysToTranslate, req.SourceLocale, req.TargetLocale)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "Translations completed",
"translated": len(translated),
"results": translated,
})
}
type AutoTranslateRequest struct {
SourceLocale string `json:"sourceLocale" binding:"required"`
TargetLocale string `json:"targetLocale" binding:"required"`
}
// GetTranslationKeys returns all translation keys with status (admin only)
// @Summary Get translation keys with status
// @Tags admin
// @Produce json
// @Param locale path string true "Locale code"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/admin/i18n/ui/:locale/keys [get]
func (h *I18nHandler) GetTranslationKeys(c *gin.Context) {
locale := c.Param("locale")
if !h.i18nService.ValidateLocale(locale) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid locale"})
return
}
// Get source locale (default: ru)
sourceLocale := service.DefaultLocale
sourceTranslations, err := h.i18nService.GetUITranslationsForLocale(c.Request.Context(), sourceLocale)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Get target locale translations
targetTranslations, err := h.i18nService.GetUITranslationsForLocale(c.Request.Context(), locale)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// Build keys with status
keys := make([]map[string]interface{}, 0)
for key, sourceValue := range sourceTranslations {
status := "missing"
value := ""
if targetValue, exists := targetTranslations[key]; exists {
status = "translated"
value = targetValue
}
keys = append(keys, map[string]interface{}{
"key": key,
"source": sourceValue,
"value": value,
"status": status,
})
}
c.JSON(http.StatusOK, gin.H{
"locale": locale,
"keys": keys,
"total": len(keys),
"translated": len(targetTranslations),
"missing": len(sourceTranslations) - len(targetTranslations),
})
}
// UpdateDataTranslation updates a data translation (admin only)
// @Summary Update data translation
// @Tags admin
// @Accept json
// @Produce json
// @Param entityType path string true "Entity type"
// @Param entityID path string true "Entity ID"
// @Param field path string true "Field name"
// @Param locale path string true "Locale code"
// @Param request body UpdateDataTranslationRequest true "Translation value"
// @Success 200 {object} map[string]string
// @Router /api/v1/admin/i18n/data/:entityType/:entityID/:field/:locale [put]
func (h *I18nHandler) UpdateDataTranslation(c *gin.Context) {
entityType := c.Param("entityType")
entityID := c.Param("entityID")
field := c.Param("field")
locale := c.Param("locale")
if !h.i18nService.ValidateLocale(locale) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid locale"})
return
}
var req UpdateDataTranslationRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := h.i18nService.SetDataTranslation(c.Request.Context(), entityType, entityID, field, locale, req.Value); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "Translation updated"})
}
type UpdateDataTranslationRequest struct {
Value string `json:"value" binding:"required"`
}
// BulkTranslateData bulk translates entities (admin only)
// @Summary Bulk translate data
// @Tags admin
// @Accept json
// @Produce json
// @Param request body BulkTranslateDataRequest true "Bulk translate request"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/admin/i18n/data/bulk-translate [post]
func (h *I18nHandler) BulkTranslateData(c *gin.Context) {
var req BulkTranslateDataRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if !h.i18nService.ValidateLocale(req.TargetLocale) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid target locale"})
return
}
// TODO: Implement bulk data translation
c.JSON(http.StatusNotImplemented, gin.H{"error": "Bulk data translation not yet implemented"})
}
type BulkTranslateDataRequest struct {
EntityType string `json:"entityType" binding:"required"`
EntityIDs []string `json:"entityIDs" binding:"required"`
TargetLocale string `json:"targetLocale" binding:"required"`
Fields []string `json:"fields"`
}
// GetMissingTranslations returns entities with missing translations (admin only)
// @Summary Get entities with missing translations
// @Tags admin
// @Produce json
// @Param entityType path string true "Entity type"
// @Param locale query string true "Locale code"
// @Success 200 {object} map[string]interface{}
// @Router /api/v1/admin/i18n/data/:entityType/missing [get]
func (h *I18nHandler) GetMissingTranslations(c *gin.Context) {
_ = c.Param("entityType") // Entity type - for future use
locale := c.Query("locale")
if locale == "" {
locale = service.DefaultLocale
}
if !h.i18nService.ValidateLocale(locale) {
c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid locale"})
return
}
// TODO: Implement missing translations detection
c.JSON(http.StatusNotImplemented, gin.H{"error": "Missing translations detection not yet implemented"})
}