From 92add102a843958c69b390563c6651c115d12f20 Mon Sep 17 00:00:00 2001 From: Virgil Date: Thu, 2 Apr 2026 07:59:58 +0000 Subject: [PATCH] feat(i18n): add count support to translation context Co-Authored-By: Virgil --- context.go | 44 +++++++++++++++++++++++++++++++++++++++++++- context_map.go | 5 +++-- context_test.go | 11 +++++++++++ i18n.go | 10 ++++++++++ transform.go | 15 ++++++++++----- 5 files changed, 77 insertions(+), 8 deletions(-) diff --git a/context.go b/context.go index 8ffb002..cdea823 100644 --- a/context.go +++ b/context.go @@ -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 +} diff --git a/context_map.go b/context_map.go index 9ca670c..a3668b7 100644 --- a/context_map.go +++ b/context_map.go @@ -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 diff --git a/context_test.go b/context_test.go index 58400a5..700a051 100644 --- a/context_test.go +++ b/context_test.go @@ -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()) diff --git a/i18n.go b/i18n.go index 8b68d84..2579c3e 100644 --- a/i18n.go +++ b/i18n.go @@ -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 { diff --git a/transform.go b/transform.go index c526913..f12f25b 100644 --- a/transform.go +++ b/transform.go @@ -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: