feat(i18n): add count support to translation context

Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
Virgil 2026-04-02 07:59:58 +00:00
parent 996fb4c849
commit 92add102a8
5 changed files with 77 additions and 8 deletions

View file

@ -1,5 +1,7 @@
package i18n
import "dappco.re/go/core"
// TranslationContext provides disambiguation for translations.
//
// T("direction.right", C("navigation")) // "rechts" (German)
@ -9,12 +11,14 @@ type TranslationContext struct {
Gender string
Location string
Formality Formality
count int
countSet bool
Extra map[string]any
}
// C creates a TranslationContext.
func C(context string) *TranslationContext {
return &TranslationContext{Context: context}
return &TranslationContext{Context: context, count: 1}
}
func (c *TranslationContext) WithGender(gender string) *TranslationContext {
@ -57,6 +61,16 @@ func (c *TranslationContext) WithFormality(f Formality) *TranslationContext {
return c
}
// Count sets the count used for plural-sensitive translations.
func (c *TranslationContext) Count(n int) *TranslationContext {
if c == nil {
return nil
}
c.count = n
c.countSet = true
return c
}
func (c *TranslationContext) Set(key string, value any) *TranslationContext {
if c == nil {
return nil
@ -109,3 +123,31 @@ func (c *TranslationContext) FormalityValue() Formality {
}
return c.Formality
}
// CountInt returns the current count value.
func (c *TranslationContext) CountInt() int {
if c == nil {
return 1
}
return c.count
}
// CountString returns the current count value formatted as text.
func (c *TranslationContext) CountString() string {
if c == nil {
return "1"
}
return core.Sprintf("%d", c.count)
}
// IsPlural reports whether the count is plural.
func (c *TranslationContext) IsPlural() bool {
return c != nil && c.count != 1
}
func (c *TranslationContext) countValue() (int, bool) {
if c == nil {
return 1, false
}
return c.count, c.countSet
}

View file

@ -47,7 +47,7 @@ func contextMapValuesAny(values map[string]any) map[string]any {
extra := make(map[string]any, len(values))
for key, value := range values {
switch key {
case "Context", "Gender", "Location", "Formality":
case "Context", "Gender", "Location", "Formality", "Count", "IsPlural":
continue
case "Extra", "extra", "Extras", "extras":
mergeContextExtra(extra, value)
@ -69,7 +69,8 @@ func contextMapValuesString(values map[string]string) map[string]any {
extra := make(map[string]any, len(values))
for key, value := range values {
switch key {
case "Context", "Gender", "Location", "Formality", "Extra", "extra", "Extras", "extras":
case "Context", "Gender", "Location", "Formality", "Count", "IsPlural",
"Extra", "extra", "Extras", "extras":
continue
default:
extra[key] = value

View file

@ -15,6 +15,9 @@ func TestC_Good(t *testing.T) {
assert.Equal(t, "navigation", ctx.Context)
assert.Equal(t, "navigation", ctx.ContextString())
assert.Equal(t, "navigation", ctx.String())
assert.Equal(t, 1, ctx.CountInt())
assert.Equal(t, "1", ctx.CountString())
assert.False(t, ctx.IsPlural())
}
func TestC_Good_EmptyContext(t *testing.T) {
@ -28,6 +31,7 @@ func TestC_Good_EmptyContext(t *testing.T) {
func TestTranslationContext_NilReceiver_Good(t *testing.T) {
var ctx *TranslationContext
assert.Nil(t, ctx.Count(2))
assert.Nil(t, ctx.WithGender("masculine"))
assert.Nil(t, ctx.In("workspace"))
assert.Nil(t, ctx.Formal())
@ -39,6 +43,9 @@ func TestTranslationContext_NilReceiver_Good(t *testing.T) {
assert.Equal(t, "", ctx.GenderString())
assert.Equal(t, "", ctx.LocationString())
assert.Equal(t, FormalityNeutral, ctx.FormalityValue())
assert.Equal(t, 1, ctx.CountInt())
assert.Equal(t, "1", ctx.CountString())
assert.False(t, ctx.IsPlural())
}
// --- WithGender ---
@ -119,12 +126,16 @@ func TestTranslationContext_Get_Bad_NilExtra(t *testing.T) {
func TestTranslationContext_FullChain_Good(t *testing.T) {
ctx := C("medical").
Count(3).
WithGender("feminine").
In("clinic").
Formal().
Set("speciality", "cardiology")
assert.Equal(t, "medical", ctx.ContextString())
assert.Equal(t, 3, ctx.CountInt())
assert.Equal(t, "3", ctx.CountString())
assert.True(t, ctx.IsPlural())
assert.Equal(t, "feminine", ctx.GenderString())
assert.Equal(t, "clinic", ctx.LocationString())
assert.Equal(t, FormalityFormal, ctx.FormalityValue())

10
i18n.go
View file

@ -252,11 +252,21 @@ func templateDataForRendering(data any) any {
if v == nil {
return nil
}
count, explicit := v.countValue()
if !explicit && v.Extra != nil {
if c, ok := v.Extra["Count"]; ok {
count = toInt(c)
} else if c, ok := v.Extra["count"]; ok {
count = toInt(c)
}
}
rendered := map[string]any{
"Context": v.Context,
"Gender": v.Gender,
"Location": v.Location,
"Formality": v.Formality,
"Count": count,
"IsPlural": count != 1,
"Extra": v.Extra,
}
for key, value := range v.Extra {

View file

@ -11,14 +11,19 @@ func getCount(data any) int {
}
return d.CountInt()
case *TranslationContext:
if d == nil || d.Extra == nil {
if d == nil {
return 0
}
if c, ok := d.Extra["Count"]; ok {
return toInt(c)
if count, ok := d.countValue(); ok {
return count
}
if c, ok := d.Extra["count"]; ok {
return toInt(c)
if d.Extra != nil {
if c, ok := d.Extra["Count"]; ok {
return toInt(c)
}
if c, ok := d.Extra["count"]; ok {
return toInt(c)
}
}
return 0
case map[string]any: