diff --git a/pkg/i18n/checks_test.go b/pkg/i18n/checks_test.go new file mode 100644 index 0000000..19cdbc4 --- /dev/null +++ b/pkg/i18n/checks_test.go @@ -0,0 +1,257 @@ +package i18n + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIsVerbFormObject(t *testing.T) { + tests := []struct { + name string + input map[string]any + expected bool + }{ + { + name: "has base only", + input: map[string]any{"base": "run"}, + expected: true, + }, + { + name: "has past only", + input: map[string]any{"past": "ran"}, + expected: true, + }, + { + name: "has gerund only", + input: map[string]any{"gerund": "running"}, + expected: true, + }, + { + name: "has all verb forms", + input: map[string]any{"base": "run", "past": "ran", "gerund": "running"}, + expected: true, + }, + { + name: "empty map", + input: map[string]any{}, + expected: false, + }, + { + name: "plural object not verb", + input: map[string]any{"one": "item", "other": "items"}, + expected: false, + }, + { + name: "unrelated keys", + input: map[string]any{"foo": "bar", "baz": "qux"}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isVerbFormObject(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIsNounFormObject(t *testing.T) { + tests := []struct { + name string + input map[string]any + expected bool + }{ + { + name: "has gender", + input: map[string]any{"gender": "masculine", "one": "file", "other": "files"}, + expected: true, + }, + { + name: "gender only", + input: map[string]any{"gender": "feminine"}, + expected: true, + }, + { + name: "no gender", + input: map[string]any{"one": "item", "other": "items"}, + expected: false, + }, + { + name: "empty map", + input: map[string]any{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isNounFormObject(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestHasPluralCategories(t *testing.T) { + tests := []struct { + name string + input map[string]any + expected bool + }{ + { + name: "has zero", + input: map[string]any{"zero": "none", "one": "one", "other": "many"}, + expected: true, + }, + { + name: "has two", + input: map[string]any{"one": "one", "two": "two", "other": "many"}, + expected: true, + }, + { + name: "has few", + input: map[string]any{"one": "one", "few": "few", "other": "many"}, + expected: true, + }, + { + name: "has many", + input: map[string]any{"one": "one", "many": "many", "other": "other"}, + expected: true, + }, + { + name: "has all categories", + input: map[string]any{"zero": "0", "one": "1", "two": "2", "few": "few", "many": "many", "other": "other"}, + expected: true, + }, + { + name: "only one and other", + input: map[string]any{"one": "item", "other": "items"}, + expected: false, + }, + { + name: "empty map", + input: map[string]any{}, + expected: false, + }, + { + name: "unrelated keys", + input: map[string]any{"foo": "bar"}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := hasPluralCategories(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestIsPluralObject(t *testing.T) { + tests := []struct { + name string + input map[string]any + expected bool + }{ + { + name: "one and other", + input: map[string]any{"one": "item", "other": "items"}, + expected: true, + }, + { + name: "all CLDR categories", + input: map[string]any{"zero": "0", "one": "1", "two": "2", "few": "few", "many": "many", "other": "other"}, + expected: true, + }, + { + name: "only other", + input: map[string]any{"other": "items"}, + expected: true, + }, + { + name: "empty map", + input: map[string]any{}, + expected: false, + }, + { + name: "nested map is not plural", + input: map[string]any{"one": "item", "other": map[string]any{"nested": "value"}}, + expected: false, + }, + { + name: "unrelated keys", + input: map[string]any{"foo": "bar", "baz": "qux"}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := isPluralObject(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestMessageIsPlural(t *testing.T) { + tests := []struct { + name string + msg Message + expected bool + }{ + { + name: "has zero", + msg: Message{Zero: "none"}, + expected: true, + }, + { + name: "has one", + msg: Message{One: "item"}, + expected: true, + }, + { + name: "has two", + msg: Message{Two: "items"}, + expected: true, + }, + { + name: "has few", + msg: Message{Few: "a few"}, + expected: true, + }, + { + name: "has many", + msg: Message{Many: "lots"}, + expected: true, + }, + { + name: "has other", + msg: Message{Other: "items"}, + expected: true, + }, + { + name: "has all", + msg: Message{Zero: "0", One: "1", Two: "2", Few: "few", Many: "many", Other: "other"}, + expected: true, + }, + { + name: "text only", + msg: Message{Text: "hello"}, + expected: false, + }, + { + name: "empty message", + msg: Message{}, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.msg.IsPlural() + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/pkg/i18n/compose.go b/pkg/i18n/compose.go index dfabbe0..1153439 100644 --- a/pkg/i18n/compose.go +++ b/pkg/i18n/compose.go @@ -94,41 +94,41 @@ func (s *Subject) IsPlural() bool { return s != nil && s.count != 1 } -// GetCount returns the count value. -func (s *Subject) GetCount() int { +// CountValue returns the count value. +func (s *Subject) CountValue() int { if s == nil { return 1 } return s.count } -// GetGender returns the grammatical gender. -func (s *Subject) GetGender() string { +// GenderValue returns the grammatical gender. +func (s *Subject) GenderValue() string { if s == nil { return "" } return s.gender } -// GetLocation returns the location context. -func (s *Subject) GetLocation() string { +// Location returns the location context. +func (s *Subject) Location() string { if s == nil { return "" } return s.location } -// GetNoun returns the noun type. -func (s *Subject) GetNoun() string { +// NounValue returns the noun type. +func (s *Subject) NounValue() string { if s == nil { return "" } return s.Noun } -// GetFormality returns the formality level. +// FormalityValue returns the formality level. // Returns FormalityNeutral if not explicitly set. -func (s *Subject) GetFormality() Formality { +func (s *Subject) FormalityValue() Formality { if s == nil { return FormalityNeutral } diff --git a/pkg/i18n/compose_test.go b/pkg/i18n/compose_test.go index 3b8276f..c378e8d 100644 --- a/pkg/i18n/compose_test.go +++ b/pkg/i18n/compose_test.go @@ -33,26 +33,26 @@ func TestSubject_Good(t *testing.T) { t.Run("with count", func(t *testing.T) { s := S("file", "*.go").Count(5) - assert.Equal(t, 5, s.GetCount()) + assert.Equal(t, 5, s.CountValue()) assert.True(t, s.IsPlural()) }) t.Run("with gender", func(t *testing.T) { s := S("user", "alice").Gender("female") - assert.Equal(t, "female", s.GetGender()) + assert.Equal(t, "female", s.GenderValue()) }) t.Run("with location", func(t *testing.T) { s := S("file", "config.yaml").In("workspace") - assert.Equal(t, "workspace", s.GetLocation()) + assert.Equal(t, "workspace", s.Location()) }) t.Run("chained methods", func(t *testing.T) { s := S("repo", "core-php").Count(3).Gender("neuter").In("organisation") - assert.Equal(t, "repo", s.GetNoun()) - assert.Equal(t, 3, s.GetCount()) - assert.Equal(t, "neuter", s.GetGender()) - assert.Equal(t, "organisation", s.GetLocation()) + assert.Equal(t, "repo", s.NounValue()) + assert.Equal(t, 3, s.CountValue()) + assert.Equal(t, "neuter", s.GenderValue()) + assert.Equal(t, "organisation", s.Location()) }) } @@ -104,10 +104,10 @@ func TestSubject_IsPlural(t *testing.T) { func TestSubject_Getters(t *testing.T) { t.Run("nil safety", func(t *testing.T) { var s *Subject - assert.Equal(t, "", s.GetNoun()) - assert.Equal(t, 1, s.GetCount()) - assert.Equal(t, "", s.GetGender()) - assert.Equal(t, "", s.GetLocation()) + assert.Equal(t, "", s.NounValue()) + assert.Equal(t, 1, s.CountValue()) + assert.Equal(t, "", s.GenderValue()) + assert.Equal(t, "", s.Location()) }) } @@ -193,31 +193,31 @@ func TestNewTemplateData(t *testing.T) { func TestSubject_Formality(t *testing.T) { t.Run("default is neutral", func(t *testing.T) { s := S("user", "name") - assert.Equal(t, FormalityNeutral, s.GetFormality()) + assert.Equal(t, FormalityNeutral, s.FormalityValue()) assert.False(t, s.IsFormal()) assert.False(t, s.IsInformal()) }) t.Run("Formal()", func(t *testing.T) { s := S("user", "name").Formal() - assert.Equal(t, FormalityFormal, s.GetFormality()) + assert.Equal(t, FormalityFormal, s.FormalityValue()) assert.True(t, s.IsFormal()) }) t.Run("Informal()", func(t *testing.T) { s := S("user", "name").Informal() - assert.Equal(t, FormalityInformal, s.GetFormality()) + assert.Equal(t, FormalityInformal, s.FormalityValue()) assert.True(t, s.IsInformal()) }) t.Run("Formality() explicit", func(t *testing.T) { s := S("user", "name").Formality(FormalityFormal) - assert.Equal(t, FormalityFormal, s.GetFormality()) + assert.Equal(t, FormalityFormal, s.FormalityValue()) }) t.Run("nil safety", func(t *testing.T) { var s *Subject - assert.Equal(t, FormalityNeutral, s.GetFormality()) + assert.Equal(t, FormalityNeutral, s.FormalityValue()) assert.False(t, s.IsFormal()) assert.False(t, s.IsInformal()) }) diff --git a/pkg/i18n/mutate_test.go b/pkg/i18n/mutate_test.go new file mode 100644 index 0000000..14009f7 --- /dev/null +++ b/pkg/i18n/mutate_test.go @@ -0,0 +1,246 @@ +package i18n + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFlatten(t *testing.T) { + tests := []struct { + name string + prefix string + data map[string]any + expected map[string]Message + }{ + { + name: "simple string", + prefix: "", + data: map[string]any{"hello": "world"}, + expected: map[string]Message{ + "hello": {Text: "world"}, + }, + }, + { + name: "nested object", + prefix: "", + data: map[string]any{ + "cli": map[string]any{ + "success": "Done", + "error": "Failed", + }, + }, + expected: map[string]Message{ + "cli.success": {Text: "Done"}, + "cli.error": {Text: "Failed"}, + }, + }, + { + name: "with prefix", + prefix: "app", + data: map[string]any{"key": "value"}, + expected: map[string]Message{ + "app.key": {Text: "value"}, + }, + }, + { + name: "deeply nested", + prefix: "", + data: map[string]any{ + "a": map[string]any{ + "b": map[string]any{ + "c": "deep value", + }, + }, + }, + expected: map[string]Message{ + "a.b.c": {Text: "deep value"}, + }, + }, + { + name: "plural object", + prefix: "", + data: map[string]any{ + "items": map[string]any{ + "one": "{{.Count}} item", + "other": "{{.Count}} items", + }, + }, + expected: map[string]Message{ + "items": {One: "{{.Count}} item", Other: "{{.Count}} items"}, + }, + }, + { + name: "full CLDR plural", + prefix: "", + data: map[string]any{ + "files": map[string]any{ + "zero": "no files", + "one": "one file", + "two": "two files", + "few": "a few files", + "many": "many files", + "other": "{{.Count}} files", + }, + }, + expected: map[string]Message{ + "files": { + Zero: "no files", + One: "one file", + Two: "two files", + Few: "a few files", + Many: "many files", + Other: "{{.Count}} files", + }, + }, + }, + { + name: "mixed content", + prefix: "", + data: map[string]any{ + "simple": "text", + "plural": map[string]any{ + "one": "singular", + "other": "plural", + }, + "nested": map[string]any{ + "child": "nested value", + }, + }, + expected: map[string]Message{ + "simple": {Text: "text"}, + "plural": {One: "singular", Other: "plural"}, + "nested.child": {Text: "nested value"}, + }, + }, + { + name: "empty data", + prefix: "", + data: map[string]any{}, + expected: map[string]Message{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + out := make(map[string]Message) + flatten(tt.prefix, tt.data, out) + assert.Equal(t, tt.expected, out) + }) + } +} + +func TestFlattenWithGrammar(t *testing.T) { + t.Run("extracts verb forms", func(t *testing.T) { + data := map[string]any{ + "gram": map[string]any{ + "verb": map[string]any{ + "run": map[string]any{ + "base": "run", + "past": "ran", + "gerund": "running", + }, + }, + }, + } + out := make(map[string]Message) + grammar := &GrammarData{ + Verbs: make(map[string]VerbForms), + Nouns: make(map[string]NounForms), + } + flattenWithGrammar("", data, out, grammar) + + assert.Contains(t, grammar.Verbs, "run") + assert.Equal(t, "ran", grammar.Verbs["run"].Past) + assert.Equal(t, "running", grammar.Verbs["run"].Gerund) + }) + + t.Run("extracts noun forms", func(t *testing.T) { + data := map[string]any{ + "gram": map[string]any{ + "noun": map[string]any{ + "file": map[string]any{ + "one": "file", + "other": "files", + "gender": "neuter", + }, + }, + }, + } + out := make(map[string]Message) + grammar := &GrammarData{ + Verbs: make(map[string]VerbForms), + Nouns: make(map[string]NounForms), + } + flattenWithGrammar("", data, out, grammar) + + assert.Contains(t, grammar.Nouns, "file") + assert.Equal(t, "file", grammar.Nouns["file"].One) + assert.Equal(t, "files", grammar.Nouns["file"].Other) + assert.Equal(t, "neuter", grammar.Nouns["file"].Gender) + }) + + t.Run("extracts articles", func(t *testing.T) { + data := map[string]any{ + "gram": map[string]any{ + "article": map[string]any{ + "indefinite": map[string]any{ + "default": "a", + "vowel": "an", + }, + "definite": "the", + }, + }, + } + out := make(map[string]Message) + grammar := &GrammarData{ + Verbs: make(map[string]VerbForms), + Nouns: make(map[string]NounForms), + } + flattenWithGrammar("", data, out, grammar) + + assert.Equal(t, "a", grammar.Articles.IndefiniteDefault) + assert.Equal(t, "an", grammar.Articles.IndefiniteVowel) + assert.Equal(t, "the", grammar.Articles.Definite) + }) + + t.Run("extracts punctuation rules", func(t *testing.T) { + data := map[string]any{ + "gram": map[string]any{ + "punct": map[string]any{ + "label": ":", + "progress": "...", + }, + }, + } + out := make(map[string]Message) + grammar := &GrammarData{ + Verbs: make(map[string]VerbForms), + Nouns: make(map[string]NounForms), + } + flattenWithGrammar("", data, out, grammar) + + assert.Equal(t, ":", grammar.Punct.LabelSuffix) + assert.Equal(t, "...", grammar.Punct.ProgressSuffix) + }) + + t.Run("nil grammar skips extraction", func(t *testing.T) { + data := map[string]any{ + "gram": map[string]any{ + "verb": map[string]any{ + "run": map[string]any{ + "past": "ran", + "gerund": "running", + }, + }, + }, + "simple": "text", + } + out := make(map[string]Message) + flattenWithGrammar("", data, out, nil) + + // Without grammar, verb forms are recursively processed as nested objects + assert.Contains(t, out, "simple") + assert.Equal(t, "text", out["simple"].Text) + }) +}