feat: release v0.26 portable i18n and presentation adapters
This commit is contained in:
221
i18n/pattern.go
Normal file
221
i18n/pattern.go
Normal file
@@ -0,0 +1,221 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"math"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
var paramName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_.-]*$`)
|
||||
var integerText = regexp.MustCompile(`^-?(0|[1-9][0-9]*)$`)
|
||||
|
||||
type part struct{ text, param string }
|
||||
type compiled struct {
|
||||
parts []part
|
||||
selectKey, pluralKey string
|
||||
cases map[string]*compiled
|
||||
}
|
||||
type budget struct{ nodes, bytes int }
|
||||
|
||||
func compile(p Pattern, schema map[string]Kind, used map[string]bool, b *budget, depth int) (*compiled, error) {
|
||||
b.nodes++
|
||||
b.bytes += len(p.Text)
|
||||
if depth > 24 || b.nodes > 100000 || b.bytes > 4<<20 || len(p.Text) > 64<<10 || !utf8.ValidString(p.Text) {
|
||||
return nil, &Diagnostic{Kind: "resource", Problem: "pattern exceeds limits or contains invalid UTF-8"}
|
||||
}
|
||||
c := &compiled{selectKey: p.Select, pluralKey: p.Plural}
|
||||
if p.Select != "" || p.Plural != "" {
|
||||
if p.Select != "" && p.Plural != "" || p.Text != "" || p.Literal || len(p.Cases) == 0 || len(p.Cases) > 128 {
|
||||
return nil, &Diagnostic{Kind: "pattern", Problem: "expected one select/plural branch with cases"}
|
||||
}
|
||||
key := p.Select
|
||||
if key == "" {
|
||||
key = p.Plural
|
||||
}
|
||||
kind, ok := schema[key]
|
||||
if !ok || p.Plural != "" && kind != Integer && kind != Number || p.Select != "" && kind != String && kind != Boolean {
|
||||
return nil, &Diagnostic{Kind: "parameter", Param: key, Problem: "branch requires a declared parameter of the appropriate type"}
|
||||
}
|
||||
used[key] = true
|
||||
if _, ok := p.Cases["other"]; !ok {
|
||||
return nil, &Diagnostic{Kind: "pattern", Problem: "branch requires other"}
|
||||
}
|
||||
c.cases = make(map[string]*compiled, len(p.Cases))
|
||||
for _, name := range sortedKeys(p.Cases) {
|
||||
if !validID(name) {
|
||||
return nil, &Diagnostic{Kind: "pattern", Problem: "invalid case name"}
|
||||
}
|
||||
if p.Plural != "" && !pluralCase(name) {
|
||||
return nil, &Diagnostic{Kind: "pattern", Problem: "invalid plural category or exact integer case"}
|
||||
}
|
||||
child, err := compile(p.Cases[name], schema, used, b, depth+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.cases[name] = child
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
if p.Cases != nil {
|
||||
return nil, &Diagnostic{Kind: "pattern", Problem: "cases require select or plural"}
|
||||
}
|
||||
if p.Literal {
|
||||
c.parts = []part{{text: p.Text}}
|
||||
return c, nil
|
||||
}
|
||||
var literal strings.Builder
|
||||
flush := func() {
|
||||
if literal.Len() > 0 {
|
||||
c.parts = append(c.parts, part{text: literal.String()})
|
||||
literal.Reset()
|
||||
}
|
||||
}
|
||||
for i := 0; i < len(p.Text); {
|
||||
ch := p.Text[i]
|
||||
if ch != '{' && ch != '}' {
|
||||
literal.WriteByte(ch)
|
||||
i++
|
||||
continue
|
||||
}
|
||||
if i+1 < len(p.Text) && p.Text[i+1] == ch {
|
||||
literal.WriteByte(ch)
|
||||
i += 2
|
||||
continue
|
||||
}
|
||||
if ch == '}' {
|
||||
return nil, &Diagnostic{Kind: "pattern", Problem: "unescaped closing brace"}
|
||||
}
|
||||
end := strings.IndexByte(p.Text[i+1:], '}')
|
||||
if end < 0 {
|
||||
return nil, &Diagnostic{Kind: "pattern", Problem: "unclosed placeholder"}
|
||||
}
|
||||
name := p.Text[i+1 : i+1+end]
|
||||
if _, ok := schema[name]; !ok {
|
||||
return nil, &Diagnostic{Kind: "parameter", Param: name, Problem: "undeclared placeholder"}
|
||||
}
|
||||
used[name] = true
|
||||
flush()
|
||||
c.parts = append(c.parts, part{param: name})
|
||||
i += end + 2
|
||||
}
|
||||
flush()
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func pluralCase(s string) bool {
|
||||
switch s {
|
||||
case "zero", "one", "two", "few", "many", "other":
|
||||
return true
|
||||
}
|
||||
if !strings.HasPrefix(s, "=") {
|
||||
return false
|
||||
}
|
||||
n, err := strconv.ParseInt(s[1:], 10, 64)
|
||||
return err == nil && strconv.FormatInt(n, 10) == s[1:]
|
||||
}
|
||||
|
||||
func scalar(kind Kind, value any) (string, error) {
|
||||
bad := func() (string, error) {
|
||||
return "", &Diagnostic{Kind: "parameter", Problem: "wrong type, range, or non-finite value"}
|
||||
}
|
||||
if value == nil {
|
||||
return bad()
|
||||
}
|
||||
v := reflect.ValueOf(value)
|
||||
switch kind {
|
||||
case String:
|
||||
if v.Kind() == reflect.String && v.Type() != reflect.TypeOf(json.Number("")) && utf8.ValidString(v.String()) && len(v.String()) <= 64<<10 {
|
||||
return v.String(), nil
|
||||
}
|
||||
case Boolean:
|
||||
if v.Kind() == reflect.Bool {
|
||||
return strconv.FormatBool(v.Bool()), nil
|
||||
}
|
||||
case Integer:
|
||||
if n, ok := value.(json.Number); ok {
|
||||
x, e := n.Int64()
|
||||
if e == nil {
|
||||
return strconv.FormatInt(x, 10), nil
|
||||
}
|
||||
return bad()
|
||||
}
|
||||
switch v.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return strconv.FormatInt(v.Int(), 10), nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
if v.Uint() <= math.MaxInt64 {
|
||||
return strconv.FormatUint(v.Uint(), 10), nil
|
||||
}
|
||||
}
|
||||
case Number:
|
||||
var n float64
|
||||
if j, ok := value.(json.Number); ok {
|
||||
var e error
|
||||
n, e = j.Float64()
|
||||
if e != nil {
|
||||
return bad()
|
||||
}
|
||||
} else {
|
||||
switch v.Kind() {
|
||||
case reflect.Float32, reflect.Float64:
|
||||
n = v.Float()
|
||||
default:
|
||||
return bad()
|
||||
}
|
||||
}
|
||||
if !math.IsNaN(n) && !math.IsInf(n, 0) {
|
||||
if n == 0 {
|
||||
return "0", nil
|
||||
}
|
||||
// ECMAScript's plain/scientific thresholds, shared with the browser runtime.
|
||||
format := byte('f')
|
||||
if math.Abs(n) < 1e-6 || math.Abs(n) >= 1e21 {
|
||||
format = 'e'
|
||||
}
|
||||
s := strconv.FormatFloat(n, format, -1, 64)
|
||||
s = strings.ReplaceAll(strings.ReplaceAll(s, "e-0", "e-"), "e+0", "e+")
|
||||
return s, nil
|
||||
}
|
||||
}
|
||||
return bad()
|
||||
}
|
||||
|
||||
func (c *compiled) render(tag language.Tag, values map[string]string) (string, error) {
|
||||
if c.cases != nil {
|
||||
key := values[c.selectKey]
|
||||
if c.pluralKey != "" {
|
||||
n := values[c.pluralKey]
|
||||
key = "=" + n
|
||||
if _, ok := c.cases[key]; !ok {
|
||||
var err error
|
||||
key, err = category(tag, n)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
}
|
||||
next := c.cases[key]
|
||||
if next == nil {
|
||||
next = c.cases["other"]
|
||||
}
|
||||
return next.render(tag, values)
|
||||
}
|
||||
var out strings.Builder
|
||||
for _, p := range c.parts {
|
||||
value := p.text
|
||||
if p.param != "" {
|
||||
value = values[p.param]
|
||||
}
|
||||
if out.Len()+len(value) > 1<<20 {
|
||||
return "", &Diagnostic{Kind: "resource", Problem: "rendered message exceeds 1 MiB"}
|
||||
}
|
||||
out.WriteString(value)
|
||||
}
|
||||
return out.String(), nil
|
||||
}
|
||||
Reference in New Issue
Block a user