fluent

package module
v0.2.0 Latest Latest
Warning

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

Go to latest
Published: Jun 9, 2026 License: Apache-2.0 Imports: 9 Imported by: 0

README

gofluent

Go Reference CI Go Report Card License

A Go implementation of Project Fluent — a localization system for natural-sounding translations.

gofluent is a port of the reference JavaScript implementation (@fluent/syntax and @fluent/bundle). Locale-aware formatting (plural rules, numbers, dates) is exposed through pluggable interfaces; the CLDR-backed implementations come from the separate github.com/hakastein/gocldr module (generated from CLDR data and validated against Node's Intl.*), wired in through the fluentx adapter.

Status: pre-1.0. The library is feature-complete and tested against the upstream conformance and Intl.* suites, but the public API may still change between minor versions until 1.0.

Install

go get github.com/hakastein/gofluent

Requires Go 1.23 or newer.

CLDR-backed formatting (plurals, numbers, dates) now comes from the separate github.com/hakastein/gocldr module, pulled in automatically as a dependency. Its locale data is opt-in: an application that formats numbers or dates must blank-import the locale data it needs — import _ "github.com/hakastein/gocldr/locales/en" for a single locale, or import _ "github.com/hakastein/gocldr/locales/all" for every locale. With no locale data imported, formatting degrades gracefully (dates render as RFC3339, numbers as ASCII root).

Packages

Package Purpose
github.com/hakastein/gofluent Runtime: fast FTL parser, fault-tolerant resolver, Bundle (one locale).
.../syntax (+ .../syntax/ast) Full AST, recursive-descent parser, serializer, visitor — for tooling.
.../fluentx Wires the gocldr formatters into a Bundle via fluentx.Options().
.../langneg Language negotiation (port of @fluent/langneg).
.../localization High-level fallback layer over an ordered chain of locale bundles.

Quick start

res, _ := fluent.NewResource("hello = Hello, { $name }!")

b := fluent.NewBundle("en")
b.AddResource(res)

msg, _ := b.GetMessage("hello")
var errs []error
out := b.FormatPatternAny(msg.Value, map[string]any{"name": "World"}, &errs)
// out == "Hello, ⁨World⁩!"  (placeable wrapped in bidi isolation marks)

The resolver is fault-tolerant: it never panics when given an error sink. Missing references and other problems are appended to errs and rendered as fluent.js-style placeholders (for example {$name}); a best-effort string is always returned.

By default placeables are wrapped in Unicode bidirectional isolation marks (FSI/PDI). Disable with fluent.NewBundle("en", fluent.WithUseIsolating(false)).

A Bundle is safe for concurrent use: FormatPattern / FormatPatternAny, HasMessage, GetMessage, and the AddResource / AddResourceOverriding / AddFunction mutators may be called from multiple goroutines simultaneously.

Locale-aware formatting

Wire the CLDR-backed formatters from fluentx to get correct plurals, number grouping, currency, and dates. The CLDR data is opt-in, so blank-import the locales you need (here, every locale via .../locales/all):

import (
    fluent "github.com/hakastein/gofluent"
    "github.com/hakastein/gofluent/fluentx"

    _ "github.com/hakastein/gocldr/locales/all" // or .../locales/ru for just Russian
)

b := fluent.NewBundle("ru", fluentx.Options()...)
b.AddResource(res) // { $n -> [one] ... [few] ... *[many] ... } now selects correctly

The underlying gocldr formatters are also usable on their own, independent of Fluent (gocldr/number, gocldr/plural, gocldr/datetime); see that module's documentation.

Localization with fallback

FSLoader accepts any fs.FS — typically an embed.FS (translations compiled into the binary) or os.DirFS("./locales") (read from disk at runtime):

import (
    "embed"
    "github.com/hakastein/gofluent/localization"
)

// e.g. locales/en/main.ftl ("greeting = Hello"), locales/de/main.ftl ("greeting = Hallo")
//go:embed locales
var locales embed.FS

loader := localization.FSLoader(locales, "locales/{locale}/{resource}.ftl")

l10n, _ := localization.NewFromLocales(
    []string{"de-DE"}, []string{"de", "en"}, "en",
    []string{"main"}, loader,
)
val, _ := l10n.FormatValue("greeting", nil) // "Hallo", falling back to "en" if missing

Provenance & verification

gofluent is generated code — it was ported from fluent.js with the assistance of large language models — and that is stated plainly, because the project's credibility rests on verification rather than authorship. Correctness is pinned to executable references, all run under go test ./...:

  • The syntax parser and serializer are checked against the upstream Project Fluent conformance fixtures (62/62 structure, 35/36 reference — the single skip matches fluent.js).
  • The CLDR formatters live in github.com/hakastein/gocldr and are checked there against Node's Intl.* (Intl.PluralRules, Intl.NumberFormat, and Intl.DateTimeFormat golden fixtures).

Read the code and the tests, not just the prose — ARCHITECTURE.md explains the design and where each guarantee is enforced.

Contributing

Contributions are welcome. See CONTRIBUTING.md for build, test, and linting mechanics, and ARCHITECTURE.md for how the codebase is organized and why. By participating you agree to the Code of Conduct.

License

Licensed under the Apache License, Version 2.0. See NOTICE for attribution of the fluent.js port lineage and the CLDR data.

Documentation

Overview

Package fluent is a Go implementation of Project Fluent (https://projectfluent.org), a localization system for natural-sounding translations.

It is a port of the reference JavaScript implementation (@fluent/syntax and @fluent/bundle) with one deliberate change: locale-aware formatting (plural rules, numbers, dates) is exposed through pluggable interfaces instead of a hard dependency on a CLDR library. This keeps the core dependency-free.

Layers

  • Package fluent (this package): the runtime — a fast FTL parser (NewResource), a fault-tolerant resolver, and Bundle (one locale).
  • Package fluent/syntax: the full AST, recursive-descent parser, and serializer used by tooling and conformance.
  • Package fluent/fluentx: CLDR-backed PluralRules, NumberFormatter, and DateTimeFormatter backed by the module's self-contained cldr packages (no external dependencies). Import it to enable real locale formatting.
  • Package fluent/langneg: language negotiation (a port of @fluent/langneg).
  • Package fluent/localization: a high-level layer that formats messages across an ordered chain of locale bundles with fallback.

Basic use

res, _ := fluent.NewResource("hello = Hello, { $name }!")
b := fluent.NewBundle("en")
b.AddResource(res)
msg, _ := b.GetMessage("hello")
var errs []error
out := b.FormatPatternAny(msg.Value, map[string]any{"name": "World"}, &errs)

The resolver is fault-tolerant: it never panics. Missing references and other problems are appended to the errors slice and rendered as fluent.js-style placeholders (for example {$name}); a best-effort string is always returned.

By default placeables are wrapped in Unicode bidirectional isolation marks (FSI/PDI). Disable this with WithUseIsolating(false).

A Bundle is safe for concurrent use: FormatPattern, HasMessage, GetMessage, and the Add* methods (AddFunction, AddResource, AddResourceOverriding) may run from multiple goroutines at once.

Example

Example shows the minimal flow: parse a resource, add it to a bundle, look up a message, and format its pattern with arguments.

package main

import (
	"fmt"

	fluent "github.com/hakastein/gofluent"
)

func main() {
	res, errs := fluent.NewResource("hello = Hello, { $name }!")
	if len(errs) > 0 {
		panic(errs[0])
	}

	// useIsolating is disabled here so the output is plain ASCII; in production
	// the default (true) wraps placeables in Unicode bidi isolation marks.
	b := fluent.NewBundle("en", fluent.WithUseIsolating(false))
	b.AddResource(res)

	msg, ok := b.GetMessage("hello")
	if !ok {
		panic("message not found")
	}

	var ferrs []error
	out := b.FormatPatternAny(msg.Value, map[string]any{"name": "World"}, &ferrs)
	fmt.Println(out)
}
Output:
Hello, World!

Index

Examples

Constants

View Source
const MaxPlaceables = 100

MaxPlaceables is the maximum number of placeables which can be expanded in a single FormatPattern call. The limit protects against the Billion Laughs and Quadratic Blowup attacks.

Variables

View Source
var (
	// ErrReference: an unknown message, term, variable, function, or attribute
	// was referenced.
	ErrReference = errors.New("fluent: reference error")
	// ErrRange: no variant matched a selector, a value is out of range, a
	// reference is cyclic, or the placeable limit was exceeded.
	ErrRange = errors.New("fluent: range error")
	// ErrType: a value cannot be used in the position it appears (e.g. a
	// non-numeric selector argument, or a term used as a placeable).
	ErrType = errors.New("fluent: type error")
)

Error kinds collected by FormatPattern, mirroring the JS error classes fluent.js reports (ReferenceError / RangeError / TypeError). Every resolution error wraps one of these sentinels, so a caller can classify a failure with errors.Is, e.g. errors.Is(err, fluent.ErrReference).

Functions

func MemoizerForLocales

func MemoizerForLocales(locales []string) map[string]any

MemoizerForLocales returns a process-wide cache map shared by all bundles using the same locale list. Mirrors getMemoizerForLocale in fluent.js.

Types

type Bundle

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

Bundle is a single-language store of translation resources, responsible for formatting message values and attributes to strings.

A Bundle is safe for concurrent use: FormatPattern/FormatPatternAny, HasMessage, GetMessage, AddFunction, AddResource, and AddResourceOverriding may be called from multiple goroutines simultaneously. The messages, terms, and functions maps are guarded by mu; the locale and the injected formatters are set once at construction and never mutated afterwards.

Example (SelectExpression)

ExampleBundle_selectExpression demonstrates a select expression. Without a real PluralRules implementation wired in, numeric selectors match by exact value, so the variant key "1" is selected for the number 1.

package main

import (
	"fmt"

	fluent "github.com/hakastein/gofluent"
)

func main() {
	src := `
emails =
    { $count ->
        [1] You have one new email.
       *[other] You have { $count } new emails.
    }
`
	res, _ := fluent.NewResource(src)
	b := fluent.NewBundle("en", fluent.WithUseIsolating(false))
	b.AddResource(res)

	msg, _ := b.GetMessage("emails")
	var errs []error
	fmt.Println(b.FormatPatternAny(msg.Value, map[string]any{"count": 1}, &errs))
	fmt.Println(b.FormatPatternAny(msg.Value, map[string]any{"count": 5}, &errs))
}
Output:
You have one new email.
You have 5 new emails.

func NewBundle

func NewBundle(locale string, opts ...BundleOption) *Bundle

NewBundle creates a Bundle for the given primary locale. useIsolating defaults to true; NUMBER and DATETIME are always available; the three formatters default to the dependency-free no-op implementations.

func (*Bundle) AddFunction

func (b *Bundle) AddFunction(name string, fn Function)

AddFunction registers (or overrides) a runtime function by name.

func (*Bundle) AddResource

func (b *Bundle) AddResource(res *Resource) []error

AddResource adds a parsed resource to the bundle without allowing overrides. It returns errors for any attempted overrides of existing messages/terms.

func (*Bundle) AddResourceOverriding

func (b *Bundle) AddResourceOverriding(res *Resource) []error

AddResourceOverriding adds a parsed resource, allowing it to override existing messages and terms.

func (*Bundle) FormatPattern

func (b *Bundle) FormatPattern(pattern Pattern, args map[string]Value, errs *[]error) string

FormatPattern formats a Pattern to a string. args resolves variable references; pass nil for none.

errs selects the error mode, mirroring fluent.js:

  • Non-nil errs is collect mode: every resolution error is appended to *errs and a best-effort string is always returned (the resolver never panics).
  • Nil errs is throw mode: the first resolution error is "thrown" (panics out of FormatPattern). The caller is responsible for recovering it. Use this only when you want strict failure rather than fault-tolerant rendering.

args accepts a map[string]Value (already-typed) — see FormatPatternAny for a map[string]any convenience wrapper.

func (*Bundle) FormatPatternAny

func (b *Bundle) FormatPatternAny(pattern Pattern, args map[string]any, errs *[]error) string

FormatPatternAny is a convenience wrapper accepting raw Go argument values (map[string]any). Values are converted to Fluent Values via coerceArg.

Precision note: integer arguments are stored as float64 (Fluent's only numeric type, matching JS). int64/uint64 magnitudes above 2^53 cannot be represented exactly and may be rounded; pass a preformatted string (or a custom Value) when exact rendering of such large integers matters.

func (*Bundle) GetMessage

func (b *Bundle) GetMessage(id string) (*Message, bool)

GetMessage returns the raw message with the given id, if present.

func (*Bundle) HasMessage

func (b *Bundle) HasMessage(id string) bool

HasMessage reports whether a public message with the given id exists.

func (*Bundle) Locale

func (b *Bundle) Locale() string

Locale returns the bundle's primary locale string.

type BundleOption

type BundleOption func(*Bundle)

BundleOption configures a Bundle in NewBundle.

func WithDateTimeFormatter

func WithDateTimeFormatter(f DateTimeFormatter) BundleOption

WithDateTimeFormatter injects a DateTimeFormatter (replaces the no-op default).

func WithFunctions

func WithFunctions(fns map[string]Function) BundleOption

WithFunctions registers additional builtin functions, merged over NUMBER and DATETIME.

func WithLocales

func WithLocales(locales ...string) BundleOption

WithLocales sets the full locale fallback list. The first entry becomes the primary locale passed to formatters.

func WithNumberFormatter

func WithNumberFormatter(f NumberFormatter) BundleOption

WithNumberFormatter injects a NumberFormatter (replaces the no-op default).

func WithPluralRules

func WithPluralRules(p PluralRules) BundleOption

WithPluralRules injects a PluralRules implementation (replaces the no-op default).

func WithTransform

func WithTransform(t TextTransform) BundleOption

WithTransform sets the text transform applied to string parts of patterns.

func WithUseIsolating

func WithUseIsolating(v bool) BundleOption

WithUseIsolating sets whether to wrap interpolations in Unicode isolation marks (FSI/PDI). Default is true.

type ComplexPattern

type ComplexPattern []PatternElement

ComplexPattern is an array of pattern elements.

type DateTime

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

DateTime is a FluentType representing a date/time (FluentDateTime in fluent.js). It stores a time.Time plus an option bag passed to the DateTimeFormatter.

func NewDateTime

func NewDateTime(value time.Time, opts DateTimeOptions) *DateTime

NewDateTime constructs a DateTime with the given time and options.

func (*DateTime) Format

func (d *DateTime) Format(scope *Scope) string

Format renders the datetime using the bundle's DateTimeFormatter.

func (*DateTime) Opts

func (d *DateTime) Opts() DateTimeOptions

Opts returns the datetime's formatting options.

func (*DateTime) Value

func (d *DateTime) Value() time.Time

Value returns the wrapped time.Time.

type DateTimeFormatter

type DateTimeFormatter interface {
	FormatDateTime(locale string, t time.Time, opts DateTimeOptions) string
}

DateTimeFormatter renders a time to a string for a given locale and options.

type DateTimeOptions

type DateTimeOptions struct {
	Hour12                 *bool
	Weekday                string
	Era                    string
	Year                   string
	Month                  string
	Day                    string
	Hour                   string
	Minute                 string
	Second                 string
	TimeZoneName           string
	DateStyle              string
	TimeStyle              string
	DayPeriod              string
	FractionalSecondDigits *int
	Calendar               string
	NumberingSystem        string
	TimeZone               string
}

DateTimeOptions carries the options that the DATETIME() builtin and FluentDateTime accept. It mirrors the subset of Intl.DateTimeFormatOptions used by fluent.js. Pointer fields distinguish "unset" from a zero value.

type Expression

type Expression = any

Expression is the union of all placeable expression types. Concrete type is one of: *SelectExpression, *VariableReference, *TermReference, *MessageReference, *FunctionReference, *StringLiteral, *NumberLiteral.

type FluentString

type FluentString string

FluentString is the FluentValue for a plain string (the JS string primitive).

func (FluentString) Format

func (s FluentString) Format(_ *Scope) string

Format returns the string unchanged.

type Function

type Function func(positional []Value, named map[string]Value) (Value, error)

Function is the signature of a Fluent builtin/runtime function. It receives positional and named Value arguments and returns a Value. Returning a non-nil error (or panicking) routes through the resolver's fault-tolerant error path, rendering `{NAME()}`. Mirrors FluentFunction in fluent.js.

type FunctionReference

type FunctionReference struct {
	Name string
	Args []any // each element is an Expression or *NamedArgument
}

FunctionReference corresponds to ast.ts `FunctionReference` (type: "func").

type Literal

type Literal = any

Literal is the union of StringLiteral and NumberLiteral. Concrete type is either *StringLiteral or *NumberLiteral.

type Message

type Message struct {
	ID         string
	Value      Pattern // nil if the message has no value
	Attributes map[string]Pattern
}

Message is the raw runtime message shape `{id, value, attributes}`.

type MessageReference

type MessageReference struct {
	Name string
	Attr string // empty string means no attribute
}

MessageReference corresponds to ast.ts `MessageReference` (type: "mesg").

type NamedArgument

type NamedArgument struct {
	Name  string
	Value Literal // either *StringLiteral or *NumberLiteral
}

NamedArgument corresponds to ast.ts `NamedArgument` (type: "narg").

type None

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

None is a FluentType representing no correct value (FluentNone in fluent.js). It renders missing references using the fluent.js fallback convention: a missing variable as `{$name}`, a missing message as `{message}`, a missing term as `{-term}`, a failed function as `{FUNC()}`. The default fallback is `???` which renders as `{???}`.

func NewNone

func NewNone(value string) *None

NewNone constructs a None with the given fallback inner value.

func (*None) Format

func (n *None) Format(_ *Scope) string

Format renders the None as `{value}`.

func (*None) Value

func (n *None) Value() string

Value returns the raw fallback string (without braces).

type Number

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

Number is a FluentType representing a number (FluentNumber in fluent.js). It stores the numeric value plus an option bag passed to the NumberFormatter.

func NewNumber

func NewNumber(value float64, opts NumberOptions) *Number

NewNumber constructs a Number with the given value and options.

func (*Number) Format

func (n *Number) Format(scope *Scope) string

Format renders the number using the bundle's NumberFormatter. A deferred option error is reported via the scope and the value falls back to its plain decimal rendering, mirroring Intl.NumberFormat throwing in fluent.js.

func (*Number) Opts

func (n *Number) Opts() NumberOptions

Opts returns the number's formatting options.

func (*Number) Value

func (n *Number) Value() float64

Value returns the wrapped numeric value.

type NumberFormatter

type NumberFormatter interface {
	FormatNumber(locale string, n float64, opts NumberOptions) string
}

NumberFormatter renders a number to a string for a given locale and options.

type NumberLiteral

type NumberLiteral struct {
	Value     float64
	Precision int
}

NumberLiteral corresponds to ast.ts `NumberLiteral` (type: "num").

type NumberOptions

type NumberOptions struct {
	Style                    string // "decimal" | "currency" | "percent" | "unit"
	Currency                 string
	CurrencyDisplay          string
	Unit                     string
	UnitDisplay              string
	UseGrouping              *bool
	MinimumIntegerDigits     *int
	MinimumFractionDigits    *int
	MaximumFractionDigits    *int
	MinimumSignificantDigits *int
	MaximumSignificantDigits *int

	// Type selects the plural ruleset: "cardinal" (default) or "ordinal".
	Type string
}

NumberOptions carries the options that the NUMBER() builtin and FluentNumber accept. It mirrors the subset of Intl.NumberFormatOptions used by fluent.js. Pointer fields distinguish "unset" from a zero value, mirroring how fluent.js merges option bags.

type Pattern

type Pattern = any

Pattern is either a simple string or a complex pattern (slice of elements).

In fluent.js a Pattern is `string | Array<PatternElement>`. Go has no union type, so we model it as `any`, where the concrete value is either:

string          – a simple pattern, or
ComplexPattern  – a complex pattern with placeables.

A nil Pattern represents the absence of a value (e.g. a message with only attributes).

type PatternElement

type PatternElement = any

PatternElement is either a string (text run) or an Expression (placeable). Concrete type is either `string` or one of the Expression structs below.

type PluralRules

type PluralRules interface {
	// Cardinal returns the cardinal plural category for n.
	Cardinal(locale string, n float64, opts NumberOptions) string
	// Ordinal returns the ordinal plural category for n.
	Ordinal(locale string, n float64, opts NumberOptions) string
}

PluralRules returns CLDR plural categories for a number in a given locale. Implementations return one of: "zero", "one", "two", "few", "many", "other".

type Resource

type Resource struct {
	Body []any
}

Resource is a structure storing parsed localization entries. Body holds *Message and *Term values, mirroring FluentResource.body in fluent.js.

func NewResource

func NewResource(source string) (*Resource, []error)

NewResource parses an FTL source into a Resource. Parse errors for individual messages are recovered (the message is skipped); the returned error slice is currently always nil-or-empty since the runtime parser silently skips broken entries, matching fluent.js. The signature returns it for API symmetry.

type Scope

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

Scope stores the data required for a single pattern resolution and for error recovery. A new Scope is created per FormatPattern call on a complex pattern.

type SelectExpression

type SelectExpression struct {
	Selector Expression
	Variants []Variant
	Star     int
}

SelectExpression corresponds to ast.ts `SelectExpression` (type: "select").

type StringLiteral

type StringLiteral struct {
	Value string
}

StringLiteral corresponds to ast.ts `StringLiteral` (type: "str").

type Term

type Term struct {
	ID         string
	Value      Pattern
	Attributes map[string]Pattern
}

Term is the raw runtime term shape `{id, value, attributes}`.

type TermReference

type TermReference struct {
	Name string
	Attr string // empty string means no attribute (ast.ts uses null)
	Args []any  // each element is an Expression or *NamedArgument
}

TermReference corresponds to ast.ts `TermReference` (type: "term").

type TextTransform

type TextTransform func(string) string

TextTransform transforms the text parts of patterns. Mirrors TextTransform.

type Value

type Value interface {
	// Format renders this value to a string, optionally using the scope's
	// pluggable formatters. Mirrors FluentType.toString(scope) in fluent.js.
	Format(scope *Scope) string
}

Value is the base of Fluent's runtime type system. Every expression resolves to a Value. Callers convert a Value to its native string with Format.

type VariableReference

type VariableReference struct {
	Name string
}

VariableReference corresponds to ast.ts `VariableReference` (type: "var").

type Variant

type Variant struct {
	Key   Literal // either *StringLiteral or *NumberLiteral
	Value Pattern
}

Variant corresponds to ast.ts `Variant`.

Directories

Path Synopsis
Package fluentx provides locale-aware, CLDR-backed implementations of the pluggable formatting interfaces defined in the core fluent package (fluent.PluralRules, fluent.NumberFormatter, fluent.DateTimeFormatter).
Package fluentx provides locale-aware, CLDR-backed implementations of the pluggable formatting interfaces defined in the core fluent package (fluent.PluralRules, fluent.NumberFormatter, fluent.DateTimeFormatter).
Package langneg is a faithful Go port of @fluent/langneg.
Package langneg is a faithful Go port of @fluent/langneg.
Package localization is the high-level, synchronous localization layer for gofluent.
Package localization is the high-level, synchronous localization layer for gofluent.
Package syntax is an idiomatic Go port of @fluent/syntax (Project Fluent): a parser, serializer, and AST visitor for the Fluent localization format.
Package syntax is an idiomatic Go port of @fluent/syntax (Project Fluent): a parser, serializer, and AST visitor for the Fluent localization format.
ast
Package ast defines the Fluent abstract syntax tree, a faithful port of the data model from @fluent/syntax (ast.ts).
Package ast defines the Fluent abstract syntax tree, a faithful port of the data model from @fluent/syntax (ast.ts).

Jump to

Keyboard shortcuts

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