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 }