feat(i18n): add debug mode and NewSubject alias

- Add SetDebug()/Debug() methods for showing key prefixes in output
- Debug mode shows: "[cli.success] Success" instead of "Success"
- Add NewSubject() as alias for S() for readability
- Both T() and C() respect debug mode

Debug mode is useful for:
- Identifying which translation keys are used where
- Verifying correct key usage during development
- QA testing of translation coverage

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 12:47:28 +00:00
parent fc74d4df9c
commit fa6d62e385
4 changed files with 134 additions and 1 deletions

View file

@ -32,6 +32,13 @@ func S(noun string, value any) *Subject {
}
}
// NewSubject is an alias for S() for readability in longer expressions.
//
// NewSubject("file", path).Count(3).In("workspace")
func NewSubject(noun string, value any) *Subject {
return S(noun, value)
}
// Count sets the count for pluralization.
// Used to determine singular/plural forms in templates.
//

View file

@ -25,6 +25,12 @@ func TestSubject_Good(t *testing.T) {
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())

View file

@ -60,6 +60,7 @@ type Service struct {
fallbackLang string
availableLangs []language.Tag
mode Mode // Translation mode (Normal, Strict, Collect)
debug bool // Debug mode shows key prefixes
mu sync.RWMutex
}
@ -241,6 +242,17 @@ func SetDefault(s *Service) {
defaultService = s
}
// SetDebug enables or disables debug mode on the default service.
// In debug mode, translations show their keys: [key] translation
//
// SetDebug(true)
// T("cli.success") // "[cli.success] Success"
func SetDebug(enabled bool) {
if svc := Default(); svc != nil {
svc.SetDebug(enabled)
}
}
// T translates a message using the default service.
// For semantic intents (core.* namespace), pass a Subject as the first argument.
//
@ -368,6 +380,24 @@ func (s *Service) Mode() Mode {
return s.mode
}
// SetDebug enables or disables debug mode.
// In debug mode, translations are prefixed with their key:
//
// [cli.success] Success
// [core.delete] Delete config.yaml?
func (s *Service) SetDebug(enabled bool) {
s.mu.Lock()
defer s.mu.Unlock()
s.debug = enabled
}
// Debug returns whether debug mode is enabled.
func (s *Service) Debug() bool {
s.mu.RLock()
defer s.mu.RUnlock()
return s.debug
}
// T translates a message by its ID.
// Optional template data can be passed for interpolation.
//
@ -431,6 +461,11 @@ func (s *Service) T(messageID string, args ...any) string {
text = applyTemplate(text, data)
}
// Debug mode: prefix with key
if s.debug {
return "[" + messageID + "] " + text
}
return text
}
@ -494,13 +529,27 @@ func (s *Service) C(intent string, subject *Subject) *Composed {
// Create template data from subject
data := newTemplateData(subject)
return &Composed{
result := &Composed{
Question: executeIntentTemplate(intentDef.Question, data),
Confirm: executeIntentTemplate(intentDef.Confirm, data),
Success: executeIntentTemplate(intentDef.Success, data),
Failure: executeIntentTemplate(intentDef.Failure, data),
Meta: intentDef.Meta,
}
// Debug mode: prefix each form with the intent key
s.mu.RLock()
debug := s.debug
s.mu.RUnlock()
if debug {
prefix := "[" + intent + "] "
result.Question = prefix + result.Question
result.Confirm = prefix + result.Confirm
result.Success = prefix + result.Success
result.Failure = prefix + result.Failure
}
return result
}
// executeIntentTemplate executes an intent template with the given data.

View file

@ -164,3 +164,74 @@ func TestNestedKeys(t *testing.T) {
result = svc.T("cmd.dev.work.flag.status")
assert.Equal(t, "Show status only, don't push", result)
}
func TestDebugMode(t *testing.T) {
t.Run("default is disabled", func(t *testing.T) {
svc, err := New()
require.NoError(t, err)
assert.False(t, svc.Debug())
})
t.Run("T with debug mode", func(t *testing.T) {
svc, err := New()
require.NoError(t, err)
// Without debug
result := svc.T("cli.success")
assert.Equal(t, "Success", result)
// Enable debug
svc.SetDebug(true)
assert.True(t, svc.Debug())
// With debug - shows key prefix
result = svc.T("cli.success")
assert.Equal(t, "[cli.success] Success", result)
// Disable debug
svc.SetDebug(false)
result = svc.T("cli.success")
assert.Equal(t, "Success", result)
})
t.Run("C with debug mode", func(t *testing.T) {
svc, err := New()
require.NoError(t, err)
subject := S("file", "config.yaml")
// Without debug
result := svc.C("core.delete", subject)
assert.NotContains(t, result.Question, "[core.delete]")
// Enable debug
svc.SetDebug(true)
// With debug - shows key prefix on all forms
result = svc.C("core.delete", subject)
assert.Contains(t, result.Question, "[core.delete]")
assert.Contains(t, result.Success, "[core.delete]")
assert.Contains(t, result.Failure, "[core.delete]")
})
t.Run("package-level SetDebug", func(t *testing.T) {
// Reset default
defaultService = nil
defaultOnce = sync.Once{}
defaultErr = nil
err := Init()
require.NoError(t, err)
// Enable debug via package function
SetDebug(true)
assert.True(t, Default().Debug())
// Translate
result := T("cli.success")
assert.Equal(t, "[cli.success] Success", result)
// Cleanup
SetDebug(false)
})
}