From e8353f68f11f6fdc8604dd3fb29f6987bfc10a3d Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 00:13:10 +0000 Subject: [PATCH] feat(i18n): add definite article composition Co-Authored-By: Virgil --- grammar.go | 66 +++++++++++++++++++++++++++++++++++++++++++++++++ grammar_test.go | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 122 insertions(+) diff --git a/grammar.go b/grammar.go index 6faeff7..9a1b658 100644 --- a/grammar.go +++ b/grammar.go @@ -709,6 +709,71 @@ func ArticlePhrase(word string) string { return article + " " + word } +// DefiniteArticle returns the language-specific definite article for a word. +// For languages such as French, this respects gendered articles, plural forms, +// and elision rules when grammar data is available. +func DefiniteArticle(word string) string { + if word == "" { + return "" + } + trimmed := core.Trim(word) + lower := core.Lower(trimmed) + if article, ok := definiteArticleForCurrentLanguage(lower, trimmed); ok { + return article + } + lang := currentLangForGrammar() + data := GetGrammarData(lang) + if data != nil && data.Articles.Definite != "" { + return data.Articles.Definite + } + return "the" +} + +// DefinitePhrase prefixes a noun phrase with the correct definite article. +func DefinitePhrase(word string) string { + if word == "" { + return "" + } + lang := currentLangForGrammar() + word = renderWord(lang, word) + article := DefiniteArticle(word) + if article == "" { + return "" + } + if strings.HasSuffix(article, "'") { + return article + word + } + return article + " " + word +} + +func definiteArticleForCurrentLanguage(lowerWord, originalWord string) (string, bool) { + lang := currentLangForGrammar() + data := GetGrammarData(lang) + if data == nil { + return "", false + } + if article, ok := articleByGender(data, lowerWord, originalWord, lang); ok { + return article, true + } + if article, ok := definiteArticleFromGrammarForms(data, lowerWord, originalWord, lang); ok { + return article, true + } + return "", false +} + +func definiteArticleFromGrammarForms(data *GrammarData, lowerWord, originalWord, lang string) (string, bool) { + if data == nil || data.Articles.Definite == "" { + return "", false + } + if isFrenchLanguage(lang) { + if isKnownPluralNoun(data, lowerWord) || looksLikeFrenchPlural(originalWord) { + return "les", true + } + return maybeElideArticle(data.Articles.Definite, originalWord, lang), true + } + return data.Articles.Definite, true +} + // TemplateFuncs returns the template.FuncMap with all grammar functions. func TemplateFuncs() template.FuncMap { return template.FuncMap{ @@ -720,6 +785,7 @@ func TemplateFuncs() template.FuncMap { "plural": Pluralize, "pluralForm": PluralForm, "article": ArticlePhrase, + "definite": DefinitePhrase, "quote": Quote, "label": Label, "progress": Progress, diff --git a/grammar_test.go b/grammar_test.go index c6467d6..d2418df 100644 --- a/grammar_test.go +++ b/grammar_test.go @@ -590,6 +590,62 @@ func TestArticlePhraseFrenchLocale(t *testing.T) { } } +func TestDefiniteArticle(t *testing.T) { + tests := []struct { + word string + want string + }{ + {"file", "the"}, + {"error", "the"}, + {"", ""}, + } + + for _, tt := range tests { + t.Run(tt.word, func(t *testing.T) { + got := DefiniteArticle(tt.word) + if got != tt.want { + t.Errorf("DefiniteArticle(%q) = %q, want %q", tt.word, got, tt.want) + } + }) + } +} + +func TestDefinitePhraseFrenchLocale(t *testing.T) { + prev := Default() + svc, err := New() + if err != nil { + t.Fatalf("New() failed: %v", err) + } + SetDefault(svc) + t.Cleanup(func() { + SetDefault(prev) + }) + + if err := SetLanguage("fr"); err != nil { + t.Fatalf("SetLanguage(fr) failed: %v", err) + } + + tests := []struct { + word string + want string + }{ + {"branche", "la branche"}, + {"branches", "les branches"}, + {"amis", "les amis"}, + {"enfant", "l'enfant"}, + {"fichier", "le fichier"}, + } + + for _, tt := range tests { + t.Run(tt.word, func(t *testing.T) { + got := DefinitePhrase(tt.word) + if got != tt.want { + t.Errorf("DefinitePhrase(%q) = %q, want %q", tt.word, got, tt.want) + } + }) + } +} + func TestLabel(t *testing.T) { svc, err := New() if err != nil { -- 2.45.3