diff --git a/pkg/i18n/compose.go b/pkg/i18n/compose.go index 54ac0e41..d82f9611 100644 --- a/pkg/i18n/compose.go +++ b/pkg/i18n/compose.go @@ -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. // diff --git a/pkg/i18n/compose_test.go b/pkg/i18n/compose_test.go index 224ffec6..6909409b 100644 --- a/pkg/i18n/compose_test.go +++ b/pkg/i18n/compose_test.go @@ -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()) diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index 3d99f51d..e0857af3 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -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. diff --git a/pkg/i18n/i18n_test.go b/pkg/i18n/i18n_test.go index 44ca33b3..98a9c7c3 100644 --- a/pkg/i18n/i18n_test.go +++ b/pkg/i18n/i18n_test.go @@ -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) + }) +}