diff --git a/pkg/i18n/i18n.go b/pkg/i18n/i18n.go index f6e74d8a..a2a18ed8 100644 --- a/pkg/i18n/i18n.go +++ b/pkg/i18n/i18n.go @@ -508,13 +508,17 @@ func L(word string) string { return Label(word) } -// _ is the standard gettext-style translation helper. -// Alias for T() - use whichever you prefer. +// _ is the raw gettext-style translation helper. +// Unlike T(), this does NOT handle core.* namespace magic. +// Use this for direct key lookups without auto-composition. // -// i18n._("cli.success") -// i18n._("cli.greeting", map[string]any{"Name": "World"}) +// i18n._("cli.success") // Raw lookup +// i18n.T("core.label.status") // Smart: returns "Status:" func _(messageID string, args ...any) string { - return T(messageID, args...) + if svc := Default(); svc != nil { + return svc.Raw(messageID, args...) + } + return messageID } // --- Service methods --- @@ -616,36 +620,35 @@ func (s *Service) PluralCategory(n int) PluralCategory { return GetPluralCategory(s.currentLang, n) } -// T translates a message by its ID. -// Optional template data can be passed for interpolation. +// T translates a message by its ID with smart core.* namespace handling. // -// For plural messages, pass a map with "Count" to select the form: +// # Core Namespace Magic // -// svc.T("cli.count.items", map[string]any{"Count": 5}) +// The core.* namespace provides auto-composed grammar shortcuts: // -// For semantic intents (core.* namespace), pass a Subject to get the Question form: +// T("core.label.status") // → "Status:" +// T("core.progress.build") // → "Building..." +// T("core.progress.check", "config") // → "Checking config..." +// T("core.count.file", 5) // → "5 files" +// T("core.done.delete", "file") // → "File deleted" +// T("core.fail.delete", "file") // → "Failed to delete file" // -// svc.T("core.delete", S("file", "config.yaml")) // "Delete config.yaml?" +// For semantic intents, pass a Subject: // -// # Fallback Chain +// T("core.delete", S("file", "config.yaml")) // → "Delete config.yaml?" // -// When a key is not found, T() tries a fallback chain: -// 1. Try the exact key in current language -// 2. Try the exact key in fallback language -// 3. If key looks like an intent (contains "."), try common.action.{verb} -// 4. Return the key as-is (or handle according to mode) +// Use _() for raw key lookup without core.* magic. func (s *Service) T(messageID string, args ...any) string { s.mu.RLock() defer s.mu.RUnlock() - // Check for semantic intent with Subject - if strings.HasPrefix(messageID, "core.") && len(args) > 0 { - if subject, ok := args[0].(*Subject); ok { - // Use C() to resolve the intent, return Question form - s.mu.RUnlock() - result := s.C(messageID, subject) - s.mu.RLock() - return result.Question + // Handle core.* namespace magic + if strings.HasPrefix(messageID, "core.") { + if result := s.handleCoreNamespace(messageID, args); result != "" { + if s.debug { + return debugFormat(messageID, result) + } + return result } } @@ -669,6 +672,72 @@ func (s *Service) T(messageID string, args ...any) string { return text } +// handleCoreNamespace processes core.* namespace patterns. +// Returns empty string if pattern not recognized. +// Must be called with s.mu.RLock held. +func (s *Service) handleCoreNamespace(key string, args []any) string { + // core.label.{word} → Label(word) + if strings.HasPrefix(key, "core.label.") { + word := strings.TrimPrefix(key, "core.label.") + return Label(word) + } + + // core.progress.{verb} → Progress(verb) or ProgressSubject(verb, subj) + if strings.HasPrefix(key, "core.progress.") { + verb := strings.TrimPrefix(key, "core.progress.") + if len(args) > 0 { + if subj, ok := args[0].(string); ok { + return ProgressSubject(verb, subj) + } + } + return Progress(verb) + } + + // core.count.{noun} → "N noun(s)" + if strings.HasPrefix(key, "core.count.") { + noun := strings.TrimPrefix(key, "core.count.") + if len(args) > 0 { + count := toInt(args[0]) + return fmt.Sprintf("%d %s", count, Pluralize(noun, count)) + } + return noun + } + + // core.done.{verb} → ActionResult(verb, subj) + if strings.HasPrefix(key, "core.done.") { + verb := strings.TrimPrefix(key, "core.done.") + if len(args) > 0 { + if subj, ok := args[0].(string); ok { + return ActionResult(verb, subj) + } + } + return Title(PastTense(verb)) + } + + // core.fail.{verb} → ActionFailed(verb, subj) + if strings.HasPrefix(key, "core.fail.") { + verb := strings.TrimPrefix(key, "core.fail.") + if len(args) > 0 { + if subj, ok := args[0].(string); ok { + return ActionFailed(verb, subj) + } + } + return ActionFailed(verb, "") + } + + // core.{intent} with Subject → C(intent, subject).Question + if len(args) > 0 { + if subject, ok := args[0].(*Subject); ok { + s.mu.RUnlock() + result := s.C(key, subject) + s.mu.RLock() + return result.Question + } + } + + return "" +} + // resolveWithFallback implements the fallback chain for message resolution. // Must be called with s.mu.RLock held. func (s *Service) resolveWithFallback(messageID string, data any) string { @@ -856,9 +925,26 @@ func executeIntentTemplate(tmplStr string, data templateData) string { return buf.String() } -// _ is the standard gettext-style translation helper. Alias for T(). -func (s *Service) _(messageID string, args ...any) string { - return s.T(messageID, args...) +// Raw is the raw translation helper without core.* namespace magic. +// Use T() for smart core.* handling, Raw() for direct key lookup. +func (s *Service) Raw(messageID string, args ...any) string { + s.mu.RLock() + defer s.mu.RUnlock() + + var data any + if len(args) > 0 { + data = args[0] + } + + text := s.resolveWithFallback(messageID, data) + if text == "" { + return s.handleMissingKey(messageID, args) + } + + if s.debug { + return debugFormat(messageID, text) + } + return text } func (s *Service) getMessage(lang, key string) (Message, bool) {