gotrans

package module
v1.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Mar 25, 2026 License: MIT Imports: 7 Imported by: 0

README

gotrans

Lightweight, framework-agnostic translation module for Go applications. Manage multi-language content directly within your backend business logic.

Key Features

  • Embedded Locale: Each entity carries its own locale information
  • Explicit Entity Naming: Define entity names explicitly via interface method (with optional reflection fallback)
  • Automatic Optimization: Translations grouped by locale for efficient batch operations
  • Type Safe: Uses Go generics for compile-time type checking
  • Explicit Field Mapping: Clear association between struct fields and translation field IDs
  • Framework Agnostic: Works with MySQL, SQLite, PostgreSQL, and any database supported by sqlx
  • 41 Supported Languages: Complete ISO-639-1 locale support
  • Zero Dependencies: Only requires sqlx for database operations
  • Cache Statistics: Monitor cache performance with real-time hit/miss tracking
  • Context Timeout Support: Automatic operation timeouts to prevent hangs
  • Configurable Batch Processing: Efficiently handle large datasets with automatic batching

Installation

go get github.com/ivan-gorbushko/gotrans

Quick Start

1. Define Your Entity
type Product struct {
    ID          int
    locale      gotrans.Locale  // Private field, accessed via method
    Title       string
    Description string
}

// Implement Translatable interface
func (p Product) TranslationEntityLocale() gotrans.Locale {
    return p.locale
}

func (p Product) TranslationEntityID() int {
    return p.ID
}

func (p Product) TranslatableFields() map[string]string {
    return map[string]string{
        "Title":       "title",
        "Description": "description",
    }
}

func (p Product) TranslationEntityName() string {
    return "product"  // Explicit entity name used in database
}
2. Setup Repository and Translator
import (
    "github.com/ivan-gorbushko/gotrans"
    "github.com/ivan-gorbushko/gotrans/mysql"
    "github.com/jmoiron/sqlx"
)

db := sqlx.Open("mysql", "user:password@tcp(localhost:3306)/dbname")
repo := mysql.NewTranslationRepository(db)
translator := gotrans.NewTranslator[Product](repo)
3. Save Translations
products := []Product{
    {ID: 1, locale: gotrans.LocaleEN, Title: "Apple", Description: "Fresh fruit"},
    {ID: 2, locale: gotrans.LocaleEN, Title: "Banana", Description: "Yellow fruit"},
}
err := translator.SaveTranslations(ctx, products)
4. Load Translations
products := []Product{
    {ID: 1, locale: gotrans.LocaleEN},
    {ID: 2, locale: gotrans.LocaleEN},
}
products, err := translator.LoadTranslations(ctx, products)
fmt.Printf("Product 1: %s - %s\n", products[0].Title, products[0].Description)
// Output: Product 1: Apple - Fresh fruit
5. Delete Translations
// Delete specific fields for specific locale
err := translator.DeleteTranslations(ctx, gotrans.LocaleEN, "product", 
    []int{1, 2}, []string{"title", "description"})

// Delete all translations for entities (all locales)
err := translator.DeleteTranslationsByEntity(ctx, "product", []int{1, 2})

How It Works

Translatable Interface

Every translatable entity must implement four methods:

type Translatable interface {
    // TranslationEntityLocale returns the language for this entity
    TranslationEntityLocale() gotrans.Locale
    
    // TranslationEntityID returns the unique identifier
    TranslationEntityID() int
    
    // TranslatableFields returns struct field to database field mapping
    // Key: struct field name (e.g., "Title")
    // Value: database field ID (e.g., "title")
    TranslatableFields() map[string]string
    
    // TranslationEntityName returns the entity name as stored in database
    // Example: "product", "geo_tag", "order_item"
    TranslationEntityName() string
}

The field mapping separates struct naming (PascalCase) from database naming conventions:

func (p Product) TranslatableFields() map[string]string {
    return map[string]string{
        "Title":            "title",              // struct field -> DB field
        "Description":     "description",
        "AIRecommendation": "ai_recommendation",  // map to any db column name
    }
}
Entity Name Resolution

You have full control over entity naming via TranslationEntityName() method. This separates your Go code naming from database naming:

type Product struct { ... }
func (p Product) TranslationEntityName() string { return "product" }

type GeoTag struct { ... }
func (g GeoTag) TranslationEntityName() string { return "geo_tag" }

type OrderItem struct { ... }
func (o OrderItem) TranslationEntityName() string { return "order_item" }

Reflection Helper (Optional)

If you want automatic snake_case conversion from struct names, use the provided helpers:

// Instead of manual implementation:
func (p Product) TranslationEntityName() string {
    return "product"
}

// You can use the reflection helper:
func (p Product) TranslationEntityName() string {
    return gotrans.GetEntityNameFromType(&p)
    // Returns "product" (auto-converted from "Product")
}

Available helpers:

  • GetEntityNameFromType[T any](t *T) string - For pointer types
  • GetEntityNameFromValue(v any) string - For any value
Translator Interface
type Translator[T Translatable] interface {
    // LoadTranslations fetches translations and populates string fields
    LoadTranslations(ctx context.Context, entities []T) ([]T, error)
    
    // SaveTranslations persists translations (creates or updates)
    SaveTranslations(ctx context.Context, entities []T) error
    
    // DeleteTranslations removes specific translations
    DeleteTranslations(ctx context.Context, locale Locale, entity string, 
        entityIDs []int, fields []string) error
    
    // DeleteTranslationsByEntity removes all translations for entities
    DeleteTranslationsByEntity(ctx context.Context, entity string, 
        entityIDs []int) error
}

Automatic Optimization

When you work with multiple locales, the translator automatically groups translations by locale for efficient batch operations:

products := []Product{
    {ID: 1, locale: gotrans.LocaleEN, Title: "Apple"},
    {ID: 1, locale: gotrans.LocaleFR, Title: "Pomme"},
    {ID: 2, locale: gotrans.LocaleEN, Title: "Banana"},
    {ID: 2, locale: gotrans.LocaleFR, Title: "Banane"},
}

// Automatically makes 2 DB calls (grouped by locale)
// Instead of 4 individual calls
translator.SaveTranslations(ctx, products)
Performance Metrics
Scenario Database Calls Improvement
100 entities, 1 locale 1 100x faster
100 entities, 2 locales 2 50x faster
100 entities, 5 locales 5 20x faster
1000 entities, 10 locales 10 100x faster

Database Schema

CREATE TABLE IF NOT EXISTS translations (
    id BIGINT AUTO_INCREMENT,
    entity VARCHAR(100) NOT NULL,
    entity_id BIGINT NOT NULL,
    field VARCHAR(100) NOT NULL,
    locale VARCHAR(10) NOT NULL,
    value TEXT NOT NULL,
    PRIMARY KEY (id),
    UNIQUE KEY uniq_translation (entity, entity_id, field, locale)
)
COLLATE = utf8mb4_unicode_ci;

Fields:

  • entity: Entity type name (as returned by TranslationEntityName())
  • entity_id: Entity's primary key
  • field: Translatable field ID (from your mapping)
  • locale: ISO-639-1 language code
  • value: Translated text

Unique Constraint: Ensures no duplicate translations for the same entity, field, and locale.

Supported Locales

The library includes 41 language locales:

gotrans.LocaleEN    // English
gotrans.LocaleFR    // French
gotrans.LocaleDE    // German
gotrans.LocaleES    // Spanish
gotrans.LocaleIT    // Italian
gotrans.LocaleRU    // Russian
gotrans.LocaleJA    // Japanese
gotrans.LocaleZH    // Chinese
gotrans.LocaleKO    // Korean
gotrans.LocaleAR    // Arabic
// ... and 31 more

Use gotrans.ParseLocale(code) to convert string codes to Locale constants:

locale, ok := gotrans.ParseLocale("en")
if ok {
    fmt.Println(locale == gotrans.LocaleEN) // true
}

Multi-Locale Operations

Handle multiple languages in a single operation:

// Load English version
productsEN := []Product{
    {ID: 1, locale: gotrans.LocaleEN},
    {ID: 2, locale: gotrans.LocaleEN},
}
productsEN, _ = translator.LoadTranslations(ctx, productsEN)

// Load French version
productsFR := []Product{
    {ID: 1, locale: gotrans.LocaleFR},
    {ID: 2, locale: gotrans.LocaleFR},
}
productsFR, _ = translator.LoadTranslations(ctx, productsFR)

Or load mixed locales in one call:

mixed := []Product{
    {ID: 1, locale: gotrans.LocaleEN},
    {ID: 1, locale: gotrans.LocaleFR},
    {ID: 2, locale: gotrans.LocaleEN},
}
mixed, _ := translator.LoadTranslations(ctx, mixed)
// Automatically optimized: 2 queries instead of 3

Example Application

Run a complete working example with SQLite:

go run ./example/main.go

The example demonstrates:

  • Creating tables in SQLite
  • Saving translations for multiple locales
  • Loading translations
  • Deleting translations
  • Multi-locale optimization in action

Testing

Run all tests:

go test -v ./...

All tests pass including:

  • Single locale operations
  • Multi-locale operations
  • Deletion operations
  • Locale parsing

Use Cases

E-commerce Platforms
type Product struct {
    ID          int
    locale      gotrans.Locale
    Name        string
    Description string
    Details     string
}

func (p Product) TranslationEntityName() string { return "product" }
CMS/Blog Systems
type Article struct {
    ID       int
    locale   gotrans.Locale
    Title    string
    Content  string
    Excerpt  string
}

func (a Article) TranslationEntityName() string { return "article" }
SaaS Applications
type Feature struct {
    ID          int
    locale      gotrans.Locale
    Name        string
    Description string
}

func (f Feature) TranslationEntityName() string { return "feature" }

Caching

Caching is an optional, opt-in layer. The main Translator and TranslationRepository interfaces are not affected — caching is added by wrapping the repository with NewCachedRepository or NewCachedRepositoryInMemory.

Built-in In-Memory Cache
import "time"

repo := mysql.NewTranslationRepository(db)

cachedRepo := gotrans.NewCachedRepositoryInMemory(repo, gotrans.CacheOptions{
    TTL: 5 * time.Minute,  // 0 means entries never expire
})

translator := gotrans.NewTranslator[Product](cachedRepo)

Everything else stays the same. Cache invalidation is automatic on save and delete.

Custom Cache Backend (Redis, etc.)

Implement the TranslationCache interface to plug in any backend:

type TranslationCache interface {
    Get(key string) ([]Translation, bool)
    Set(key string, value []Translation, ttl time.Duration)
    Delete(keys ...string)
    Clear()
}
type RedisCache struct { client *redis.Client }

func (r *RedisCache) Get(key string) ([]gotrans.Translation, bool)                    { /* ... */ }
func (r *RedisCache) Set(key string, v []gotrans.Translation, ttl time.Duration)       { /* ... */ }
func (r *RedisCache) Delete(keys ...string)                                            { /* ... */ }
func (r *RedisCache) Clear()                                                           { /* ... */ }

// Wire it up
cachedRepo := gotrans.NewCachedRepository(repo, &RedisCache{client}, gotrans.CacheOptions{
    TTL: 10 * time.Minute,
})
translator := gotrans.NewTranslator[Product](cachedRepo)
Cache Invalidation

Invalidation is automatic and transparent:

Operation Invalidated entries
SaveTranslations All cache keys for affected entities + locale
DeleteTranslations Cache keys for specified locale + entity IDs
DeleteTranslationsByEntity All locale variants for specified entity IDs
Cache Behaviour Details
  • Cache-aside pattern: per entity ID, so a batch of 100 entities with 90 already cached triggers only 10 DB rows.
  • Empty results are cached: if an entity has no translations, the empty result is cached to avoid repeated DB hits.
  • Cross-locale invalidation: DeleteTranslationsByEntity correctly evicts every locale's entry for those entities using an internal entity index — without scanning the whole cache.

Best Practices

  1. Make Locale Field Private - Use lowercase for locale field and expose via TranslationEntityLocale() method
  2. Implement TranslationEntityName() - Return the exact entity name as stored in your database
  3. Use Field Mapping Consistently - Keep mapping aligned between struct fields and database
  4. Leverage Batch Operations - Pass multiple entities to save/load for better performance
  5. Handle Missing Translations - Check if fields are empty after loading
  6. Use Transactions - Wrap multiple save operations in database transactions for consistency

Implementation Details

Reflection Usage

The library uses reflection only where necessary:

  • During SaveTranslations(): To extract string field values
  • During LoadTranslations(): To apply fetched translations to struct fields
  • In GetEntityNameFromType() helper: Optional reflection-based entity name

This is acceptable because:

  • These operations are not in hot loops (typically called per request/batch)
  • Performance impact is negligible compared to database I/O
  • It provides flexibility and type safety
Type Safety

The library uses Go generics to ensure compile-time type checking:

translator := gotrans.NewTranslator[Product](repo)
// Only Product entities can be used with this translator
// Compile-time error if you try to use other types

Advanced Features

Cache Statistics

Monitor cache performance in production:

cache := gotrans.NewInMemoryCache()
cachedRepo := gotrans.NewCachedRepository(repo, cache, gotrans.CacheOptions{
    TTL: 5 * time.Minute,
})

// Perform operations...

stats := cache.Stats()
hitRate := float64(stats.Hits) / float64(stats.Hits + stats.Misses) * 100
fmt.Printf("Cache Hit Rate: %.1f%%\n", hitRate)
Context Timeout Support

Prevent operations from hanging indefinitely:

translator := gotrans.NewTranslatorWithOptions(gotrans.TranslatorOptions[Product]{
    Repository: repo,
    DefaultContextTimeout: 30 * time.Second,
})

// Operations automatically timeout after 30 seconds if not completed
products, err := translator.LoadTranslations(context.Background(), items)
Batch Processing

Efficiently handle large datasets:

cachedRepo := gotrans.NewCachedRepository(repo, cache, gotrans.CacheOptions{
    TTL: 5 * time.Minute,
    BatchSize: 500,  // Process 500 IDs per database query
})

// Large queries automatically split into batches
// Loading 5000 items = 10 queries of 500 items each

Examples

Complete working examples demonstrating all features:

License

MIT License - See LICENSE file for details

Documentation

Index

Constants

This section is empty.

Variables

View Source
var ErrEmptyEntityName = errors.New("entity name cannot be empty")

ErrEmptyEntityName is returned when an entity name is empty.

Functions

This section is empty.

Types

type CacheOptions added in v1.1.0

type CacheOptions struct {
	// TTL defines how long a cached entry remains valid.
	// Zero means entries never expire.
	TTL time.Duration

	// BatchSize defines the maximum number of IDs to fetch in a single query.
	// Default is 1000 if not set or set to 0.
	BatchSize int

	// DefaultContextTimeout specifies default timeout for cache operations.
	// Zero means no timeout.
	DefaultContextTimeout time.Duration
}

CacheOptions configures the caching behaviour of a cached repository.

type CacheStats added in v1.2.0

type CacheStats struct {
	Hits        int64
	Misses      int64
	Sets        int64
	Deletes     int64
	LastCleared time.Time
}

CacheStats provides statistics about cache performance.

type InMemoryCache added in v1.1.0

type InMemoryCache struct {
	// contains filtered or unexported fields
}

InMemoryCache is a thread-safe in-memory implementation of TranslationCache. It is the default backend used by NewCachedRepositoryInMemory.

func NewInMemoryCache added in v1.1.0

func NewInMemoryCache() *InMemoryCache

NewInMemoryCache returns a ready-to-use in-memory cache.

func (*InMemoryCache) Clear added in v1.1.0

func (c *InMemoryCache) Clear()

func (*InMemoryCache) Delete added in v1.1.0

func (c *InMemoryCache) Delete(keys ...string)

func (*InMemoryCache) Get added in v1.1.0

func (c *InMemoryCache) Get(key string) ([]Translation, bool)

func (*InMemoryCache) ResetStats added in v1.2.0

func (c *InMemoryCache) ResetStats()

ResetStats resets cache statistics to zero.

func (*InMemoryCache) Set added in v1.1.0

func (c *InMemoryCache) Set(key string, value []Translation, ttl time.Duration)

func (*InMemoryCache) Stats added in v1.2.0

func (c *InMemoryCache) Stats() CacheStats

Stats returns cache statistics.

type Locale

type Locale int16
const (
	LocaleNone Locale = iota
	LocaleSQ          // Albanian
	LocaleAR          // Arabic
	LocaleAZ          // Azerbaijani
	LocaleBS          // Bosnian
	LocaleBG          // Bulgarian
	LocaleZH          // Chinese
	LocaleHR          // Croatian
	LocaleCS          // Czech
	LocaleDA          // Danish
	LocaleNL          // Dutch
	LocaleEN          // English
	LocaleET          // Estonian
	LocaleFI          // Finnish
	LocaleFR          // French
	LocaleKA          // Georgian
	LocaleDE          // German
	LocaleEL          // Greek
	LocaleHE          // Hebrew
	LocaleHU          // Hungarian
	LocaleID          // Indonesian
	LocaleJA          // Japanese
	LocaleKK          // Kazakh
	LocaleKO          // Korean
	LocaleLV          // Latvian
	LocaleLT          // Lithuanian
	LocaleMK          // Macedonian
	LocaleNO          // Norwegian
	LocalePL          // Polish
	LocalePT          // Portuguese
	LocaleRO          // Romanian
	LocaleRU          // Russian
	LocaleSR          // Serbian
	LocaleSK          // Slovak
	LocaleSL          // Slovenian
	LocaleES          // Spanish
	LocaleSV          // Swedish
	LocaleTH          // Thai
	LocaleTR          // Turkish
	LocaleUK          // Ukrainian
	LocaleVI          // Vietnamese
	LocaleIT          // Italian
)

func AllLocales added in v1.2.0

func AllLocales() []Locale

AllLocales returns all supported locales in an unspecified order, excluding LocaleNone.

func ParseLocale

func ParseLocale(code string) (Locale, bool)

ParseLocale returns a Locale enum from a language code (ISO-639-1). Returns (LocaleNone, false) for unknown codes.

func ParseLocaleList

func ParseLocaleList(list string) []Locale

ParseLocaleList converts "en,ru,uk" into []Locale.

func (Locale) Code

func (l Locale) Code() string

Code returns the ISO-639-1 code for a language.

func (Locale) Name

func (l Locale) Name() string

Name returns the human-readable language name.

func (Locale) String

func (l Locale) String() string

String returns the ISO-639-1 code, or "none" for LocaleNone.

type Translatable added in v1.1.0

type Translatable interface {
	TranslationEntityID() int
	TranslationEntityName() string
	TranslationEntityLocale() Locale
	TranslatableFields() map[string]string
}

Translatable is the interface every translatable entity must implement. TranslatableFields returns a map: struct field name → translation field ID in DB. Example: map[string]string{"Title": "title", "Description": "desc"} TranslationEntityName returns the name stored in the translations table.

type Translation

type Translation struct {
	// ID is the database primary key (omitted for inserts).
	ID int
	// Entity is the entity type name (e.g., "product", "parameter").
	Entity string
	// EntityID is the unique identifier of the entity instance.
	EntityID int
	// Field is the database field identifier (e.g., "title", "description").
	Field string
	// Locale is the language variant for this translation.
	Locale Locale
	// Value contains the translated text.
	Value string
}

Translation represents a translated field value for a specific entity and locale. ID is typically auto-incremented by the database and may be omitted for insert operations. Entity is the type name (e.g., "product"), EntityID identifies the specific entity instance. Field maps to the translatable field name (e.g., "title", "description"). Locale specifies the language variant. Value contains the translated text.

type TranslationCache added in v1.1.0

type TranslationCache interface {
	// Get retrieves cached translations. Returns the slice and true on a hit.
	Get(key string) ([]Translation, bool)

	// Set stores translations under the given key. A zero TTL means no expiration.
	Set(key string, value []Translation, ttl time.Duration)

	// Delete removes one or more entries by key.
	Delete(keys ...string)

	// Clear removes all entries from the cache.
	Clear()

	// Stats returns cache statistics (hits, misses, etc).
	Stats() CacheStats

	// ResetStats resets cache statistics to zero.
	ResetStats()
}

TranslationCache is the interface for pluggable cache backends. Implementations can be in-memory, Redis, Memcached, or any custom store.

type TranslationRepository

type TranslationRepository interface {
	GetTranslations(
		ctx context.Context,
		locale Locale,
		entity string,
		entityIDs []int,
	) ([]Translation, error)

	MassDelete(
		ctx context.Context,
		locale Locale,
		entity string,
		entityIDs []int,
		fields []string,
	) error

	MassCreateOrUpdate(
		ctx context.Context,
		locale Locale,
		translations []Translation,
	) error
}

func NewCachedRepository added in v1.1.0

func NewCachedRepository(repo TranslationRepository, cache TranslationCache, opts CacheOptions) TranslationRepository

NewCachedRepository wraps repo with the provided cache backend. Use this when you want to supply your own cache implementation (e.g. Redis).

cache := gotrans.NewInMemoryCache()
cachedRepo := gotrans.NewCachedRepository(repo, cache, gotrans.CacheOptions{TTL: 5 * time.Minute})
translator := gotrans.NewTranslator[Product](cachedRepo)

func NewCachedRepositoryInMemory added in v1.1.0

func NewCachedRepositoryInMemory(repo TranslationRepository, opts CacheOptions) TranslationRepository

NewCachedRepositoryInMemory wraps repo with the built-in in-memory cache. This is the simplest way to add caching with no external dependencies.

cachedRepo := gotrans.NewCachedRepositoryInMemory(repo, gotrans.CacheOptions{TTL: 5 * time.Minute})
translator := gotrans.NewTranslator[Product](cachedRepo)

type Translator

type Translator[T Translatable] interface {
	LoadTranslations(ctx context.Context, entities []T) ([]T, error)
	SaveTranslations(ctx context.Context, entities []T) error
	// DeleteTranslations removes translations for specific entity IDs, locale and fields.
	DeleteTranslations(ctx context.Context, locale Locale, entityIDs []int, fields []string) error
	// DeleteTranslationsByEntity removes all translations for the given entity IDs across all locales.
	DeleteTranslationsByEntity(ctx context.Context, entityIDs []int) error
}

Translator is the main interface for translation operations. The entity name is derived from T at construction, so it does not need to be passed to the delete methods — the translator already knows it.

func NewTranslator

func NewTranslator[T Translatable](repo TranslationRepository) Translator[T]

NewTranslator creates a translator for entity type T. The entity name and field index are resolved once from a zero value of T.

func NewTranslatorWithOptions added in v1.2.0

func NewTranslatorWithOptions[T Translatable](opts TranslatorOptions[T]) Translator[T]

NewTranslatorWithOptions creates a translator with advanced options including default context timeout. Use this when you want to set a default timeout for operations.

trans := NewTranslatorWithOptions(TranslatorOptions[Product]{
	Repository: repo,
	DefaultContextTimeout: 30 * time.Second,
})

type TranslatorOptions added in v1.2.0

type TranslatorOptions[T Translatable] struct {
	// Repository is the translation repository (required).
	Repository TranslationRepository

	// DefaultContextTimeout specifies a default timeout for translation operations.
	// If set to a positive value, contexts without a deadline will be wrapped with this timeout.
	// Zero means no default timeout is applied.
	DefaultContextTimeout time.Duration
}

TranslatorOptions provides configuration options for a Translator.

Directories

Path Synopsis
example
advanced command
basic command
caching command
error-handling command
performance command

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL