#80753·Go

x/text/feature/plural: tables are generated from CLDR 32 (2017); wrong plural category for 26 locales

Author: josefguentherCreated Aug 6, 2026Updated Sep 17, 2026
Labelshelp wantedNeedsInvestigation

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:

  1. many was added for Romance languages (es, fr, it, pt, pt_PT, ca), rule e = 0 and i != 0 and i % 1000000 = 0 and v = 0 or e != 0..5. x/text has no many for these at all. This is the root cause of #27052, where gotext rejects the form for French with plural: form 'many' not supported for language 'fr' — open since 2018 and never diagnosed as a data-version problem.
  2. many was removed from Hebrew. he is now one, two, other; x/text still returns Many at multiples of 10.
  3. Macedonian's one gained the i % 100 != 11 exclusionone: v = 0 and i % 10 = 1 and i % 100 != 11 or f % 10 = 1 and f % 100 != 11. x/text returns One for 11.
  4. Marathi's one narrowed to exactly n = 1, no longer including 0. x/text returns One for 0.

Rules quoted from unicode-org/cldr-jsoncldr-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.