MessageFormat Go
A Go implementation of Unicode MessageFormat 2.0 for parsing, validating, and formatting localized messages
Features
- MessageFormat 2.0: Parse, validate, select, and format messages with the Unicode MessageFormat 2.0 model.
- Established API surface: Use stable constructor concepts, option names, formatting methods, and runtime defaults across MessageFormat 2 implementations.
- Locale-aware functions: Format numbers, dates, currencies, percentages, offsets, strings, and units through
github.com/agentable/go-intl. - Custom formatters: Register application functions with
WithFunctionorWithFunctions. - Structured rendering: Use
FormatToPartsfor rich text, markup-aware rendering, and post-processing. - Safe data-model tooling: Construct and serialize the public model with explicit errors and deterministic source output.
- Migration path: Keep existing ICU MessageFormat v1 code on the supported
github.com/kaptinlin/messageformat-go/mf1module. - Pinned corpus verification: Reproduce behavior against the checked-in Unicode MessageFormat Working Group corpus with
task test-officialortask test-v2.
Installation
go get github.com/kaptinlin/messageformat-goRequires Go 1.26.5+.
For ICU MessageFormat v1 compatibility, install its independent module:
go get github.com/kaptinlin/messageformat-go/mf1@latestThen import:
import "github.com/kaptinlin/messageformat-go/mf1"Quick Start
package main
import (
"fmt"
"log"
"github.com/kaptinlin/messageformat-go"
)
func main() {
mf, err := messageformat.Parse([]string{"en"}, "Hello, {$name}!")
if err != nil {
log.Fatal(err)
}
out, err := mf.Format(map[string]any{"name": "World"})
if err != nil {
log.Fatal(err)
}
fmt.Println(out) // Hello, World!
}Core API
| API | Purpose |
|---|---|
Parse(locales, source, options...) | Parse source text into a formatter |
Compile(locales, message, options...) | Build a formatter from a parsed data model |
(*MessageFormat).Format(values) | Format to a string and return runtime diagnostics |
(*MessageFormat).FormatToParts(values) | Format to structured parts and return runtime diagnostics |
datamodel.ParseMessage(source) | Parse source into the public data model |
datamodel.StringifyMessage(message) | Convert the data model back to source or return an error |
datamodel.ValidateMessage(message, onError) | Validate a parsed message |
Full API details are available on pkg.go.dev and in docs/api-reference.md.
Import github.com/kaptinlin/messageformat-go/pkg/datamodel for direct model
construction, validation, visitation, and type guards.
Common Usage
Select Messages
Declare selectors before .match messages:
mf, err := messageformat.Parse([]string{"en"}, `
.input {$count :number}
.match $count
0 {{No items}}
one {{One item}}
* {{{$count} items}}
`)
if err != nil {
log.Fatal(err)
}
out, err := mf.Format(map[string]any{"count": 3})
if err != nil {
log.Fatal(err)
}
fmt.Println(out)Built-In Formatting
Use MessageFormat functions directly in source text:
mf, err := messageformat.Parse(
[]string{"en"},
"Total: {$amount :currency currency=USD}",
)
if err != nil {
log.Fatal(err)
}
out, err := mf.Format(map[string]any{"amount": 29.99})
if err != nil {
log.Fatal(err)
}
fmt.Println(out)Stable default functions include :number, :integer, :string, :offset, :currency, and :percent. Draft date/time/unit functions (:date, :datetime, :time, :unit) are available only when supplied explicitly with WithFunctions(functions.DraftFunctionMap()); :math is an extension function and must be supplied explicitly with WithFunction.
Structured Parts
Use FormatToParts when a UI needs structured output instead of one string. Format follows the documented string conversion path; FormatToParts keeps the resolved part values for rich rendering.
Package-owned expression parts retain their concrete Go types when universal
u:id or u:dir metadata is applied, so type-specific accessors remain
available.
parts, err := mf.FormatToParts(map[string]any{"amount": 29.99})
if err != nil {
log.Fatal(err)
}
for _, part := range parts {
fmt.Printf("%s: %v\n", part.Type(), part.Value())
}Custom Functions
Custom functions receive locale context, resolved options, and the operand value:
func uppercase(
ctx messageformat.MessageFunctionContext,
options functions.Options,
operand any,
) messagevalue.MessageValue {
locale := "en"
if locales := ctx.Locales(); len(locales) > 0 {
locale = locales[0]
}
return messagevalue.NewStringValue(
strings.ToUpper(fmt.Sprint(operand)),
locale,
ctx.Source(),
)
}
mf, err := messageformat.Parse(
[]string{"en"},
"Hello, {$name :uppercase}!",
messageformat.WithFunction("uppercase", uppercase),
)See docs/custom-functions.md and examples/custom-functions for complete examples.
Use ICU MessageFormat 1
The independent mf1 module keeps ICU MessageFormat v1 patterns on a typed Go boundary. Construct from one locale, compile once, and reuse the returned message:
messageFormat, err := mf1.New("en", nil)
if err != nil {
log.Fatal(err)
}
compiled, err := messageFormat.Compile("Hello, {name}!")
if err != nil {
log.Fatal(err)
}
out, err := compiled.Format(map[string]any{"name": "World"})
if err != nil {
log.Fatal(err)
}
fmt.Println(out) // Hello, World!Use mf1.NewWithPlural with a mf1.PluralProfile for caller-supplied plural
behavior. Malformed locale tags return an error; syntactically valid locales
without plural data use the module's stable fallback locale.
Plural operands preserve exact Go integers and base-10 integer strings through
exact matching, offset calculation, category selection, and # formatting.
Selector failures are returned by both output projections rather than silently
choosing other.
Configuration
Use functional options for focused constructor changes:
| Option | Purpose | Default |
|---|---|---|
WithBidiIsolation(strategy) | Control Unicode bidi isolation markers | BidiDefault |
WithDir(direction) | Set message base direction | Locale-derived |
WithLocaleMatcher(matcher) | Select locale matching behavior | LocaleBestFit |
WithFunction(name, fn) | Register one custom function | Built-ins only |
WithFunctions(funcs) | Register multiple custom functions | Built-ins only |
Example:
mf, err := messageformat.Parse(
[]string{"ar"},
"مرحبا {$name}!",
messageformat.WithDir(messageformat.DirRTL),
)BidiIsolation, Direction, and LocaleMatcher use closed typed vocabularies. Parse and Compile return an error matching ErrInvalidOption when a value is outside its exported constants.
Use messageformat.Options(...) when a struct is more convenient:
mf, err := messageformat.Parse(
[]string{"en"},
"Hello, {$name}!",
messageformat.Options(messageformat.MessageFormatOptions{
BidiIsolation: messageformat.BidiDefault,
LocaleMatcher: messageformat.LocaleBestFit,
}),
)Conformance
The tests/messageformat-wg gitlink pins the official Unicode MessageFormat Working Group corpus used by this repository. A passing run describes that exact pin.
task test-official # Official MessageFormat 2.0 suite only
task test-v2 # Package tests plus official suite, with race detectionProject design contracts live in SPECS/.
Documentation
| Guide | Use it for |
|---|---|
docs/getting-started.md | Installation and first steps |
docs/message-syntax.md | MessageFormat 2.0 syntax |
docs/formatting-functions.md | Built-in formatter behavior |
docs/custom-functions.md | Custom formatter authoring |
docs/error-handling.md | Syntax, resolution, and selection errors |
docs/api-reference.md | Public API reference |
SPECS/ | Design contracts and architecture boundaries |
Runnable examples live in examples/.
Development
task submodules # Initialize official test suite submodule
task test # Run all tests with race detection
task test-v2 # Run package tests and the official suite
task lint # Check tidy state and lint both modules
task verify # Run read-only vet, lint, test, and vuln checks for both modules
task examples # Run example programsContributing
Contributions are welcome. Start with CONTRIBUTING.md, then run task verify before opening a pull request.
License
This project is licensed under the MIT License - see the LICENSE file for details.