feat: release v0.26 portable i18n and presentation adapters
This commit is contained in:
196
i18n/plural.go
Normal file
196
i18n/plural.go
Normal file
@@ -0,0 +1,196 @@
|
||||
package i18n
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
// PluralRulesVersion pins both runtimes to the same Unicode data. Changing it
|
||||
// is a presentation compatibility change, independent of message format Version.
|
||||
const PluralRulesVersion = "cldr-46.1"
|
||||
|
||||
//go:embed cldr/plurals.json
|
||||
var pluralData []byte
|
||||
|
||||
//go:embed cldr/LICENSE
|
||||
var unicodeLicense string
|
||||
|
||||
type pluralRange struct{ lo, hi *big.Int }
|
||||
type pluralCondition struct {
|
||||
operand byte
|
||||
modulus *big.Int
|
||||
negate bool
|
||||
ranges []pluralRange
|
||||
}
|
||||
type pluralRule [][]pluralCondition // OR of AND groups, per the CLDR grammar.
|
||||
type pluralSet struct {
|
||||
text map[string]string
|
||||
rules map[string]pluralRule
|
||||
}
|
||||
|
||||
var pluralConditionSyntax = regexp.MustCompile(`^([nivwftec])(?: % ([1-9][0-9]*))? (!=|=) ([0-9]+(?:\.\.[0-9]+)?(?:,\s*[0-9]+(?:\.\.[0-9]+)?)*)$`)
|
||||
var cardinalRules = loadCardinalRules()
|
||||
|
||||
func bigDecimal(s string) *big.Int {
|
||||
if s == "" {
|
||||
return new(big.Int)
|
||||
}
|
||||
n, ok := new(big.Int).SetString(s, 10)
|
||||
if !ok {
|
||||
panic("i18n: invalid embedded plural integer")
|
||||
}
|
||||
return n
|
||||
}
|
||||
func parsePluralRule(text string) (pluralRule, error) {
|
||||
var result pluralRule
|
||||
if len(text) > 2048 {
|
||||
return nil, fmt.Errorf("plural rule too large")
|
||||
}
|
||||
for _, or := range strings.Split(text, " or ") {
|
||||
var group []pluralCondition
|
||||
for _, and := range strings.Split(or, " and ") {
|
||||
parts := pluralConditionSyntax.FindStringSubmatch(strings.TrimSpace(and))
|
||||
if parts == nil {
|
||||
return nil, fmt.Errorf("invalid CLDR plural condition")
|
||||
}
|
||||
c := pluralCondition{operand: parts[1][0], negate: parts[3] == "!="}
|
||||
if parts[2] != "" {
|
||||
c.modulus = bigDecimal(parts[2])
|
||||
}
|
||||
for _, r := range strings.Split(parts[4], ",") {
|
||||
lo, hi, has := strings.Cut(strings.TrimSpace(r), "..")
|
||||
if !has {
|
||||
hi = lo
|
||||
}
|
||||
low, high := bigDecimal(lo), bigDecimal(hi)
|
||||
if low.Cmp(high) > 0 {
|
||||
return nil, fmt.Errorf("reversed plural range")
|
||||
}
|
||||
c.ranges = append(c.ranges, pluralRange{low, high})
|
||||
}
|
||||
group = append(group, c)
|
||||
}
|
||||
result = append(result, group)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func loadCardinalRules() map[string]pluralSet {
|
||||
var data struct {
|
||||
Supplemental struct {
|
||||
Rules map[string]map[string]string `json:"plurals-type-cardinal"`
|
||||
} `json:"supplemental"`
|
||||
}
|
||||
if err := json.Unmarshal(pluralData, &data); err != nil {
|
||||
panic("i18n: invalid embedded CLDR data")
|
||||
}
|
||||
out := map[string]pluralSet{}
|
||||
for locale, entries := range data.Supplemental.Rules {
|
||||
set := pluralSet{text: map[string]string{}, rules: map[string]pluralRule{}}
|
||||
for name, raw := range entries {
|
||||
category := strings.TrimPrefix(name, "pluralRule-count-")
|
||||
text, _, _ := strings.Cut(raw, "@")
|
||||
text = strings.TrimSpace(text)
|
||||
if category == "other" {
|
||||
continue
|
||||
}
|
||||
rule, err := parsePluralRule(text)
|
||||
if err != nil {
|
||||
panic("i18n: unsupported embedded CLDR rule for " + locale)
|
||||
}
|
||||
set.text[category] = text
|
||||
set.rules[category] = rule
|
||||
}
|
||||
out[strings.ReplaceAll(locale, "_", "-")] = set
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func rulesFor(tag language.Tag) pluralSet {
|
||||
for {
|
||||
if rules, ok := cardinalRules[tag.String()]; ok {
|
||||
return rules
|
||||
}
|
||||
if tag == language.Und {
|
||||
break
|
||||
}
|
||||
tag = tag.Parent()
|
||||
}
|
||||
return pluralSet{text: map[string]string{}, rules: map[string]pluralRule{}}
|
||||
}
|
||||
|
||||
// operands expands the canonical numeric string exactly, avoiding any float
|
||||
// conversion for int64 and any browser rounding of tiny fractional operands.
|
||||
func operands(text string) (map[byte]*big.Int, bool) {
|
||||
text = strings.TrimPrefix(text, "-")
|
||||
mantissa, exponent, has := strings.Cut(text, "e")
|
||||
shift := 0
|
||||
if has {
|
||||
shift, _ = strconv.Atoi(exponent)
|
||||
}
|
||||
whole, fraction, _ := strings.Cut(mantissa, ".")
|
||||
digits := whole + fraction
|
||||
point := len(whole) + shift
|
||||
switch {
|
||||
case point <= 0:
|
||||
whole = "0"
|
||||
fraction = strings.Repeat("0", -point) + digits
|
||||
case point >= len(digits):
|
||||
whole = digits + strings.Repeat("0", point-len(digits))
|
||||
fraction = ""
|
||||
default:
|
||||
whole = digits[:point]
|
||||
fraction = digits[point:]
|
||||
}
|
||||
tail := strings.TrimRight(fraction, "0")
|
||||
i, f := bigDecimal(whole), bigDecimal(fraction)
|
||||
return map[byte]*big.Int{'n': i, 'i': i, 'v': big.NewInt(int64(len(fraction))), 'w': big.NewInt(int64(len(tail))), 'f': f, 't': bigDecimal(tail), 'c': new(big.Int), 'e': new(big.Int)}, f.Sign() != 0
|
||||
}
|
||||
func (rule pluralRule) matches(values map[byte]*big.Int, fractional bool) bool {
|
||||
for _, group := range rule {
|
||||
all := true
|
||||
for _, condition := range group {
|
||||
value := values[condition.operand]
|
||||
if condition.modulus != nil {
|
||||
value = new(big.Int).Mod(value, condition.modulus)
|
||||
}
|
||||
found := false
|
||||
if condition.operand != 'n' || !fractional {
|
||||
for _, r := range condition.ranges {
|
||||
if value.Cmp(r.lo) >= 0 && value.Cmp(r.hi) <= 0 {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if condition.negate {
|
||||
found = !found
|
||||
}
|
||||
if !found {
|
||||
all = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if all {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
func category(tag language.Tag, text string) (string, error) {
|
||||
values, fractional := operands(text)
|
||||
rules := rulesFor(tag).rules
|
||||
for _, category := range []string{"zero", "one", "two", "few", "many"} {
|
||||
if rule, ok := rules[category]; ok && rule.matches(values, fractional) {
|
||||
return category, nil
|
||||
}
|
||||
}
|
||||
return "other", nil
|
||||
}
|
||||
Reference in New Issue
Block a user