Files
go-omni/i18n/httpi18n/http.go

132 lines
3.5 KiB
Go

// Package httpi18n negotiates an i18n language for standard HTTP requests.
package httpi18n
import (
"fmt"
"net/http"
"strings"
"git.yuntuo.hk/k2safe/go-omni/i18n"
"golang.org/x/text/language"
)
// Resolver supplies ordered language tags, e.g. from an authenticated preference.
// It must be safe for concurrent requests. It does not receive a mutable Bundle.
// A panic is treated as no preference, preserving request execution.
type Resolver func(*http.Request) []string
type Option func(*Policy) error
type Policy struct {
bundle *i18n.Bundle
resolver Resolver
vary []string
private bool
}
// WithResolver overrides the default Accept-Language resolver. Cache variation
// is explicit. With no varying headers, responses are marked private.
func WithResolver(resolver Resolver, varyHeaders ...string) Option {
return func(p *Policy) error {
if resolver == nil {
return fmt.Errorf("httpi18n: nil resolver")
}
p.resolver = resolver
p.vary = nil
p.private = len(varyHeaders) == 0
for _, h := range varyHeaders {
if !headerName(h) {
return fmt.Errorf("httpi18n: invalid Vary header")
}
p.vary = append(p.vary, http.CanonicalHeaderKey(h))
}
return nil
}
}
func headerName(s string) bool {
if s == "" {
return false
}
for _, c := range s {
if c > 127 || !(c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.ContainsRune("!#$%&'*+-.^_`|~", c)) {
return false
}
}
return true
}
func New(bundle *i18n.Bundle, options ...Option) (*Policy, error) {
if bundle == nil {
return nil, fmt.Errorf("httpi18n: bundle required")
}
p := &Policy{bundle: bundle, vary: []string{"Accept-Language"}, resolver: func(r *http.Request) []string {
header := r.Header.Get("Accept-Language")
if len(header) > 8192 {
return nil
}
tags, _, err := language.ParseAcceptLanguage(header)
if err != nil {
return nil
}
out := []string{}
for _, tag := range tags {
if len(out) == 64 {
break
}
out = append(out, tag.String())
}
return out
}}
for _, o := range options {
if o == nil {
return nil, fmt.Errorf("httpi18n: nil option")
}
if err := o(p); err != nil {
return nil, err
}
}
return p, nil
}
// Apply installs the negotiated language before application middleware. An
// explicit context preference takes priority and makes the response private.
// It does not claim Content-Language: individual messages may use fallbacks.
func (p *Policy) Apply(w http.ResponseWriter, r *http.Request) *http.Request {
if p == nil {
return r
}
preferences := []string{}
explicit := i18n.Locale(r.Context())
if explicit != "" {
preferences = append(preferences, explicit)
}
preferences = append(preferences, p.resolve(r)...)
for _, h := range p.vary {
addVary(w.Header(), h)
}
if p.private || explicit != "" {
w.Header().Add("Cache-Control", "private")
}
return r.WithContext(i18n.WithLocale(r.Context(), p.bundle.Match(preferences...)))
}
func (p *Policy) resolve(r *http.Request) (preferences []string) {
defer func() {
if recover() != nil {
preferences = nil
}
}()
return p.resolver(r)
}
func addVary(h http.Header, value string) {
for _, line := range h.Values("Vary") {
for _, v := range strings.Split(line, ",") {
if strings.EqualFold(strings.TrimSpace(v), value) || strings.TrimSpace(v) == "*" {
return
}
}
}
h.Add("Vary", value)
}
func (p *Policy) Wrap(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { next.ServeHTTP(w, p.Apply(w, r)) })
}