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 }