translation

package
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 26, 2026 License: MIT Imports: 9 Imported by: 0

README

translation

Type-safe, declarative translation of per-record model content using struct tags. Distinct from i18n, which localizes static catalog and error strings — translation translates data stored per row (a story title, a widget description) into the request language.

translation/                core: Translator, Store, Config, Translatable, parser, MockStore
├── postgres/               Postgres implementation of Store (+ Schema/EnsureSchema)
└── translationrest/        rest.MidFunc that auto-translates responses

How it works

  • Mark translatable fields with translate:"..." tags (like json/db/validate).
  • The model implements Translatable to expose its (modelName, keyID).
  • The middleware resolves the request language and translates the response in place — handlers stay translation-agnostic.

Canonical content lives in your normal tables in the default language; translations for other languages are stored generically in the translations table keyed by (model_name, column_name, key_id, language_id).

1. Declare a model

type Story struct {
    ID          string `json:"id"          translate:"primary"`
    Title       string `json:"title"       translate:"title"`
    Description string `json:"description" translate:"description"`
    AuthorID    int    `json:"author_id"` // untagged -> never translated
}

func (s *Story) GetTranslationKey() (modelName, keyID string) {
    return "stories", s.ID
}

Only primary is required. Every other tagged field must be a string.

2. Build the translator

store := postgres.NewStore(log, db) // db is *sqlx.DB
tr, err := translation.New(translation.Config{
    Store:           store,
    DefaultLanguage: translation.LanguageRu,
    SupportedLangs:  []translation.Language{translation.LanguageRu, translation.LanguageKk},
})

// Tests / simple boot: create the table under an advisory lock. In production
// prefer a migration (goose serializes those itself).
_ = store.EnsureSchema(ctx)

The predefined LanguageRu/LanguageKk/LanguageEn are conveniences; declare your own translation.Language{Code, Name} for any other language.

3. Automatic translation (middleware)

Install one app middleware; handlers just return their DTO:

r := router.New(errorMapper, translationrest.Middleware(log, tr))

For a response to translate, its rest.ResponseEncoder must expose its model(s):

// Single model: the DTO is both Translatable and a rest.ResponseEncoder.
func (h *Handler) get(ctx context.Context, r *http.Request) rest.ResponseEncoder {
    return h.toAppStory(story) // *Story, implements Encode()
}

// Collection: wrap items and expose them via TranslatableList for one batch query.
type StoryList struct{ Items []*Story `json:"items"` }
func (l StoryList) Translatables() []translation.Translatable {
    out := make([]translation.Translatable, len(l.Items))
    for i, s := range l.Items { out[i] = s }
    return out
}
func (l StoryList) Encode() ([]byte, string, error) { /* json */ }

Language is taken from X-Language, then Accept-Language, then the translator default. Translating to the default language is a no-op; translation errors are logged and the original content is served (never fails the request).

4. Managing translations (CRUD)

// Save a Kazakh translation (upsert per column):
tr.Save(ctx, translation.LanguageKk, &Story{ID: "123", Title: "Қазақша атауы"})

// Read translations into a model:
tr.Get(ctx, translation.LanguageKk, &Story{ID: "123"})

// Delete:
tr.Delete(ctx, translation.LanguageKk, &Story{ID: "123"})

5. Manual translation

// Single model:
tr.Translate(ctx, story, translation.LanguageKk)

// Batch (one query per model type), type-safe:
translation.TranslateSlice(ctx, tr, stories, translation.LanguageKk)

Validation

// Tags are well-formed (one primary, string fields):
tr.ValidateTranslateTags(&Story{})

// All non-default languages have every translatable column (e.g. before publish):
tr.CheckTranslationsExist(ctx, &Story{ID: "123", Title: "x", Description: "y"})

Testing

tr, _ := translation.New(translation.Config{
    Store:           translation.NewMockStore(),
    DefaultLanguage: translation.LanguageRu,
    SupportedLangs:  []translation.Language{translation.LanguageRu, translation.LanguageKk},
})
tr.Save(ctx, translation.LanguageKk, &Story{ID: "123", Title: "Тест"})

Notes & limitations

  • Translatable fields must be strings; the parser rejects other kinds with ErrInvalidTag.
  • Nested structs are walked for tagged fields; empty nested structs contribute nothing. Slice fields of Translatable are translated recursively.
  • Get/batch lookups return ErrTranslationNotFound only from Get; the middleware path treats a missing translation as "keep original".
  • Language resolution overlaps with i18n; if you use both, let one middleware set the request language and have the other read it.

Documentation

Overview

Package translation provides a type-safe, declarative system for translating model content stored per-record (distinct from i18n, which localizes static catalog/error strings).

Translatable fields are marked with struct tags, similar to json/db/validate. Each model implements the Translatable interface to expose its model name and instance key. The Translator reads and writes translations through a Store; the Postgres implementation lives in the postgres subpackage, and an in-memory MockStore is provided for tests.

Declaring a model

type Story struct {
    ID          string `json:"id"          translate:"primary"`
    Title       string `json:"title"       translate:"title"`
    Description string `json:"description" translate:"description"`
}

func (s *Story) GetTranslationKey() (modelName, keyID string) {
    return "stories", s.ID
}

Only the primary key field is required; every other tagged field must be a string. Fields without a translate tag are ignored.

Setup

store := postgres.NewStore(log, db)
tr, err := translation.New(translation.Config{
    Store:           store,
    DefaultLanguage: translation.LanguageRu,
    SupportedLangs:  []translation.Language{translation.LanguageRu, translation.LanguageKk},
})

Automatic translation in REST

The translationrest subpackage installs one middleware that resolves the request language and translates the response in place, so handlers only return their DTO:

r := router.New(errorMapper, translationrest.Middleware(log, tr))

A response is translated when its rest.ResponseEncoder is a Translatable (single model) or a TranslatableList (collection — translated in one batch query). Translating to the default language is a no-op.

Manual use

// Single model:
err := tr.Translate(ctx, story, translation.LanguageKk)
// Batch, type-safe:
err := translation.TranslateSlice(ctx, tr, stories, translation.LanguageKk)
// CRUD on the stored translations:
err := tr.Save(ctx, translation.LanguageKk, story)
err := tr.Get(ctx, translation.LanguageKk, story)
err := tr.Delete(ctx, translation.LanguageKk, story)

Testing

tr, _ := translation.New(translation.Config{Store: translation.NewMockStore(), ...})

Index

Constants

This section is empty.

Variables

View Source
var (
	// ErrTranslationNotFound is returned when a translation is not found
	ErrTranslationNotFound = errors.New("translation not found")

	// ErrInvalidLanguage is returned when an invalid language code is provided
	ErrInvalidLanguage = errors.New("invalid language")

	// ErrMissingTranslations is returned when required translations are missing
	ErrMissingTranslations = errors.New("missing required translations")

	// ErrInvalidTag is returned when a translate tag is malformed
	ErrInvalidTag = errors.New("invalid translate tag")

	// ErrNoPrimaryKey is returned when no primary key field is found
	ErrNoPrimaryKey = errors.New("no primary key field found with translate:\"primary\" tag")

	// ErrMultiplePrimaryKeys is returned when multiple primary key fields are found
	ErrMultiplePrimaryKeys = errors.New("multiple primary key fields found")

	// ErrInvalidModel is returned when the model doesn't implement Translatable
	ErrInvalidModel = errors.New("model must implement Translatable interface")
)
View Source
var (
	LanguageRu = Language{Code: "ru", Name: "Russian"}
	LanguageKk = Language{Code: "kk", Name: "Kazakh"}
	LanguageEn = Language{Code: "en", Name: "English"}
)

Predefined languages offered as a convenience; applications are free to declare their own. The SDK does not assume any particular language set.

Functions

func SetLanguageInContext

func SetLanguageInContext(ctx context.Context, lang Language) context.Context

SetLanguageInContext returns a child context carrying lang.

func TranslateSlice

func TranslateSlice[T Translatable](ctx context.Context, t *Translator, models []T, lang Language) error

TranslateSlice translates a slice of models with type safety using generics

Types

type Config

type Config struct {
	// Store is the translation storage backend (e.g. the postgres subpackage).
	Store Store

	// DefaultLanguage is the language the canonical content is authored in.
	// Translating a model to the default language is a no-op.
	DefaultLanguage Language

	// SupportedLangs is the set of languages the application serves. It must
	// include DefaultLanguage.
	SupportedLangs []Language
}

Config holds configuration for the Translator.

func (*Config) Validate

func (c *Config) Validate() error

Validate checks that the configuration is usable.

type Data

type Data struct {
	ModelName string
	KeyID     string
	Language  Language
	Columns   map[string]string // column_name -> translation_value
	CreatedAt time.Time
	UpdatedAt time.Time
}

Data is a unit of translation to persist for one model instance in one language: a set of column -> value pairs.

type Language

type Language struct {
	Code string `json:"code"` // BCP-47 / ISO 639-1 code, e.g. "ru", "kk", "en"
	Name string `json:"name"` // human-readable name, e.g. "Russian"
}

Language identifies a language by its code (and an optional display name).

func LanguageFromContext

func LanguageFromContext(ctx context.Context) Language

LanguageFromContext returns the language stored by the middleware. The zero Language is returned when none is set; callers that need a concrete default should compare against Translator.DefaultLanguage.

func (Language) String

func (l Language) String() string

String returns the language code.

type MockStore

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

MockStore is a mock implementation of Store for testing

func NewMockStore

func NewMockStore() *MockStore

NewMockStore creates a new mock store

func (*MockStore) CheckTranslationsExist

func (m *MockStore) CheckTranslationsExist(ctx context.Context, modelName, keyID string, columns []string, langs []Language) error

CheckTranslationsExist checks if translations exist for all columns and languages

func (*MockStore) DeleteTranslations

func (m *MockStore) DeleteTranslations(ctx context.Context, modelName, keyID string, lang Language) error

DeleteTranslations deletes translations from memory

func (*MockStore) GetTranslations

func (m *MockStore) GetTranslations(ctx context.Context, modelName, keyID string, lang Language) (map[string]string, error)

GetTranslations retrieves translations from memory

func (*MockStore) GetTranslationsBatch

func (m *MockStore) GetTranslationsBatch(ctx context.Context, modelName string, keyIDs []string, lang Language) (map[string]map[string]string, error)

GetTranslationsBatch retrieves translations for multiple model instances

func (*MockStore) SaveTranslation

func (m *MockStore) SaveTranslation(ctx context.Context, data Data) error

SaveTranslation saves translation data in memory

type Store

type Store interface {
	// SaveTranslation saves translation data (should be executed in a transaction)
	SaveTranslation(ctx context.Context, data Data) error

	// GetTranslations retrieves all translations for a specific model instance and language
	// Returns a map of column_name -> translation_value
	GetTranslations(ctx context.Context, modelName, keyID string, lang Language) (map[string]string, error)

	// DeleteTranslations deletes all translations for a specific model instance and language
	DeleteTranslations(ctx context.Context, modelName, keyID string, lang Language) error

	// CheckTranslationsExist checks if translations exist for all specified columns and languages
	// Returns nil if all translations exist, ErrMissingTranslations otherwise
	CheckTranslationsExist(ctx context.Context, modelName, keyID string, columns []string, langs []Language) error

	// GetTranslationsBatch retrieves translations for multiple model instances
	// Returns a map of keyID -> (column_name -> translation_value)
	GetTranslationsBatch(ctx context.Context, modelName string, keyIDs []string, lang Language) (map[string]map[string]string, error)
}

Store is the interface for translation storage

type Translatable

type Translatable interface {
	GetTranslationKey() (modelName, keyID string)
}

Translatable is implemented by models that carry translatable fields. The returned modelName groups instances of the same type; keyID identifies the instance.

type TranslatableList

type TranslatableList interface {
	Translatables() []Translatable
}

TranslatableList is implemented by response payloads that wrap a collection of Translatable models, so the REST middleware can translate them in one batch without per-handler code.

type Translator

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

Translator active object holds all deps

func New

func New(cfg Config) (*Translator, error)

New creates a new Translator instance

func NewMockTranslator

func NewMockTranslator() *Translator

NewMockTranslator creates a translator with mock store for testing

func (*Translator) CheckTranslationsExist

func (t *Translator) CheckTranslationsExist(ctx context.Context, model Translatable) error

CheckTranslationsExist checks if translations exist for all supported languages

func (*Translator) DefaultLanguage

func (t *Translator) DefaultLanguage() Language

DefaultLanguage returns the default language

func (*Translator) Delete

func (t *Translator) Delete(ctx context.Context, lang Language, model Translatable) error

Delete deletes translations for a model

func (*Translator) Get

func (t *Translator) Get(ctx context.Context, lang Language, model Translatable) error

Get retrieves translations for a model

func (*Translator) HTTPMiddleware

func (t *Translator) HTTPMiddleware(next http.Handler) http.Handler

HTTPMiddleware is net/http middleware that resolves the request language and stores it in the context for downstream handlers (and the REST translation middleware) to read via LanguageFromContext.

func (*Translator) IsDefaultLanguage

func (t *Translator) IsDefaultLanguage(lang Language) bool

IsDefaultLanguage checks if the given language is the default language

func (*Translator) LanguageFromRequest

func (t *Translator) LanguageFromRequest(r *http.Request) Language

LanguageFromRequest resolves the request language from the X-Language header, falling back to Accept-Language and then the translator's default. Unsupported codes fall back to the default.

func (*Translator) Languages

func (t *Translator) Languages() []Language

Languages returns all supported languages by the app

func (*Translator) Match

func (t *Translator) Match(code string) Language

Match maps a language code (e.g. one already resolved by an upstream i18n middleware) to a supported Language, falling back to the default when the code is empty or unsupported.

func (*Translator) Save

func (t *Translator) Save(ctx context.Context, lang Language, model Translatable) error

Save saves translations for a model

func (*Translator) Translate

func (t *Translator) Translate(ctx context.Context, model Translatable, lang Language) error

Translate translates a single model to the specified language Also translates nested Translatable objects in slice fields

func (*Translator) ValidateLanguage

func (t *Translator) ValidateLanguage(lang Language) error

ValidateLanguage checks if the language is supported

func (*Translator) ValidateTranslateTags

func (t *Translator) ValidateTranslateTags(model Translatable) error

ValidateTranslateTags validates that the model has correct translate tags

Directories

Path Synopsis
Package postgres is the Postgres implementation of translation.Store.
Package postgres is the Postgres implementation of translation.Store.
Package translationrest wires the translation package into a skit REST router as a single rest.MidFunc that translates responses automatically.
Package translationrest wires the translation package into a skit REST router as a single rest.MidFunc that translates responses automatically.

Jump to

Keyboard shortcuts

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