x/text/feature/plural: tables are generated from CLDR 32 (2017); wrong plural category for 26 locales
golang.org/x/text/feature/plural selects plural categories from tables generated at CLDR 32, released September 2017. tables.go declares it:
// CLDRVersion is the CLDR version from which the tables in this package are derived.
const CLDRVersion = "32"
That is still the value in v0.34.0, the current release. Current CLDR is 48.
I swept all 224 locales for which CLDR 48 defines cardinal rules, comparing plural.Cardinal.MatchPlural against Intl.PluralRules in Node (ICU 77.1 / CLDR 47) as an independent oracle, over integer counts 0..200 plus 1000, 10000, 100000, 1000000, 1000001, 2000000.
214 were comparable — the other 10 (cv ie jw kok kok-Latn mo sgs sh tl und) are excluded because ICU canonicalises them to a different base language, so the comparison would not be like-for-like.
26 of the 214 disagree, in three classes:
| Class | n | Locales |
|---|---|---|
A — x/text has no rules at all; returns other for every count (locale post-dates CLDR 32) |
15 | an bal bho blo ceb csw doi ia lij lld pcm sat sc scn vec |
| B — has rules, but the category set is wrong | 9 | ca es fr he it kw mt pt pt_PT |
| C — correct set, but specific counts map to the wrong category | 2 | mk mr |
There is no error and no diagnostic in any of these — a caller simply gets a grammatically wrong form.
Representative examples
Cornish (kw) is the most affected: x/text has {one, two, other}, CLDR 48 has all six categories.
| n | x/text | CLDR 48 |
|---|---|---|
| 0 | other |
zero |
| 3 | other |
few |
| 21 | other |
many |
| 22 | other |
two |
Maltese (mt): CLDR added two and changed few (n = 0 or n % 100 = 3..10).
| n | x/text | CLDR 48 |
|---|---|---|
| 2 | few |
two |
| 102 | few |
other |
Four rule changes affecting widely-used locales:
manywas added for Romance languages (es,fr,it,pt,pt_PT,ca), rulee = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5. x/text has nomanyfor these at all. This is the root cause of #27052, wheregotextrejects the form for French withplural: form 'many' not supported for language 'fr'— open since 2018 and never diagnosed as a data-version problem.manywas removed from Hebrew.heis nowone,two,other; x/text still returnsManyat multiples of 10.- Macedonian's
onegained thei % 100 != 11exclusion —one: v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11. x/text returnsOnefor 11. - Marathi's
onenarrowed to exactlyn = 1, no longer including 0. x/text returnsOnefor 0.
Rules quoted from unicode-org/cldr-json → cldr-json/cldr-core/supplemental/plurals.json (CLDR 48).
Minimal reproducer
go.mod:
module xtplural
go 1.24
require golang.org/x/text v0.34.0
main.go:
package main
import (
"fmt"
"golang.org/x/text/feature/plural"
"golang.org/x/text/language"
)
var names = map[plural.Form]string{0: "Other", 1: "Zero", 2: "One", 3: "Two", 4: "Few", 5: "Many"}
func main() {
fmt.Println("x/text CLDRVersion:", plural.CLDRVersion)
for _, t := range []struct {
loc string
n int
want string
}{
{"es", 1000000, "many"}, {"fr", 1000000, "many"}, {"it", 1000000, "many"},
{"pt", 1000000, "many"}, {"ca", 1000000, "many"},
{"he", 20, "other"}, {"mk", 11, "other"}, {"mr", 0, "other"},
{"kw", 0, "zero"}, {"kw", 3, "few"}, {"kw", 21, "many"}, {"kw", 22, "two"},
{"mt", 2, "two"}, {"mt", 102, "other"}, {"sat", 1, "one"}, {"sat", 2, "two"},
} {
got := plural.Cardinal.MatchPlural(language.Make(t.loc), t.n, 0, 0, 0, 0)
fmt.Printf(" %-4s n=%-8d x/text=%-6s currentCLDR=%s\n", t.loc, t.n, names[got], t.want)
}
}
Output:
x/text CLDRVersion: 32
es n=1000000 x/text=Other currentCLDR=many
fr n=1000000 x/text=Other currentCLDR=many
it n=1000000 x/text=Other currentCLDR=many
pt n=1000000 x/text=Other currentCLDR=many
ca n=1000000 x/text=Other currentCLDR=many
he n=20 x/text=Many currentCLDR=other
mk n=11 x/text=One currentCLDR=other
mr n=0 x/text=One currentCLDR=other
kw n=0 x/text=Other currentCLDR=zero
kw n=3 x/text=Other currentCLDR=few
kw n=21 x/text=Other currentCLDR=many
kw n=22 x/text=Other currentCLDR=two
mt n=2 x/text=Few currentCLDR=two
mt n=102 x/text=Few currentCLDR=other
sat n=1 x/text=Other currentCLDR=one
sat n=2 x/text=Other currentCLDR=two
Impact
Anything using this package for user-facing pluralisation emits wrong grammar in these locales silently. Hebrew (two at exactly 2, plus the spurious many), Macedonian (11), Maltese (2) and Cornish (0, 3, 21, 22) are all reachable at everyday counts; the Romance many only at multiples of 10⁶.
Suggested fix
Regenerate the tables from current CLDR — the package already ships gen.go for exactly this. Worth noting that regeneration changes selection for existing callers, so it is arguably a behaviour change deserving a release note, even though the current behaviour is incorrect.
Not Go-version or platform specific: the CLDR data is compiled into the package, so this reproduces on any supported Go release, GOOS and GOARCH. Verified on go1.26.5 darwin/arm64 with x/text v0.34.0.
Other notes
Possibly in scope for the same regeneration: language/tables.go carries the same CLDRVersion = "32". I haven't verified a behavioural difference there, so this is an observation about generation scope rather than a second defect report — but if the plural tables are regenerated it may be worth checking whether the sibling package should be too.
Source: golang/go