alx

package module
v0.1.0 Latest Latest
Warning

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

Go to latest
Published: Aug 8, 2026 License: MIT Imports: 5 Imported by: 1

README

alx

Parse the header once. alx tells you which language to serve.

Go Reference Tests Go Report Card Coverage Zero dependencies Go version MIT License

alx is an Accept-Language toolkit for Go (RFC 9110 / RFC 4647), with zero third-party dependencies. It gives you two layers of usage:

  • Direct functions — parse, sort, normalize and match a header in one call.
  • Resolver — decide the request's language across URL, query, cookie, header and default, and tell you which one won.

On top of either layer you get quality-value ordering, wildcards, fallback chains, canonical tag casing, and framework adapters that stay out of your dependency graph.

Installation

go get github.com/bakhod1r/alx

🚀 The killer feature: one line at the edge

Content negotiation is usually a hand-rolled strings.Split that quietly ignores q= values and serves the wrong language. It is one call:

func handler(w http.ResponseWriter, r *http.Request) {
    tag, ok := alx.Best(r.Header.Get("Accept-Language"), []string{"en", "uz", "ru"})
    if !ok {
        tag = "en"
    }
    alx.SetContentLanguage(w, tag)
    alx.SetVary(w) // so caches key on the header you just read
}

Best sorts by quality, walks the fallback chain (zh-Hant-TW → zh-Hant → zh), honours *, and returns your supported tag — spelled exactly as you wrote it, not as the client sent it.

🌍 Resolver: the language is not only in the header

Real apps read the language from a URL prefix, a ?lang= link, a cookie the user set last week, and only then the browser. The Resolver does that in one place, in the order you declare:

resolver := alx.NewResolver(
    alx.WithSupported("en", "uz", "ru"),
    alx.WithURL(),            // /uz/products
    alx.WithQuery("lang"),    // ?lang=uz
    alx.WithCookie("lang"),
    alx.WithHeader(),         // Accept-Language, or the headers you name
    alx.WithFallback("uz-Cyrl", "ru"),
    alx.WithDefault("en"),
)

res := resolver.ResolveRequest(r)

A Resolution reports the decision and its provenance:

res.Tag      // "uz"
res.Language // "uz"
res.Script   // "" — e.g. "Hant"
res.Region   // "" — e.g. "UZ"
res.Quality  // 1.0
res.Matched  // the supported tag that matched
res.Source   // SourceURL | SourceQuery | SourceCookie | SourceHeader | SourceDefault

res.Source is the field that makes this debuggable: when a user insists the site is in the wrong language, you can log whether it came from their cookie or their browser.

For a bare header string, use resolver.Resolve(header).

🔤 Parsing

langs := alx.ParseSorted("uz-UZ,uz;q=0.9,en;q=0.8,*;q=0.1")

langs[0].Tag      // "uz-UZ"
langs[0].Language // "uz"
langs[0].Region   // "UZ"
langs[0].Script   // ""
langs[0].Quality  // 1.0
langs[0].String() // "uz-UZ"  — round-trips as "uz;q=0.9" when q != 1
Function Behaviour
Parse / ParseRaw Lenient. Header order preserved, malformed segments dropped.
ParseSorted Lenient, then sorted by descending quality.
ParseStrict Returns error on any malformed segment.
Validate ParseStrict with the result discarded — error only.
PrimaryLanguage Allocation-free fast path: first tag, no q= handling.
Sort Stable sort of []Language by descending quality.

Strict mode returns a *ParseError that says where it broke:

_, err := alx.ParseStrict("en;q=banana")
var pe *alx.ParseError
if errors.As(err, &pe) {
    pe.Position // byte offset in the header
    pe.Token    // the offending token
    pe.Reason   // why it was rejected
}

Equal quality values keep header order, so a client's own preference ordering survives — that is what makes the sort stable rather than merely correct.

🎯 Matching strategies

alx.Best(header, supported)                    // Lookup — the sensible default
alx.Match(header, supported)                   // alias for Best
alx.MatchWith(header, supported, alx.Exact)    // tag must match exactly
alx.MatchWith(header, supported, alx.Basic)    // base language only: en-US matches en
alx.MatchWith(header, supported, alx.Lookup)   // fallback chain: zh-Hant-TW → zh-Hant → zh
Strategy en-GB against ["en"] zh-Hant-TW against ["zh"]
Exact no match no match
Basic en zh
Lookup en zh

Fallback is an alias for Lookup; PreferRegion currently behaves as Lookup.

The chain itself is exported, so you can drive your own lookup — translation files, a CDN path, whatever:

alx.Fallbacks("zh-Hant-TW") // ["zh-Hant-TW", "zh-Hant", "zh"]

🧹 Normalization

Clients send EN-us, config files hold en_US, your file names use en-US. One function each way:

alx.Normalize("ZH-hant-tw") // "zh-Hant-TW"  — language lower, script Title, region UPPER
alx.Canonical("en_US")      // "en-US"       — underscores to dashes, then Normalize

🌐 Framework adapters

Each adapter is its own Go module, so importing alx never pulls a web framework into your build. Install only the one you use:

Framework Import
Gin go get github.com/bakhod1r/alx/gin
Fiber go get github.com/bakhod1r/alx/fiber
Echo go get github.com/bakhod1r/alx/echo
Iris go get github.com/bakhod1r/alx/iris
Beego go get github.com/bakhod1r/alx/beego
fasthttp go get github.com/bakhod1r/alx/fasthttp

Every adapter exposes the same two functions, typed for that framework's context:

import alxgin "github.com/bakhod1r/alx/gin"

func handler(c *gin.Context) {
    lang := alxgin.FromRequest(c) // best Language
    all := alxgin.Languages(c)    // all languages, sorted by quality
}
import alxfiber "github.com/bakhod1r/alx/fiber"

func handler(c *fiber.Ctx) error {
    lang := alxfiber.FromRequest(c)
    return c.SendString(lang.Tag)
}

🔌 net/http helpers

alx.FromRequest(r)      // highest-quality Language
alx.Languages(r)        // all languages, sorted
alx.SetContentLanguage(w, "uz")
alx.SetVary(w)

FromRequest and Languages check Accept-Language, then Language, then Lang — some clients and proxies use the shorter spellings. Both return zero values on a nil request rather than panicking, so they are safe in middleware that runs before routing.

Development

The repository is a Go workspace, so the adapter modules build against the local core:

make test   # go test -cover across every module
make race   # go test -race
make cover  # coverage report for the core
make all    # fmt + vet + test

Contributions welcome — see CONTRIBUTING.md. Releases are documented in RELEASING.md, changes in CHANGELOG.md.

License

MIT © bakhod1r

Documentation

Overview

Package alx parses, sorts and matches HTTP Accept-Language headers (RFC 9110 / RFC 4647).

The core package depends only on the standard library. Framework adapters live in separate submodules (github.com/bakhod1r/alx/gin, /fiber, /echo, /iris, /beego, /fasthttp) so importing alx never pulls a web framework into your build.

Basic matching:

tag, ok := alx.Best(r.Header.Get("Accept-Language"), []string{"en", "uz", "ru"})

Multi-source resolution (URL segment, query, cookie, header, default):

res := alx.NewResolver(
	alx.WithSupported("en", "uz", "ru"),
	alx.WithURL(),
	alx.WithQuery("lang"),
	alx.WithCookie("lang"),
	alx.WithHeader(),
	alx.WithDefault("en"),
).ResolveRequest(r)

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func Best

func Best(header string, supported []string) (string, bool)

Best matches an Accept-Language header against a list of supported languages using the default Lookup strategy, returning the best match.

func Canonical

func Canonical(tag string) string

Canonical transforms the input tag into a canonical format by replacing underscores with dashes and then applying Normalization.

func Fallbacks

func Fallbacks(tag string) []string

Fallbacks generates a slice of language tags representing the fallback chain for a given tag. For example, "zh-Hant-TW" results in ["zh-Hant-TW", "zh-Hant", "zh"].

func Match

func Match(header string, supported []string) (string, bool)

Match is an alias for Best, providing a simple way to get the best match.

func MatchWith

func MatchWith(header string, supported []string, strategy MatchStrategy) (string, bool)

MatchWith matches an Accept-Language header against a list of supported languages using the specified matching strategy.

func Normalize

func Normalize(tag string) string

Normalize normalizes a language tag to its canonical casing. It formats the language code to lowercase, script code to titlecase, and region code to uppercase. For example: "EN-us" -> "en-US", "ZH-hant-tw" -> "zh-Hant-TW".

func PrimaryLanguage

func PrimaryLanguage(header string) string

PrimaryLanguage returns the most preferred language from the Accept-Language header using a zero-allocation fast path. It simply returns the first valid language tag found without parsing full Q-values or sorting.

func SetContentLanguage

func SetContentLanguage(w http.ResponseWriter, lang string)

SetContentLanguage sets the Content-Language HTTP response header.

func SetVary

func SetVary(w http.ResponseWriter)

SetVary adds Accept-Language to the Vary HTTP response header.

func Sort

func Sort(langs []Language)

Sort sorts a slice of Language structs based on their Quality values in descending order. It uses a stable sort to preserve the original order of elements with the same Quality, which matches the order they appeared in the header.

func Validate

func Validate(header string) error

Validate checks if the Accept-Language header is completely valid. It returns an error if the header contains malformed elements.

Types

type Language

type Language struct {
	Tag      string  // The full language tag (e.g., "en-US")
	Language string  // The base language (e.g., "en")
	Script   string  // The script (e.g., "Hant")
	Region   string  // The region (e.g., "US")
	Quality  float64 // The quality value (q-factor), default is 1.0
}

Language represents a parsed language tag and its associated quality value.

func FromRequest

func FromRequest(r *http.Request) Language

FromRequest parses the Accept-Language header from the request and returns the highest quality Language.

func Languages

func Languages(r *http.Request) []Language

Languages extracts and parses the language from the request, checking Accept-Language, Language, and Lang headers.

func Parse

func Parse(header string) []Language

Parse parses the Accept-Language header in lenient mode, preserving the original order.

func ParseRaw

func ParseRaw(header string) []Language

ParseRaw parses the Accept-Language header and returns the languages in the exact order they appeared. It ignores malformed segments (lenient mode).

func ParseSorted

func ParseSorted(header string) []Language

ParseSorted parses the Accept-Language header, removes invalid segments, and returns the valid languages sorted by quality (descending).

func ParseStrict

func ParseStrict(header string) ([]Language, error)

ParseStrict parses the Accept-Language header and returns an error if any segment is malformed.

func (Language) String

func (l Language) String() string

String returns the string representation of the language, including the q-value if it's not 1.0.

type MatchStrategy

type MatchStrategy int

MatchStrategy defines how parsed languages are matched against supported languages.

const (
	// Exact requires an exact match of the language tag.
	Exact MatchStrategy = iota
	// Basic matches based on the base language only (e.g., en-US matches en).
	Basic
	// Lookup uses standard fallback lookup (e.g., zh-Hant-TW -> zh-Hant -> zh).
	Lookup
	// Fallback is an alias for Lookup.
	Fallback
	// PreferRegion prefers matching the same region if possible, falls back to Lookup.
	PreferRegion
)

type ParseError

type ParseError struct {
	Position int
	Token    string
	Reason   string
}

ParseError describes an error that occurred while parsing an Accept-Language header.

func (*ParseError) Error

func (e *ParseError) Error() string

Error implements the error interface.

type Resolution

type Resolution struct {
	Tag      string
	Language string
	Script   string
	Region   string
	Source   Source
	Quality  float64
	Matched  string
}

Resolution represents the result of resolving a locale.

type Resolver

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

Resolver resolves the best language for an HTTP request or header.

func NewResolver

func NewResolver(opts ...ResolverOption) *Resolver

NewResolver creates a new Resolver with the given options.

func (*Resolver) Resolve

func (r *Resolver) Resolve(header string) Resolution

Resolve resolves the best matching language for an Accept-Language header string.

func (*Resolver) ResolveRequest

func (r *Resolver) ResolveRequest(req *http.Request) Resolution

ResolveRequest resolves the best matching language for an HTTP request based on the configured extractors (URL, Query, Cookie, Header) and their priority.

type ResolverOption

type ResolverOption func(*Resolver)

ResolverOption configures the Resolver.

func WithCookie

func WithCookie(name string) ResolverOption

WithCookie enables extracting the language from a cookie.

func WithDefault

func WithDefault(lang string) ResolverOption

WithDefault sets the default language to use if no match is found.

func WithFallback

func WithFallback(tag, fallback string) ResolverOption

WithFallback adds a custom fallback mapping.

func WithHeader

func WithHeader(headers ...string) ResolverOption

WithHeader enables extracting the language from HTTP headers. If no headers are specified, it defaults to "Accept-Language".

func WithQuery

func WithQuery(param string) ResolverOption

WithQuery enables extracting the language from a query parameter.

func WithSupported

func WithSupported(langs ...string) ResolverOption

WithSupported sets the list of supported languages.

func WithURL

func WithURL() ResolverOption

WithURL enables extracting the language from the first path segment of the URL (e.g. /en/products).

type Source

type Source string

Source identifies where the language was resolved from.

const (
	SourceURL     Source = "url"
	SourceQuery   Source = "query"
	SourceCookie  Source = "cookie"
	SourceHeader  Source = "header"
	SourceDefault Source = "default"
)

The sources a language can be resolved from, in the order a Resolver is typically configured to consult them.

Jump to

Keyboard shortcuts

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