cli/pkg/i18n/compose_test.go
Snider 46f6d4c5fe feat(i18n): add Phase 4 extended language support
Fluent Intent Builder API:
- I("core.delete").For(S("file", path)).Question()
- I("core.delete").With(subject).Compose()
- Convenience methods: Question(), Success(), Failure(), Meta(), IsDangerous()

Formality Levels (for Sie/du, vous/tu languages):
- FormalityNeutral, FormalityInformal, FormalityFormal constants
- Subject.Formal(), Subject.Informal(), Subject.Formality()
- Service.SetFormality(), Service.Formality()
- Package-level SetFormality()

CLDR Plural Categories:
- PluralZero, PluralOne, PluralTwo, PluralFew, PluralMany, PluralOther
- Language-specific plural rules: English, German, French, Spanish, Russian, Polish, Arabic, Chinese, Japanese, Korean
- Message.ForCategory() for proper plural selection
- Service.PluralCategory() for getting category by count

RTL Text Direction Support:
- TextDirection type (DirLTR, DirRTL)
- IsRTLLanguage() for language detection
- Service.Direction(), Service.IsRTL()
- Package-level Direction(), IsRTL()

GrammaticalGender type:
- GenderNeuter, GenderMasculine, GenderFeminine, GenderCommon
- For future gender agreement in gendered languages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-30 12:55:41 +00:00

271 lines
7.1 KiB
Go

package i18n
import (
"testing"
"github.com/stretchr/testify/assert"
)
// stringerValue is a test helper that implements fmt.Stringer
type stringerValue struct {
value string
}
func (s stringerValue) String() string {
return s.value
}
func TestSubject_Good(t *testing.T) {
t.Run("basic creation", func(t *testing.T) {
s := S("file", "config.yaml")
assert.Equal(t, "file", s.Noun)
assert.Equal(t, "config.yaml", s.Value)
assert.Equal(t, 1, s.count)
assert.Equal(t, "", s.gender)
assert.Equal(t, "", s.location)
})
t.Run("NewSubject alias", func(t *testing.T) {
s := NewSubject("repo", "core-php")
assert.Equal(t, "repo", s.Noun)
assert.Equal(t, "core-php", s.Value)
})
t.Run("with count", func(t *testing.T) {
s := S("file", "*.go").Count(5)
assert.Equal(t, 5, s.GetCount())
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())
})
t.Run("with location", func(t *testing.T) {
s := S("file", "config.yaml").In("workspace")
assert.Equal(t, "workspace", s.GetLocation())
})
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())
})
}
func TestSubject_String(t *testing.T) {
t.Run("string value", func(t *testing.T) {
s := S("file", "config.yaml")
assert.Equal(t, "config.yaml", s.String())
})
t.Run("stringer interface", func(t *testing.T) {
// Using a struct that implements Stringer via embedded method
s := S("item", stringerValue{"test"})
assert.Equal(t, "test", s.String())
})
t.Run("nil subject", func(t *testing.T) {
var s *Subject
assert.Equal(t, "", s.String())
})
t.Run("int value", func(t *testing.T) {
s := S("count", 42)
assert.Equal(t, "42", s.String())
})
}
func TestSubject_IsPlural(t *testing.T) {
t.Run("singular (count 1)", func(t *testing.T) {
s := S("file", "test.go")
assert.False(t, s.IsPlural())
})
t.Run("plural (count 0)", func(t *testing.T) {
s := S("file", "*.go").Count(0)
assert.True(t, s.IsPlural())
})
t.Run("plural (count > 1)", func(t *testing.T) {
s := S("file", "*.go").Count(5)
assert.True(t, s.IsPlural())
})
t.Run("nil subject", func(t *testing.T) {
var s *Subject
assert.False(t, s.IsPlural())
})
}
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())
})
}
func TestIntentMeta(t *testing.T) {
meta := IntentMeta{
Type: "action",
Verb: "delete",
Dangerous: true,
Default: "no",
Supports: []string{"force", "recursive"},
}
assert.Equal(t, "action", meta.Type)
assert.Equal(t, "delete", meta.Verb)
assert.True(t, meta.Dangerous)
assert.Equal(t, "no", meta.Default)
assert.Contains(t, meta.Supports, "force")
assert.Contains(t, meta.Supports, "recursive")
}
func TestComposed(t *testing.T) {
composed := Composed{
Question: "Delete config.yaml?",
Confirm: "Really delete config.yaml?",
Success: "Config.yaml deleted",
Failure: "Failed to delete config.yaml",
Meta: IntentMeta{
Type: "action",
Verb: "delete",
Dangerous: true,
Default: "no",
},
}
assert.Equal(t, "Delete config.yaml?", composed.Question)
assert.Equal(t, "Really delete config.yaml?", composed.Confirm)
assert.Equal(t, "Config.yaml deleted", composed.Success)
assert.Equal(t, "Failed to delete config.yaml", composed.Failure)
assert.True(t, composed.Meta.Dangerous)
}
func TestNewTemplateData(t *testing.T) {
t.Run("from subject", func(t *testing.T) {
s := S("file", "config.yaml").Count(3).Gender("neuter").In("workspace")
data := newTemplateData(s)
assert.Equal(t, "config.yaml", data.Subject)
assert.Equal(t, "file", data.Noun)
assert.Equal(t, 3, data.Count)
assert.Equal(t, "neuter", data.Gender)
assert.Equal(t, "workspace", data.Location)
assert.Equal(t, "config.yaml", data.Value)
})
t.Run("from nil subject", func(t *testing.T) {
data := newTemplateData(nil)
assert.Equal(t, "", data.Subject)
assert.Equal(t, "", data.Noun)
assert.Equal(t, 1, data.Count)
assert.Equal(t, "", data.Gender)
assert.Equal(t, "", data.Location)
assert.Nil(t, data.Value)
})
t.Run("with formality", func(t *testing.T) {
s := S("user", "Hans").Formal()
data := newTemplateData(s)
assert.Equal(t, FormalityFormal, data.Formality)
assert.True(t, data.IsFormal)
})
t.Run("with plural", func(t *testing.T) {
s := S("file", "*.go").Count(5)
data := newTemplateData(s)
assert.True(t, data.IsPlural)
assert.Equal(t, 5, data.Count)
})
}
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.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.True(t, s.IsFormal())
})
t.Run("Informal()", func(t *testing.T) {
s := S("user", "name").Informal()
assert.Equal(t, FormalityInformal, s.GetFormality())
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())
})
t.Run("nil safety", func(t *testing.T) {
var s *Subject
assert.Equal(t, FormalityNeutral, s.GetFormality())
assert.False(t, s.IsFormal())
assert.False(t, s.IsInformal())
})
}
func TestIntentBuilder(t *testing.T) {
// Initialize the default service for tests
_ = Init()
t.Run("basic fluent API", func(t *testing.T) {
builder := I("core.delete").For(S("file", "config.yaml"))
assert.NotNil(t, builder)
})
t.Run("With alias", func(t *testing.T) {
builder := I("core.delete").With(S("file", "config.yaml"))
assert.NotNil(t, builder)
})
t.Run("Compose returns all forms", func(t *testing.T) {
result := I("core.delete").For(S("file", "config.yaml")).Compose()
assert.NotEmpty(t, result.Question)
assert.NotEmpty(t, result.Success)
assert.NotEmpty(t, result.Failure)
})
t.Run("Question returns string", func(t *testing.T) {
question := I("core.delete").For(S("file", "config.yaml")).Question()
assert.Contains(t, question, "config.yaml")
})
t.Run("Success returns string", func(t *testing.T) {
success := I("core.delete").For(S("file", "config.yaml")).Success()
assert.NotEmpty(t, success)
})
t.Run("Failure returns string", func(t *testing.T) {
failure := I("core.delete").For(S("file", "config.yaml")).Failure()
assert.Contains(t, failure, "delete")
})
t.Run("Meta returns metadata", func(t *testing.T) {
meta := I("core.delete").Meta()
assert.True(t, meta.Dangerous)
})
t.Run("IsDangerous helper", func(t *testing.T) {
assert.True(t, I("core.delete").IsDangerous())
assert.False(t, I("core.save").IsDangerous())
})
}