Documentation
¶
Overview ¶
Package translatable stores multilingual fields in an application's own tables and resolves them with locale fallback.
A translated field is a jsonb column holding one string per locale:
CREATE TABLE products (
id bigint PRIMARY KEY,
title jsonb NOT NULL DEFAULT '{}'
);
In Go the column is a Text, which implements json.Marshaler, json.Unmarshaler, sql.Scanner and driver.Valuer, so it can be scanned from and bound to queries with pgx or database/sql without conversion. The package defines no tables of its own.
Locales are BCP 47 tags from golang.org/x/text/language. Keys are always stored in canonical form (en-us becomes en-US, iw becomes he), and invalid tags are rejected with ErrInvalidLocale.
Fallback ¶
A Fallback decides which locales to try when a translation is missing: the requested locale, an explicit chain for it (pt-BR, then pt, then en) or its CLDR parents (en-GB, then en-001, then en), then the default locales, and optionally any translation at all. Text.Resolve applies it and reports which locale the value came from; Text.ResolveContext uses the locale stored in a context by WithLocale.
Subpackages ¶
Package httplocale negotiates a request's locale from a query parameter, a cookie and Accept-Language, and stores it with WithLocale. Package pgsql builds SQL expressions that apply the same fallback inside queries, for selecting, sorting and filtering, plus index DDL.
Validation ¶
Rules checks that required locales are present and that translations respect a maximum length, returning FieldErrors that name the field and locale of each problem.
Index ¶
- Constants
- Variables
- func Key(tag language.Tag) string
- func LocaleFrom(ctx context.Context) (language.Tag, bool)
- func ParseLocale(s string) (language.Tag, error)
- func WithLocale(ctx context.Context, tag language.Tag) context.Context
- type Fallback
- type FallbackOptions
- type FieldError
- type FieldErrors
- type Rules
- type Text
- func (t Text) Clone() Text
- func (t *Text) Delete(tag language.Tag)
- func (t Text) Get(tag language.Tag) (string, bool)
- func (t Text) Has(tag language.Tag) bool
- func (t Text) IsEmpty() bool
- func (t Text) Len() int
- func (t Text) Locales() []language.Tag
- func (t Text) Map() map[string]string
- func (t Text) MarshalJSON() ([]byte, error)
- func (t Text) Resolve(want language.Tag, f *Fallback) (string, language.Tag, bool)
- func (t Text) ResolveContext(ctx context.Context, f *Fallback) (string, language.Tag, bool)
- func (t *Text) Scan(src any) error
- func (t *Text) Set(tag language.Tag, value string) error
- func (t *Text) UnmarshalJSON(data []byte) error
- func (t Text) Value() (driver.Value, error)
Examples ¶
Constants ¶
const ( CodeRequired = "required" CodeTooLong = "too_long" )
Codes reported in FieldError.Code. They are stable strings, suitable for API responses and for looking up user-facing messages.
Variables ¶
var ErrDuplicateLocale = errors.New("translatable: duplicate locale")
ErrDuplicateLocale is returned, wrapped, when decoding input that names the same locale twice under different spellings, such as "en-us" and "en-US". Picking one silently would lose a translation, so decoding fails instead.
var ErrInvalidLocale = errors.New("translatable: invalid locale")
ErrInvalidLocale is returned, wrapped, when a string is not a well-formed and known BCP 47 language tag, or when the undetermined tag "und" is used where a concrete locale is required.
var ErrValidation = errors.New("translatable: validation failed")
ErrValidation is matched by errors.Is for every error returned by Rules.Validate, so callers can tell input problems from other failures.
Functions ¶
func Key ¶
Key returns the canonical string under which translations for tag are stored, both in a Text and in the jsonb column it is persisted to. Tags built with language.Raw are canonicalized first, so every way of naming a locale maps to one key.
func LocaleFrom ¶
LocaleFrom returns the locale stored in ctx by WithLocale. The boolean is false when ctx carries no locale or carries the undetermined tag.
func ParseLocale ¶
ParseLocale parses s as a BCP 47 language tag and returns it in canonical form, so "en-us" and "EN_us" both yield en-US. The undetermined tag "und" is rejected because it cannot identify a translation.
Types ¶
type Fallback ¶
type Fallback struct {
// contains filtered or unexported fields
}
Fallback decides which locales to try, and in which order, when the requested translation is missing. Build one with NewFallback. A Fallback is immutable and safe for concurrent use. A nil *Fallback behaves like one built from zero FallbackOptions.
func MustFallback ¶
func MustFallback(opts FallbackOptions) *Fallback
MustFallback is like NewFallback but panics on error. It is meant for package-level variables built from literals.
func NewFallback ¶
func NewFallback(opts FallbackOptions) (*Fallback, error)
NewFallback validates opts and returns a Fallback. Every locale in Defaults and Chains must be a concrete, valid BCP 47 tag; errors wrap ErrInvalidLocale.
func (*Fallback) AnyAvailable ¶
AnyAvailable reports whether resolution falls back to any present translation once the chain is exhausted.
func (*Fallback) Chain ¶
Chain returns the locales to try for want, in order and without duplicates: want itself, then either its explicit chain or its parent tags (an explicit chain on a parent takes over from there), then the defaults. When want is undetermined only the defaults are returned. The "any available" last resort is not part of the chain because it depends on the data; see AnyAvailable.
Example ¶
package main
import (
"fmt"
"golang.org/x/text/language"
translatable "github.com/byteminus/go-translatable"
)
func main() {
fb := translatable.MustFallback(translatable.FallbackOptions{
Defaults: []language.Tag{language.English},
})
fmt.Println(fb.Chain(language.MustParse("es-MX")))
}
Output: [es-MX es-419 es en]
type FallbackOptions ¶
type FallbackOptions struct {
// Defaults are tried, in order, after the requested locale and its
// chain. They are usually the application's default locale.
Defaults []language.Tag
// Chains maps a locale to an explicit list of locales to try after it,
// for example {"pt-BR": {"pt", "en"}}. Keys and values are BCP 47
// strings so the map can come straight from configuration. When the
// requested locale, or one of its parents, has an explicit chain, that
// chain replaces the remaining automatic parent tags.
Chains map[string][]string
// SkipParents disables automatic parent tags, so en-GB no longer falls
// back to en. Parents follow CLDR inheritance (see language.Tag.Parent),
// which deliberately keeps scripts apart: zh-TW reaches zh-Hant but
// never zh.
SkipParents bool
// AnyAvailable makes resolution fall back, as a last resort, to any
// translation present, choosing the lowest canonical key so the result is
// stable. It suits display contexts where some text beats none.
AnyAvailable bool
}
FallbackOptions configures a Fallback. The zero value is valid: it tries the requested locale and then its parent tags.
type FieldError ¶
type FieldError struct {
// Field is the name the caller passed to Rules.Validate.
Field string
// Locale is the locale the problem concerns.
Locale language.Tag
// Code is CodeRequired or CodeTooLong.
Code string
// Max is the length limit for CodeTooLong, and zero otherwise.
Max int
}
FieldError describes one problem with one locale of a translated field.
func (*FieldError) Path ¶
func (e *FieldError) Path() string
Path returns the field and locale joined with a dot, such as "title.en", the form most API error formats use for nested fields.
func (*FieldError) Unwrap ¶
func (e *FieldError) Unwrap() error
Unwrap makes every FieldError match ErrValidation.
type FieldErrors ¶
type FieldErrors []*FieldError
FieldErrors is the error returned by Rules.Validate when at least one rule fails. Use errors.As to retrieve it and inspect each FieldError.
func (FieldErrors) Error ¶
func (es FieldErrors) Error() string
Error implements error, joining every problem on one line.
func (FieldErrors) Unwrap ¶
func (es FieldErrors) Unwrap() []error
Unwrap exposes each FieldError to errors.Is and errors.As.
type Rules ¶
type Rules struct {
// Required lists locales that must have a translation. A common choice
// is Fallback.Defaults, so resolution can always succeed.
Required []language.Tag
// MaxLength, when positive, caps every translation at this many
// characters, counted as Unicode code points to match Postgres
// varchar(n) and char_length.
MaxLength int
}
Rules are validation constraints for a Text. The zero value accepts everything.
func (Rules) Validate ¶
Validate checks t against r and returns nil or a FieldErrors naming field. Missing required locales are reported first, in the order given, then over-long translations in canonical locale order, so the output is deterministic.
Example ¶
package main
import (
"errors"
"fmt"
"golang.org/x/text/language"
translatable "github.com/byteminus/go-translatable"
)
func main() {
rules := translatable.Rules{Required: []language.Tag{language.English}, MaxLength: 10}
title := translatable.MustText(map[string]string{"de": "Ein sehr langer Titel"})
err := rules.Validate("title", title)
var fes translatable.FieldErrors
if errors.As(err, &fes) {
for _, fe := range fes {
fmt.Println(fe.Path(), fe.Code)
}
}
}
Output: title.en required title.de too_long
type Text ¶
type Text struct {
// contains filtered or unexported fields
}
Text holds the translations of one string field, keyed by locale. It is stored as a jsonb object such as {"en": "Hello", "pt-BR": "Ola"}.
Keys are always canonical BCP 47 tags (see Key), and a Text never holds an empty string: setting a locale to "" removes it, so "translated as empty" and "not translated" cannot diverge between Go and SQL.
The zero value is an empty Text ready to use. Like a map, a Text has reference semantics: copies share their translations until one of them is first written from empty, so use Clone for an independent copy. A Text is not safe for concurrent mutation; concurrent reads are safe.
Example ¶
package main
import (
"encoding/json"
"fmt"
"golang.org/x/text/language"
translatable "github.com/byteminus/go-translatable"
)
func main() {
var title translatable.Text
_ = title.Set(language.MustParse("en-us"), "Color")
_ = title.Set(language.German, "Farbe")
v, ok := title.Get(language.AmericanEnglish)
fmt.Println(v, ok)
fmt.Println(title.Locales())
b, _ := json.Marshal(title)
fmt.Println(string(b))
}
Output: Color true [de en-US] {"de":"Farbe","en-US":"Color"}
func MustText ¶
MustText is like NewText but panics on error. It is meant for literals in tests and package-level variables, where an invalid locale is a programmer error.
func NewText ¶
NewText builds a Text from translations keyed by locale strings. Keys are parsed with ParseLocale; an invalid key or two keys naming the same locale is an error. Empty values are skipped.
func (Text) Locales ¶
Locales returns the locales that have a translation, sorted by their canonical key so output is stable across runs.
func (Text) Map ¶
Map returns a copy of the translations keyed by canonical locale string, for handing to templates or encoders that expect a plain map.
func (Text) MarshalJSON ¶
MarshalJSON encodes t as a JSON object keyed by canonical locale. An empty Text encodes as {} rather than null, matching the NOT NULL DEFAULT '{}' columns it is designed for.
func (Text) Resolve ¶
Resolve returns the first translation found along f's fallback chain for want (see Fallback.Chain), together with the locale it came from. A nil f behaves like a zero Fallback. The boolean is false when no candidate has a translation; the returned locale is then the undetermined tag.
Example ¶
package main
import (
"fmt"
"golang.org/x/text/language"
translatable "github.com/byteminus/go-translatable"
)
func main() {
fb := translatable.MustFallback(translatable.FallbackOptions{
Defaults: []language.Tag{language.English},
Chains: map[string][]string{"pt-BR": {"pt", "en"}},
})
title := translatable.MustText(map[string]string{"en": "Chair", "pt": "Cadeira"})
for _, want := range []string{"pt-BR", "en-GB", "ja"} {
v, from, _ := title.Resolve(language.MustParse(want), fb)
fmt.Printf("%s: %s (from %s)\n", want, v, from)
}
}
Output: pt-BR: Cadeira (from pt) en-GB: Chair (from en) ja: Chair (from en)
func (Text) ResolveContext ¶
ResolveContext is Resolve with the locale carried by ctx (see WithLocale). When ctx carries no locale, only f's defaults and last resort apply.
Example ¶
package main
import (
"context"
"fmt"
"golang.org/x/text/language"
translatable "github.com/byteminus/go-translatable"
)
func main() {
fb := translatable.MustFallback(translatable.FallbackOptions{Defaults: []language.Tag{language.English}})
greeting := translatable.MustText(map[string]string{"en": "Hello", "de": "Hallo"})
ctx := translatable.WithLocale(context.Background(), language.MustParse("de-AT"))
v, _, _ := greeting.ResolveContext(ctx, fb)
fmt.Println(v)
}
Output: Hallo
func (*Text) Scan ¶
Scan implements sql.Scanner for json and jsonb columns. SQL NULL scans as an empty Text. On error t is left unchanged.
func (*Text) Set ¶
Set stores value as the translation for tag. An empty value removes the translation. Setting the undetermined tag is an error wrapping ErrInvalidLocale.
func (*Text) UnmarshalJSON ¶
UnmarshalJSON decodes a JSON object of locale to string. Keys are canonicalized; invalid keys, non-string values and duplicate locales are errors. JSON null, either for the whole value or for one locale, means "no translation". On error t is left unchanged.
Directories
¶
| Path | Synopsis |
|---|---|
|
Package httplocale negotiates the locale of an HTTP request against the set of locales an application supports, and stores the result in the request context with translatable.WithLocale, where Text.ResolveContext and the SQL builders' callers can pick it up.
|
Package httplocale negotiates the locale of an HTTP request against the set of locales an application supports, and stores the result in the request context with translatable.WithLocale, where Text.ResolveContext and the SQL builders' callers can pick it up. |
|
internal
|
|
|
pgtest
Package pgtest gives integration tests an isolated PostgreSQL schema.
|
Package pgtest gives integration tests an isolated PostgreSQL schema. |
|
Package pgsql builds PostgreSQL expressions for reading, sorting and filtering translatable.Text values stored in jsonb columns, following a translatable.Fallback chain inside the query.
|
Package pgsql builds PostgreSQL expressions for reading, sorting and filtering translatable.Text values stored in jsonb columns, following a translatable.Fallback chain inside the query. |