feat(i18n): implement formality-based message selection
Add support for formal/informal translation variants using suffix convention:
- key._formal for formal address (Sie, vous, usted)
- key._informal for informal address (du, tu, tú)
Formality priority: Subject.Formal() > Service.SetFormality() > neutral
Example locale file:
"greeting": "Hello",
"greeting._formal": "Good morning, sir",
"greeting._informal": "Hey there"
Usage:
svc.SetFormality(FormalityFormal)
svc.T("greeting") // "Good morning, sir"
svc.T("greeting", S("user", name).Informal()) // "Hey there"
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
b4ed8a09f1
commit
b04b2a70dc
2 changed files with 93 additions and 0 deletions
|
|
@ -343,3 +343,56 @@ func TestRawBypassesI18nNamespace(t *testing.T) {
|
|||
result = svc.T("i18n.label.status")
|
||||
assert.Equal(t, "Status:", result)
|
||||
}
|
||||
|
||||
func TestFormalityMessageSelection(t *testing.T) {
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Add test messages with formality variants
|
||||
svc.AddMessages("en-GB", map[string]string{
|
||||
"greeting": "Hello",
|
||||
"greeting._formal": "Good morning, sir",
|
||||
"greeting._informal": "Hey there",
|
||||
"farewell": "Goodbye",
|
||||
"farewell._formal": "Farewell",
|
||||
})
|
||||
|
||||
t.Run("neutral formality uses base key", func(t *testing.T) {
|
||||
svc.SetFormality(FormalityNeutral)
|
||||
assert.Equal(t, "Hello", svc.T("greeting"))
|
||||
assert.Equal(t, "Goodbye", svc.T("farewell"))
|
||||
})
|
||||
|
||||
t.Run("formal uses ._formal variant", func(t *testing.T) {
|
||||
svc.SetFormality(FormalityFormal)
|
||||
assert.Equal(t, "Good morning, sir", svc.T("greeting"))
|
||||
assert.Equal(t, "Farewell", svc.T("farewell"))
|
||||
})
|
||||
|
||||
t.Run("informal uses ._informal variant", func(t *testing.T) {
|
||||
svc.SetFormality(FormalityInformal)
|
||||
assert.Equal(t, "Hey there", svc.T("greeting"))
|
||||
// No informal variant for farewell, falls back to base
|
||||
assert.Equal(t, "Goodbye", svc.T("farewell"))
|
||||
})
|
||||
|
||||
t.Run("subject formality overrides service formality", func(t *testing.T) {
|
||||
svc.SetFormality(FormalityNeutral)
|
||||
|
||||
// Subject with formal overrides neutral service
|
||||
result := svc.T("greeting", S("user", "test").Formal())
|
||||
assert.Equal(t, "Good morning, sir", result)
|
||||
|
||||
// Subject with informal overrides neutral service
|
||||
result = svc.T("greeting", S("user", "test").Informal())
|
||||
assert.Equal(t, "Hey there", result)
|
||||
})
|
||||
|
||||
t.Run("subject formality overrides service formal", func(t *testing.T) {
|
||||
svc.SetFormality(FormalityFormal)
|
||||
|
||||
// Subject with informal overrides formal service
|
||||
result := svc.T("greeting", S("user", "test").Informal())
|
||||
assert.Equal(t, "Hey there", result)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -414,6 +414,24 @@ func (s *Service) resolveWithFallback(messageID string, data any) string {
|
|||
// Returns empty string if not found.
|
||||
// Must be called with s.mu.RLock held.
|
||||
func (s *Service) tryResolve(lang, key string, data any) string {
|
||||
// Determine effective formality
|
||||
formality := s.getEffectiveFormality(data)
|
||||
|
||||
// Try formality-specific key first (key._formal or key._informal)
|
||||
if formality != FormalityNeutral {
|
||||
formalityKey := key + "._" + formality.String()
|
||||
if text := s.resolveMessage(lang, formalityKey, data); text != "" {
|
||||
return text
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to base key
|
||||
return s.resolveMessage(lang, key, data)
|
||||
}
|
||||
|
||||
// resolveMessage resolves a single message key without formality fallback.
|
||||
// Must be called with s.mu.RLock held.
|
||||
func (s *Service) resolveMessage(lang, key string, data any) string {
|
||||
msg, ok := s.getMessage(lang, key)
|
||||
if !ok {
|
||||
return ""
|
||||
|
|
@ -438,6 +456,28 @@ func (s *Service) tryResolve(lang, key string, data any) string {
|
|||
return text
|
||||
}
|
||||
|
||||
// getEffectiveFormality returns the formality to use for translation.
|
||||
// Priority: Subject.formality > Service.formality > FormalityNeutral
|
||||
// Must be called with s.mu.RLock held.
|
||||
func (s *Service) getEffectiveFormality(data any) Formality {
|
||||
// Check if data is a Subject with explicit formality
|
||||
if subj, ok := data.(*Subject); ok && subj != nil {
|
||||
if subj.formality != FormalityNeutral {
|
||||
return subj.formality
|
||||
}
|
||||
}
|
||||
|
||||
// Check if data is a map with Formality field
|
||||
if m, ok := data.(map[string]any); ok {
|
||||
if f, ok := m["Formality"].(Formality); ok && f != FormalityNeutral {
|
||||
return f
|
||||
}
|
||||
}
|
||||
|
||||
// Fall back to service default
|
||||
return s.formality
|
||||
}
|
||||
|
||||
// handleMissingKey handles a missing translation key based on the current mode.
|
||||
// Must be called with s.mu.RLock held.
|
||||
func (s *Service) handleMissingKey(key string, args []any) string {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue