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:
parent
62e5b0b75a
commit
cbe44d28f0
1 changed files with 114 additions and 28 deletions
142
pkg/i18n/i18n.go
142
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) {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue