From 1c9cf442b6afc1bbb02b274a4432a04dbc258bc9 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 04:37:45 +0000 Subject: [PATCH] feat(i18n): add base language grammar fallback Co-Authored-By: Virgil --- grammar.go | 31 +++++++++++++++++++------ grammar_test.go | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 7 deletions(-) diff --git a/grammar.go b/grammar.go index efc4948..365f10a 100644 --- a/grammar.go +++ b/grammar.go @@ -190,7 +190,7 @@ func Upper(s string) string { } func getVerbForm(lang, verb, form string) string { - data := GetGrammarData(lang) + data := grammarDataForLang(lang) if data == nil || data.Verbs == nil { return "" } @@ -207,7 +207,7 @@ func getVerbForm(lang, verb, form string) string { } func getWord(lang, word string) string { - data := GetGrammarData(lang) + data := grammarDataForLang(lang) if data == nil || data.Words == nil { return "" } @@ -215,7 +215,7 @@ func getWord(lang, word string) string { } func getPunct(lang, rule, defaultVal string) string { - data := GetGrammarData(lang) + data := grammarDataForLang(lang) if data == nil { return defaultVal } @@ -233,7 +233,7 @@ func getPunct(lang, rule, defaultVal string) string { } func getNounForm(lang, noun, form string) string { - data := GetGrammarData(lang) + data := grammarDataForLang(lang) if data == nil || data.Nouns == nil { return "" } @@ -477,7 +477,7 @@ func Article(word string) string { func articleForCurrentLanguage(lowerWord, originalWord string) (string, bool) { lang := currentLangForGrammar() - data := GetGrammarData(lang) + data := grammarDataForLang(lang) if data == nil { return "", false } @@ -768,7 +768,7 @@ func DefiniteArticle(word string) string { return article } lang := currentLangForGrammar() - data := GetGrammarData(lang) + data := grammarDataForLang(lang) if data != nil && data.Articles.Definite != "" { return data.Articles.Definite } @@ -794,7 +794,7 @@ func DefinitePhrase(word string) string { func definiteArticleForCurrentLanguage(lowerWord, originalWord string) (string, bool) { lang := currentLangForGrammar() - data := GetGrammarData(lang) + data := grammarDataForLang(lang) if data == nil { return "", false } @@ -807,6 +807,23 @@ func definiteArticleForCurrentLanguage(lowerWord, originalWord string) (string, return "", false } +func grammarDataForLang(lang string) *GrammarData { + if data := GetGrammarData(lang); data != nil { + return data + } + if base := baseLanguageTag(lang); base != "" { + return GetGrammarData(base) + } + return nil +} + +func baseLanguageTag(lang string) string { + if idx := indexAny(lang, "-_"); idx > 0 { + return lang[:idx] + } + return "" +} + func definiteArticleFromGrammarForms(data *GrammarData, lowerWord, originalWord, lang string) (string, bool) { if data == nil || data.Articles.Definite == "" { return "", false diff --git a/grammar_test.go b/grammar_test.go index 618ca22..80350e3 100644 --- a/grammar_test.go +++ b/grammar_test.go @@ -7,6 +7,16 @@ import ( "time" ) +type regionFallbackLoader struct{} + +func (regionFallbackLoader) Languages() []string { + return []string{"en-GB"} +} + +func (regionFallbackLoader) Load(lang string) (map[string]Message, *GrammarData, error) { + return map[string]Message{}, nil, nil +} + func TestPastTense(t *testing.T) { // Ensure grammar data is loaded from embedded JSON svc, err := New() @@ -933,6 +943,58 @@ func TestFrenchGrammarData(t *testing.T) { } } +func TestGrammarFallbackToBaseLanguageTag(t *testing.T) { + prevDefault := Default() + prevGrammar := GetGrammarData("en") + t.Cleanup(func() { + SetGrammarData("en", prevGrammar) + SetDefault(prevDefault) + }) + + SetGrammarData("en", &GrammarData{ + Verbs: map[string]VerbForms{ + "delete": {Past: "deleted", Gerund: "deleting"}, + }, + Nouns: map[string]NounForms{ + "file": {One: "file", Other: "files"}, + }, + Articles: ArticleForms{ + IndefiniteDefault: "a", + IndefiniteVowel: "an", + Definite: "the", + }, + Punct: PunctuationRules{ + LabelSuffix: ":", + ProgressSuffix: "...", + }, + Words: map[string]string{ + "status": "Status", + }, + }) + + svc, err := NewWithLoader(regionFallbackLoader{}) + if err != nil { + t.Fatalf("NewWithLoader() failed: %v", err) + } + SetDefault(svc) + if err := svc.SetLanguage("en-GB"); err != nil { + t.Fatalf("SetLanguage(en-GB) failed: %v", err) + } + + if got := PastTense("delete"); got != "deleted" { + t.Fatalf("PastTense(delete) = %q, want %q", got, "deleted") + } + if got := Pluralize("file", 2); got != "files" { + t.Fatalf("Pluralize(file, 2) = %q, want %q", got, "files") + } + if got := Article("apple"); got != "an" { + t.Fatalf("Article(apple) = %q, want %q", got, "an") + } + if got := Label("status"); got != "Status:" { + t.Fatalf("Label(status) = %q, want %q", got, "Status:") + } +} + func TestTemplateFuncs(t *testing.T) { funcs := TemplateFuncs() expected := []string{ -- 2.45.3