Help us shape the future of Go! Take the 2026 Go Developer Survey

translatable

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Oct 23, 2026 License: MIT Imports: 11 Imported by: 0

README

go-translatable

tests Go Reference

Multilingual database fields and locale fallback for Go SaaS applications. Translations live in your own tables as jsonb columns, so there are no extra tables, joins or migrations to own. translatable.Text scans from and binds to those columns with pgx or database/sql; a Fallback decides what to show when a translation is missing (pt-BR to pt to en, en-GB to en); httplocale picks the request locale from the URL, a cookie or Accept-Language; and pgsql applies the same fallback inside SQL for selecting, sorting and filtering, with every locale and value bound as a parameter.

Install

go get github.com/byteminus/go-translatable

Requires Go 1.26 or later.

Quick start

package main

import (
	"context"
	"fmt"
	"log"
	"net/http"
	"os"

	"github.com/jackc/pgx/v5/pgxpool"
	"golang.org/x/text/language"

	translatable "github.com/byteminus/go-translatable"
	"github.com/byteminus/go-translatable/httplocale"
	"github.com/byteminus/go-translatable/pgsql"
)

var fallback = translatable.MustFallback(translatable.FallbackOptions{
	Defaults: []language.Tag{language.English},
	Chains:   map[string][]string{"pt-BR": {"pt", "en"}},
})

func main() {
	ctx := context.Background()
	pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))
	if err != nil {
		log.Fatal(err)
	}
	defer pool.Close()

	// Write: a Text is an ordinary query argument.
	title := translatable.MustText(map[string]string{"en": "Chair", "pt": "Cadeira"})
	if err := (translatable.Rules{Required: fallback.Defaults(), MaxLength: 120}).Validate("title", title); err != nil {
		log.Fatal(err)
	}
	if _, err := pool.Exec(ctx, `INSERT INTO products (id, title) VALUES ($1, $2)`, 1, title); err != nil {
		log.Fatal(err)
	}

	negotiator, err := httplocale.New(httplocale.Options{
		Supported: []language.Tag{language.English, language.MustParse("pt-BR")},
	})
	if err != nil {
		log.Fatal(err)
	}

	http.Handle("/products/1", negotiator.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Read in Go: scan the whole column, resolve with the request locale.
		var t translatable.Text
		if err := pool.QueryRow(r.Context(), `SELECT title FROM products WHERE id = 1`).Scan(&t); err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		value, from, _ := t.ResolveContext(r.Context(), fallback)
		fmt.Fprintf(w, "%s (%s)\n", value, from)
	})))

	http.Handle("/products", negotiator.Middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// Read in SQL: resolve, filter and sort inside the query.
		want, _ := httplocale.FromRequest(r)
		col := pgsql.MustColumn("p", "title")
		var args pgsql.Args
		q := `SELECT p.id, ` + col.Value(&args, fallback, want) + ` FROM products p` +
			` WHERE ` + col.Contains(&args, fallback, want, r.URL.Query().Get("q")) +
			` ORDER BY ` + col.SortKey(&args, fallback, want, "")
		rows, err := pool.Query(r.Context(), q, args...)
		if err != nil {
			http.Error(w, err.Error(), http.StatusInternalServerError)
			return
		}
		defer rows.Close()
		for rows.Next() {
			var id int
			var name string
			if err := rows.Scan(&id, &name); err != nil {
				http.Error(w, err.Error(), http.StatusInternalServerError)
				return
			}
			fmt.Fprintf(w, "%d %s\n", id, name)
		}
	})))

	log.Fatal(http.ListenAndServe(":8080", nil))
}

Concepts

Locales are BCP 47 tags (golang.org/x/text/language). Every key is stored canonically: en-us becomes en-US, iw becomes he. ParseLocale rejects malformed and unknown tags and the undetermined und with ErrInvalidLocale; input that spells one locale two ways ({"en-us": .., "en-US": ..}) fails with ErrDuplicateLocale instead of silently dropping a translation.

Text holds one string per locale. Get, Has, Set, Delete, Locales (sorted), Len, IsEmpty, Clone and Map work on exact locales. A Text never stores empty strings: setting "" removes the locale, and empty or null values are dropped when decoding, so "translated as empty" and "not translated" mean the same thing in Go and in SQL. The zero value encodes as {}. Like a map, a Text has reference semantics; use Clone for an independent copy.

Fallback turns a requested locale into an ordered list of locales to try (Chain):

  1. the requested locale;
  2. its explicit chain from Chains, or else its CLDR parents (en-GB, en-001, en; es-MX, es-419, es). An explicit chain on a parent takes over from that parent. Parents never cross scripts: zh-TW reaches zh-Hant, never zh. SkipParents turns parents off;
  3. the Defaults, in order;
  4. with AnyAvailable, any translation present, lowest key first.

Text.Resolve(want, fb) returns the value and the locale it came from. WithLocale and LocaleFrom carry the request locale in a context, and Text.ResolveContext(ctx, fb) resolves with it. A nil *Fallback behaves like the zero options. A Fallback is immutable and safe for concurrent use.

Request locale. httplocale.Negotiator reads candidates from the query parameter lang, the cookie lang and Accept-Language (order and names configurable), matches each against the supported locales with language.Matcher, and returns the supported tag itself (en-GB is served by en, pt-PT by pt-BR). The first source with a match wins; the first supported locale is the default. Its middleware stores the result with WithLocale and sets Vary for the headers it consulted.

Validation. Rules{Required, MaxLength}.Validate(field, text) returns FieldErrors, one FieldError per problem with Field, Locale, Code (required, too_long), Max and a Path() such as title.pt-BR. All of them match ErrValidation with errors.Is. Length is counted in Unicode code points, as Postgres char_length and varchar(n) do.

PostgreSQL

This package has no tables of its own. A translated field is a column in your table:

CREATE TABLE products (
    id    bigint PRIMARY KEY,
    title jsonb  NOT NULL DEFAULT '{}'
);

Text implements sql.Scanner and driver.Valuer, and is tested both with pgx v5 directly and through database/sql (pgx stdlib). SQL NULL scans as an empty Text.

Querying

Package pgsql builds expressions over such a column. A Column is built once from identifier parts (quoted with pgx.Identifier), and each builder appends its bind parameters to an Args, which hands out $n placeholders, so fragments compose with each other and with your own parameters. Args is a []any, so pass it as args....

Builder SQL shape
Get(&a, tag) NULLIF(col->>$1::text, '')
Value(&a, fb, want) COALESCE(NULLIF(col->>$1::text, ''), NULLIF(col->>$2::text, ''), ...), plus an any-available subquery when configured
Locale(&a, fb, want) CASE WHEN ... THEN $1::text ... END: the locale Value came from
SortKey(&a, fb, want, collation) Value, optionally COLLATE "de-x-icu"
Has(&a, tag) (COALESCE(col->>$1::text, '') <> ''), never NULL
Equals(&a, tag, value) (col @> jsonb_build_object($1::text, $2::text))
Contains(&a, fb, want, s) (Value ILIKE $n::text ESCAPE '\') with % and _ escaped

The integration suite checks that Value and Locale agree with Text.Resolve for every row, locale and fallback configuration it covers. Locales and values are always bind parameters; nothing user-supplied is interpolated.

Indexes

Index DDL cannot take parameters, so LocaleIndex and GINIndex generate it from canonical tags (letters, digits and hyphens only) and quoted identifiers:

ddl, err := pgsql.LocaleIndex("products_title_en", []string{"products"}, "title", language.English)
// CREATE INDEX IF NOT EXISTS "products_title_en" ON "products" ((NULLIF("title"->>'en', '')))

ddl, err = pgsql.GINIndex("products_title_gin", []string{"products"}, "title")
// CREATE INDEX IF NOT EXISTS "products_title_gin" ON "products" USING gin ("title" jsonb_path_ops)

The per-locale expression index serves Get(...) = $n and sorting by one exact locale. The GIN index serves Equals for every locale. Fallback expressions (Value, Contains) are evaluated per row; for large tables, narrow the rows with an indexed predicate first.

Transactions

There is no store and therefore no WithConn option: the builders produce SQL and arguments, and you run them on whatever you already hold, a *pgxpool.Pool, a *pgx.Conn or a pgx.Tx, so writes join your transaction naturally.

API overview

Package translatable:

  • ParseLocale, Key, ErrInvalidLocale, ErrDuplicateLocale
  • Text, NewText, MustText; methods Get, Set, Delete, Has, Locales, Len, IsEmpty, Clone, Map, Resolve, ResolveContext, plus JSON and SQL interfaces
  • Fallback, FallbackOptions, NewFallback, MustFallback; methods Chain, ChainKeys, Defaults, AnyAvailable
  • WithLocale, LocaleFrom
  • Rules, FieldError, FieldErrors, ErrValidation, CodeRequired, CodeTooLong

Package httplocale: New, Options, Negotiator (Negotiate, Middleware), Source (Query, Cookie, Header), FromRequest, ErrNoSupported.

Package pgsql: Args, Column, NewColumn, MustColumn, the builders above, LocaleIndex, GINIndex, ErrInvalidIdentifier.

Testing

go test -race ./...
TEST_DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres?sslmode=disable go test -race ./...

Without TEST_DATABASE_URL the PostgreSQL tests skip, unless CI is set, in which case they fail. Each test works in its own randomly named schema, dropped afterwards, so the suite is safe to run in parallel against a shared database.

Roadmap

  • A generic Value[T] for structured translated content (for example translated JSON objects). It was left out of v0.1.0 because the semantics that make Text predictable do not carry over cleanly: there is no generic notion of "empty" to treat as missing, Clone could only be shallow, and the SQL side needs -> with jsonb results rather than ->> with text.
  • An Allowed rule that rejects locales outside the supported set.
  • Full-text search helpers (per-locale tsvector with the matching text search configuration).
  • Writing the negotiated locale back to a cookie from the middleware.
  • Partial-update helpers (jsonb_set for one locale) for concurrent editors working on different languages of the same field.

License

MIT. See LICENSE.

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

Examples

Constants

View Source
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

View Source
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.

View Source
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.

View Source
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

func Key(tag language.Tag) string

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

func LocaleFrom(ctx context.Context) (language.Tag, bool)

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

func ParseLocale(s string) (language.Tag, error)

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.

func WithLocale

func WithLocale(ctx context.Context, tag language.Tag) context.Context

WithLocale returns a copy of ctx that carries tag as the request locale. Storing the undetermined tag is equivalent to storing no locale.

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

func (f *Fallback) AnyAvailable() bool

AnyAvailable reports whether resolution falls back to any present translation once the chain is exhausted.

func (*Fallback) Chain

func (f *Fallback) Chain(want language.Tag) []language.Tag

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]

func (*Fallback) ChainKeys

func (f *Fallback) ChainKeys(want language.Tag) []string

ChainKeys is Chain expressed as canonical keys, which is what the SQL builders bind as parameters.

func (*Fallback) Defaults

func (f *Fallback) Defaults() []language.Tag

Defaults returns the configured default locales, in order. It is handy for requiring the defaults in validation Rules.

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) Error

func (e *FieldError) Error() string

Error implements error.

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

func (r Rules) Validate(field string, t Text) error

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

func MustText(values map[string]string) Text

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

func NewText(values map[string]string) (Text, error)

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) Clone

func (t Text) Clone() Text

Clone returns an independent copy of t.

func (*Text) Delete

func (t *Text) Delete(tag language.Tag)

Delete removes the translation for tag, if any.

func (Text) Get

func (t Text) Get(tag language.Tag) (string, bool)

Get returns the translation for exactly tag, with no fallback.

func (Text) Has

func (t Text) Has(tag language.Tag) bool

Has reports whether a translation exists for exactly tag.

func (Text) IsEmpty

func (t Text) IsEmpty() bool

IsEmpty reports whether t has no translations.

func (Text) Len

func (t Text) Len() int

Len returns the number of translations.

func (Text) Locales

func (t Text) Locales() []language.Tag

Locales returns the locales that have a translation, sorted by their canonical key so output is stable across runs.

func (Text) Map

func (t Text) Map() map[string]string

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

func (t Text) MarshalJSON() ([]byte, error)

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

func (t Text) Resolve(want language.Tag, f *Fallback) (string, language.Tag, bool)

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

func (t Text) ResolveContext(ctx context.Context, f *Fallback) (string, language.Tag, bool)

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

func (t *Text) Scan(src any) error

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

func (t *Text) Set(tag language.Tag, value string) error

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

func (t *Text) UnmarshalJSON(data []byte) error

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.

func (Text) Value

func (t Text) Value() (driver.Value, error)

Value implements driver.Valuer. It returns the JSON encoding as a string, which every Postgres driver accepts for json and jsonb parameters; a []byte would be sent as bytea by some drivers.

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.

Jump to

Keyboard shortcuts

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