feat(i18n): add core.* namespace magic in T()

T() now auto-composes grammar patterns for core.* keys:
- core.label.{word} → "Status:"
- core.progress.{verb} → "Building..."
- core.count.{noun}, n → "5 files"
- core.done.{verb}, subj → "File deleted"
- core.fail.{verb}, subj → "Failed to delete file"

_() and Raw() do direct key lookup without magic.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 13:57:34 +00:00
parent 62e5b0b75a
commit cbe44d28f0

View file

@ -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) {