diff --git a/grammar.go b/grammar.go index 3465375..0ee8893 100644 --- a/grammar.go +++ b/grammar.go @@ -35,6 +35,9 @@ func MergeGrammarData(lang string, data *GrammarData) { maps.Copy(existing.Verbs, data.Verbs) maps.Copy(existing.Nouns, data.Nouns) maps.Copy(existing.Words, data.Words) + if data.Number != (NumberFormat{}) { + existing.Number = data.Number + } } // IrregularVerbs returns a copy of the irregular verb forms map. diff --git a/loader.go b/loader.go index 41c02df..31f98e4 100644 --- a/loader.go +++ b/loader.go @@ -222,6 +222,20 @@ func flattenWithGrammar(prefix string, data map[string]any, out map[string]Messa continue } + // Number formatting rules + if grammar != nil && fullKey == "gram.number" { + if thousands, ok := v["thousands"].(string); ok { + grammar.Number.ThousandsSep = thousands + } + if decimal, ok := v["decimal"].(string); ok { + grammar.Number.DecimalSep = decimal + } + if percent, ok := v["percent"].(string); ok { + grammar.Number.PercentFmt = percent + } + continue + } + // CLDR plural object if isPluralObject(v) { msg := Message{} diff --git a/loader_test.go b/loader_test.go index 2605f8a..1ce7f06 100644 --- a/loader_test.go +++ b/loader_test.go @@ -85,6 +85,17 @@ func TestFSLoaderLoad(t *testing.T) { t.Errorf("punct.progress = %q, want '...'", grammar.Punct.ProgressSuffix) } + // Number formatting from gram.number + if grammar.Number.ThousandsSep != "," { + t.Errorf("number.thousands = %q, want ','", grammar.Number.ThousandsSep) + } + if grammar.Number.DecimalSep != "." { + t.Errorf("number.decimal = %q, want '.'", grammar.Number.DecimalSep) + } + if grammar.Number.PercentFmt != "%s%%" { + t.Errorf("number.percent = %q, want '%%s%%%%'", grammar.Number.PercentFmt) + } + // Words from gram.word.* if len(grammar.Words) == 0 { t.Error("grammar has 0 words") @@ -135,6 +146,11 @@ func TestFlattenWithGrammar(t *testing.T) { "label": ":", "progress": "...", }, + "number": map[string]any{ + "thousands": ",", + "decimal": ".", + "percent": "%s%%", + }, "article": map[string]any{ "indefinite": map[string]any{ "default": "a", @@ -179,6 +195,11 @@ func TestFlattenWithGrammar(t *testing.T) { t.Errorf("punct.label = %q, want ':'", grammar.Punct.LabelSuffix) } + // Number formatting extracted + if grammar.Number.ThousandsSep != "," { + t.Errorf("number.thousands = %q, want ','", grammar.Number.ThousandsSep) + } + // Articles extracted if grammar.Articles.IndefiniteDefault != "a" { t.Errorf("article.indefinite.default = %q, want 'a'", grammar.Articles.IndefiniteDefault) @@ -188,6 +209,9 @@ func TestFlattenWithGrammar(t *testing.T) { if msg, ok := messages["prompt.yes"]; !ok || msg.Text != "y" { t.Errorf("prompt.yes not flattened correctly, got %+v", messages["prompt.yes"]) } + if _, ok := messages["gram.number.thousands"]; ok { + t.Error("gram.number.thousands should not be flattened into messages") + } } func TestFlattenPluralObject(t *testing.T) { diff --git a/numbers.go b/numbers.go index b84a4ea..ecc4ece 100644 --- a/numbers.go +++ b/numbers.go @@ -9,15 +9,27 @@ import ( func getNumberFormat() NumberFormat { lang := currentLangForGrammar() - if idx := indexAny(lang, "-_"); idx > 0 { - lang = lang[:idx] - } - if fmt, ok := numberFormats[lang]; ok { + if fmt, ok := getLocaleNumberFormat(lang); ok { return fmt } + if idx := indexAny(lang, "-_"); idx > 0 { + if fmt, ok := getLocaleNumberFormat(lang[:idx]); ok { + return fmt + } + } return numberFormats["en"] } +func getLocaleNumberFormat(lang string) (NumberFormat, bool) { + if data := GetGrammarData(lang); data != nil && data.Number != (NumberFormat{}) { + return data.Number, true + } + if fmt, ok := numberFormats[lang]; ok { + return fmt, true + } + return NumberFormat{}, false +} + // FormatNumber formats an integer with locale-specific thousands separators. func FormatNumber(n int64) string { return formatIntWithSep(n, getNumberFormat().ThousandsSep) diff --git a/numbers_test.go b/numbers_test.go index e86f86a..235b5b0 100644 --- a/numbers_test.go +++ b/numbers_test.go @@ -143,3 +143,25 @@ func TestFormatOrdinal(t *testing.T) { } } } + +func TestFormatNumberFromLocale(t *testing.T) { + svc, err := New() + if err != nil { + t.Fatalf("New() failed: %v", err) + } + SetDefault(svc) + + if err := SetLanguage("fr"); err != nil { + t.Fatalf("SetLanguage(fr) failed: %v", err) + } + + if got := FormatNumber(1234567); got != "1 234 567" { + t.Errorf("FormatNumber(fr) = %q, want %q", got, "1 234 567") + } + if got := FormatDecimal(1234.56); got != "1 234,56" { + t.Errorf("FormatDecimal(fr) = %q, want %q", got, "1 234,56") + } + if got := FormatPercent(0.85); got != "85 %" { + t.Errorf("FormatPercent(fr) = %q, want %q", got, "85 %") + } +} diff --git a/types.go b/types.go index 13abb4b..dbe1c66 100644 --- a/types.go +++ b/types.go @@ -52,7 +52,7 @@ type TextDirection int const ( DirLTR TextDirection = iota // Left-to-right - DirRTL // Right-to-left + DirRTL // Right-to-left ) // PluralCategory represents CLDR plural categories. @@ -193,6 +193,7 @@ type GrammarData struct { Words map[string]string // base word translations Punct PunctuationRules // language-specific punctuation Signals SignalData // disambiguation signal word lists + Number NumberFormat // locale-specific number formatting } // VerbForms holds verb conjugations. @@ -385,7 +386,7 @@ var irregularVerbs = map[string]VerbForms{ "rebel": {Past: "rebelled", Gerund: "rebelling"}, "excel": {Past: "excelled", Gerund: "excelling"}, "cancel": {Past: "cancelled", Gerund: "cancelling"}, "travel": {Past: "travelled", Gerund: "travelling"}, "label": {Past: "labelled", Gerund: "labelling"}, "model": {Past: "modelled", Gerund: "modelling"}, - "level": {Past: "levelled", Gerund: "levelling"}, + "level": {Past: "levelled", Gerund: "levelling"}, "format": {Past: "formatted", Gerund: "formatting"}, "analyse": {Past: "analysed", Gerund: "analysing"}, "organise": {Past: "organised", Gerund: "organising"},