Skip to content

Translation

Comprehensive internationalization (i18n) system for multi-language applications with flexible field translation

Overview

Base Framework's translation system provides a flexible, database-driven internationalization solution. It supports both static text translation and dynamic field translation at the model level, allowing you to create truly multi-language applications.

Key Features

  • Database-Driven - Store translations directly in your database with automatic loading and caching for optimal performance
  • Field-Level Translation - Translate specific model fields with automatic fallback to original values when translations are missing
  • Bulk Operations - Efficiently update multiple translations at once with transaction-based bulk operations
  • REST API - Complete REST API for managing translations with filtering, pagination, and language-specific endpoints

Translation Field Type

  • Custom Field type that handles multiple languages automatically
  • JSON marshaling that adapts based on available translations
  • Database storage with original value preservation
  • Automatic translation loading and caching

Module Integration

  • Translatable interface for modules to define translated fields
  • Helper functions for retrieving module translations
  • Automatic response enrichment with translated content
  • Service layer integration for seamless usage

Advanced Operations

  • Bulk translation updates with transaction support
  • Language discovery and supported language listing
  • Model-specific translation management
  • Translation history and versioning support

CLI Generation

Use the Base CLI to automatically generate models with translation fields. The CLI handles all the boilerplate code for you, including automatic translation loading in service methods.

Generate Model with Translation Fields

bash
# Generate a Post model with translatable title field
./cmd g Post title:translation desc:text feat:image

# Generate a Product model with translatable fields
./cmd g Product name:translation description:translation price:float category_id:uint

# Generate a Page model with multiple translatable fields
./cmd g Page title:translation content:translation meta_title:translation meta_description:translation slug:string

Generated Model Structure

When you use title:translation, the CLI automatically generates:

go
type Post struct {
    Id        uint                `json:"id" gorm:"primarykey"`
    CreatedAt time.Time           `json:"created_at"`
    UpdatedAt time.Time           `json:"updated_at"`
    DeletedAt gorm.DeletedAt      `json:"deleted_at" gorm:"index"`
    Title     translation.Field   `json:"title"`  // Translatable field
    Desc      string              `json:"desc"`
    FeatId    uint                `json:"feat_id,omitempty"`
    Feat      *storage.Attachment `json:"feat,omitempty"`
}

// Request types use string for input
type CreatePostRequest struct {
    Title string `json:"title" binding:"required"`
    Desc  string `json:"desc"`
}

// Response types return translation.Field with full translation object
type PostResponse struct {
    Id        uint              `json:"id"`
    CreatedAt time.Time         `json:"created_at"`
    UpdatedAt time.Time         `json:"updated_at"`
    DeletedAt gorm.DeletedAt    `json:"deleted_at"`
    Title     translation.Field `json:"title"`  // Returns translation object
    Desc      string            `json:"desc"`
}

Automatic Service Integration

The CLI automatically generates service methods that load translations:

go
func (s *PostService) GetById(id uint) (*models.Post, error) {
    var item models.Post
    if err := s.DB.Preload(clause.Associations).First(&item, id).Error; err != nil {
        return nil, err
    }
    
    // Automatically load translations for translation fields
    s.loadTranslationsForItem(&item)
    return &item, nil
}

func (s *PostService) loadTranslationsForItem(item *models.Post) error {
    if item == nil {
        return nil
    }
    
    modelName := item.GetModelName()
    modelId := item.GetId()
    
    // Load translations for Title field
    if err := s.TranslationHelper.Service.LoadTranslationsForField(&item.Title, modelName, modelId, "title"); err != nil {
        s.Logger.Error("Failed to load translations for Title", logger.String("error", err.Error()))
    }
    
    return nil
}

Translation Field Usage

Model Definition

go
package models

import (
    "base/core/translation"
    "time"
    "gorm.io/gorm"
)

type Post struct {
    ID          uint                   `gorm:"primarykey"`
    CreatedAt   time.Time
    UpdatedAt   time.Time
    DeletedAt   gorm.DeletedAt         `gorm:"index"`
    
    // Translatable fields
    Title       translation.Field      `gorm:"type:text"`
    Content     translation.Field      `gorm:"type:text"`
    Description translation.Field      `gorm:"type:text"`
    
    // Regular fields
    AuthorID    uint                   `gorm:"not null"`
    Published   bool                   `gorm:"default:false"`
    ViewCount   int                    `gorm:"default:0"`
}

Working with Translation Fields

go
// Create a new post with original text
post := &Post{
    Title:       translation.NewField("Hello World"),
    Content:     translation.NewField("This is the original content"),
    Description: translation.NewField("Original description"),
}

// Set translations for different languages
post.Title.SetTranslation("es", "Hola Mundo")
post.Title.SetTranslation("fr", "Bonjour le Monde")

// Get translation for specific language with fallback
titleInSpanish := post.Title.GetTranslationOrOriginal("es")  // Returns: "Hola Mundo"
titleInGerman := post.Title.GetTranslationOrOriginal("de")   // Returns: "Hello World" (fallback)

// Check if translation exists
hasSpanish := post.Title.HasTranslation("es")  // Returns: true
hasGerman := post.Title.HasTranslation("de")   // Returns: false

// Get all available languages for a field
languages := post.Title.GetAvailableLanguages()  // Returns: ["es", "fr"]

Service Integration

Generated Service Pattern

go
// All generated services follow this pattern
type YourEntityService struct {
    DB                *gorm.DB
    Emitter           *emitter.Emitter
    Storage           *storage.ActiveStorage
    Logger            logger.Logger
    TranslationHelper *translation.Helper  // Automatically injected
}

func (s *YourEntityService) Create(req *models.CreateYourEntityRequest) (*models.YourEntity, error) {
    item := &models.YourEntity{
        // For translation fields, use translation.NewField()
        TranslatableField: translation.NewField(req.TranslatableField),
        // For regular fields, use directly
        RegularField: req.RegularField,
    }
    
    if err := s.DB.Create(item).Error; err != nil {
        return nil, err
    }
    
    return s.GetById(item.Id)  // Automatically loads translations
}

func (s *YourEntityService) GetById(id uint) (*models.YourEntity, error) {
    item := &models.YourEntity{}
    if err := s.DB.First(item, id).Error; err != nil {
        return nil, err
    }
    
    // Automatically load translations for all translatable fields
    if err := s.loadTranslationsForItem(item); err != nil {
        s.Logger.Error("Failed to load translations", logger.String("error", err.Error()))
    }
    
    return item, nil
}

Loading Translations

go
// This method is automatically generated for each entity
func (s *YourEntityService) loadTranslationsForItem(item *models.YourEntity) error {
    if item == nil {
        return nil
    }
    
    modelName := item.GetModelName()
    modelId := item.GetId()
    
    // Load translations for each translatable field
    // This loops through all fields marked with 'translation' type
    if err := s.TranslationHelper.Service.LoadTranslationsForField(&item.TranslatableField1, modelName, modelId, "translatable_field1"); err != nil {
        s.Logger.Error("Failed to load translations", logger.String("error", err.Error()))
    }
    if err := s.TranslationHelper.Service.LoadTranslationsForField(&item.TranslatableField2, modelName, modelId, "translatable_field2"); err != nil {
        s.Logger.Error("Failed to load translations", logger.String("error", err.Error()))
    }
    
    return nil
}

// Using the Translation Helper directly
func (s *YourEntityService) AddTranslation(id uint, field, language, value string) error {
    modelName := "your_entity"  // matches your model name
    return s.TranslationHelper.SetTranslation(modelName, id, field, value, language)
}

// Bulk set multiple translations
func (s *YourEntityService) BulkAddTranslations(id uint, language string, translations map[string]string) error {
    modelName := "your_entity"
    return s.TranslationHelper.BulkSetTranslations(modelName, id, language, translations)
}

Module Integration

Generated Module Pattern

go
// All generated modules follow this pattern
func Init(deps module.Dependencies) module.Module {
    // Translation service and helper are automatically created
    translationService := translation.NewTranslationService(deps.DB, deps.Emitter, deps.Storage, deps.Logger)
    translationHelper := translation.NewHelper(translationService)
    
    // Service gets translation helper injected
    service := NewYourEntityService(deps.DB, deps.Emitter, deps.Storage, deps.Logger, translationHelper)
    controller := NewYourEntityController(service, deps.Storage)
    
    mod := &Module{
        DB:                deps.DB,
        Service:           service,
        Controller:        controller,
        TranslationHelper: translationHelper,
    }
    
    return mod
}

// Module structure includes translation helper
type Module struct {
    DB                *gorm.DB
    Service           *YourEntityService
    Controller        *YourEntityController
    TranslationHelper *translation.Helper
}

Using Translation Helper

go
// Frontend usage - accessing translation values
// From API response: {"title": {"original": "Title", "de": "Titel", "sq": "Titulli"}}

// Get specific language or fallback to original
function getTranslation(field, language) {
    return field[language] || field.original;
}

// Usage examples
const title_de = getTranslation(post.title, "de");  // "Titel"
const title_en = getTranslation(post.title, "en");  // "Title" (fallback to original)

// Backend helper usage
helper := translation.NewHelper(translationService)

// Set a single translation
err = helper.SetTranslation("your_entity", 1, "field_name", "Translated Value", "de")

// Bulk set multiple translations
translationMap := map[string]string{
    "title": "Deutscher Titel",
    "description": "Deutsche Beschreibung",
}
err = helper.BulkSetTranslations("your_entity", 1, "de", translationMap)

API Reference

Translation Endpoints

MethodEndpointDescription
GET/translationsList all translations with pagination and filtering
POST/translationsCreate a new translation
GET/translations/by-id/{id}Get a specific translation by ID
PUT/translations/by-id/{id}Update an existing translation
DELETE/translations/by-id/{id}Delete a translation
POST/translations/bulkBulk update multiple translations
GET/translations/languagesGet supported languages
GET/translations/models/{model}/{id}Get all translations for a model

API Request Examples

Create Translation

bash
POST /api/translations
Content-Type: application/json

{
    "key": "title",
    "value": "Hola Mundo",
    "model": "post",
    "model_id": 1,
    "language": "es"
}

Bulk Update Translations

bash
POST /api/translations/bulk
Content-Type: application/json

{
    "model": "post",
    "model_id": 1,
    "language": "es",
    "translations": {
        "title": "Título en Español",
        "content": "Contenido en Español",
        "description": "Descripción en Español"
    }
}

Get Model Translations

bash
GET /api/translations/models/post/1/es

# List with Filters
GET /api/translations?model=post&model_id=1&page=1&limit=20

JSON Response Format

Adaptive JSON Marshaling

json
// When no translations are loaded (original value only)
{
    "title": "Hello World",
    "content": "This is the original content"
}

// When translations are loaded (translation map)
{
    "title": {
        "es": "Hola Mundo",
        "fr": "Bonjour le Monde",
        "de": "Hallo Welt"
    },
    "content": {
        "es": "Este es el contenido original",
        "fr": "Ceci est le contenu original"
    }
}

Actual API Response

GET /api/posts/1

json
{
    "id": 1,
    "created_at": "2025-08-28T20:37:28.986523+02:00",
    "updated_at": "2025-08-28T20:37:28.986523+02:00",
    "deleted_at": null,
    "title": {
        "original": "Title",
        "de": "Titel",
        "sq": "Titulli"
    },
    "desc": "description"
}

Best Practices

Do's

  • Always provide meaningful original values in your default language
  • Use standard language codes (ISO 639-1) like "en", "es", "fr"
  • Implement fallback logic to handle missing translations gracefully
  • Use bulk operations for updating multiple translations efficiently
  • Implement the Translatable interface for modules with translated fields
  • Load translations only when needed to optimize performance
  • Use transactions when updating multiple translations together

Considerations

  • Translation fields store original values in the main table for performance
  • Translations are stored separately and loaded on demand
  • Consider caching strategies for frequently accessed translations
  • Plan your database indexing strategy for translation queries
  • Be mindful of character limits and text expansion in translations

Don'ts

  • Don't rely solely on translations without original fallbacks
  • Don't use translation fields for data that doesn't need localization
  • Don't forget to handle cases where translations might be empty
  • Don't load all translations when only specific languages are needed
  • Don't hardcode language codes in your application logic
  • Don't forget to clean up orphaned translations when deleting records

Advanced Usage

Querying with Translations

go
// Search in original values (faster)
var posts []Post
db.Where("title LIKE ?", "%search%").Find(&posts)

// Search in translations (requires join)
db.Joins("LEFT JOIN translations t ON t.model = 'post' AND t.model_id = posts.id").
   Where("t.key = 'title' AND t.language = 'es' AND t.value LIKE ?", "%búsqueda%").
   Find(&posts)

// Get posts with specific translation availability
db.Joins("INNER JOIN translations t ON t.model = 'post' AND t.model_id = posts.id").
   Where("t.language = ?", "es").
   Distinct("posts.*").
   Find(&posts)

// Count translations by language
type LanguageCount struct {
    Language string
    Count    int64
}

var counts []LanguageCount
db.Model(&Translation{}).
   Select("language, count(*) as count").
   Where("model = ?", "post").
   Group("language").
   Find(&counts)

Migration and Setup

sql
-- The Translation module automatically creates this table:
CREATE TABLE translations (
    id SERIAL PRIMARY KEY,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL,
    deleted_at TIMESTAMP,
    `key` VARCHAR(255) NOT NULL,
    value TEXT NOT NULL,
    model VARCHAR(255) NOT NULL,
    model_id INTEGER NOT NULL,
    language VARCHAR(5) NOT NULL,
    INDEX idx_translation_lookup (`key`, model, model_id, language),
    INDEX idx_deleted_at (deleted_at)
);
go
// Register the translation module in app/init.go
func RegisterModules(deps module.Dependencies) error {
    // Core modules
    translationModule := translation.NewTranslationModule(
        deps.DB, deps.Router, deps.Logger, deps.Emitter, deps.Storage,
    )
    module.RegisterModule("translation", translationModule)
    
    // Your app modules...
    return nil
}

Released under the MIT License.