refactor(i18n): rename core.* namespace to i18n.*

More self-documenting: when you see T("i18n.label.status") it's
immediately clear the i18n package is composing it, not looking up a key.

- T("i18n.label.status") → "Status:"
- T("i18n.progress.build") → "Building..."
- T("i18n.count.file", 5) → "5 files"
- T("i18n.done.delete", "file") → "File deleted"
- T("i18n.fail.delete", "file") → "Failed to delete file"

Intent keys (core.delete, core.install, etc.) remain unchanged -
they're semantic intent names, not i18n namespace patterns.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-01-30 14:32:08 +00:00
parent 39de3c2836
commit 5e55f66de9
3 changed files with 52 additions and 62 deletions

View file

@ -513,7 +513,7 @@ func L(word string) string {
// Use this for direct key lookups without auto-composition.
//
// i18n._("cli.success") // Raw lookup
// i18n.T("core.label.status") // Smart: returns "Status:"
// i18n.T("i18n.label.status") // Smart: returns "Status:"
func _(messageID string, args ...any) string {
if svc := Default(); svc != nil {
return svc.Raw(messageID, args...)
@ -626,25 +626,25 @@ func (s *Service) PluralCategory(n int) PluralCategory {
//
// The core.* namespace provides auto-composed grammar shortcuts:
//
// 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"
// T("i18n.label.status") // → "Status:"
// T("i18n.progress.build") // → "Building..."
// T("i18n.progress.check", "config") // → "Checking config..."
// T("i18n.count.file", 5) // → "5 files"
// T("i18n.done.delete", "file") // → "File deleted"
// T("i18n.fail.delete", "file") // → "Failed to delete file"
//
// For semantic intents, pass a Subject:
//
// T("core.delete", S("file", "config.yaml")) // → "Delete config.yaml?"
//
// Use _() for raw key lookup without core.* magic.
// Use _() for raw key lookup without i18n.* magic.
func (s *Service) T(messageID string, args ...any) string {
s.mu.RLock()
defer s.mu.RUnlock()
// Handle core.* namespace magic
if strings.HasPrefix(messageID, "core.") {
if result := s.handleCoreNamespace(messageID, args); result != "" {
// Handle i18n.* namespace magic
if strings.HasPrefix(messageID, "i18n.") {
if result := s.handleI18nNamespace(messageID, args); result != "" {
if s.debug {
return debugFormat(messageID, result)
}
@ -672,19 +672,19 @@ func (s *Service) T(messageID string, args ...any) string {
return text
}
// handleCoreNamespace processes core.* namespace patterns.
// handleI18nNamespace processes i18n.* 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.")
func (s *Service) handleI18nNamespace(key string, args []any) string {
// i18n.label.{word} → Label(word)
if strings.HasPrefix(key, "i18n.label.") {
word := strings.TrimPrefix(key, "i18n.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.")
// i18n.progress.{verb} → Progress(verb) or ProgressSubject(verb, subj)
if strings.HasPrefix(key, "i18n.progress.") {
verb := strings.TrimPrefix(key, "i18n.progress.")
if len(args) > 0 {
if subj, ok := args[0].(string); ok {
return ProgressSubject(verb, subj)
@ -693,9 +693,9 @@ func (s *Service) handleCoreNamespace(key string, args []any) string {
return Progress(verb)
}
// core.count.{noun} → "N noun(s)"
if strings.HasPrefix(key, "core.count.") {
noun := strings.TrimPrefix(key, "core.count.")
// i18n.count.{noun} → "N noun(s)"
if strings.HasPrefix(key, "i18n.count.") {
noun := strings.TrimPrefix(key, "i18n.count.")
if len(args) > 0 {
count := toInt(args[0])
return fmt.Sprintf("%d %s", count, Pluralize(noun, count))
@ -703,9 +703,9 @@ func (s *Service) handleCoreNamespace(key string, args []any) string {
return noun
}
// core.done.{verb} → ActionResult(verb, subj)
if strings.HasPrefix(key, "core.done.") {
verb := strings.TrimPrefix(key, "core.done.")
// i18n.done.{verb} → ActionResult(verb, subj)
if strings.HasPrefix(key, "i18n.done.") {
verb := strings.TrimPrefix(key, "i18n.done.")
if len(args) > 0 {
if subj, ok := args[0].(string); ok {
return ActionResult(verb, subj)
@ -714,9 +714,9 @@ func (s *Service) handleCoreNamespace(key string, args []any) string {
return Title(PastTense(verb))
}
// core.fail.{verb} → ActionFailed(verb, subj)
if strings.HasPrefix(key, "core.fail.") {
verb := strings.TrimPrefix(key, "core.fail.")
// i18n.fail.{verb} → ActionFailed(verb, subj)
if strings.HasPrefix(key, "i18n.fail.") {
verb := strings.TrimPrefix(key, "i18n.fail.")
if len(args) > 0 {
if subj, ok := args[0].(string); ok {
return ActionFailed(verb, subj)
@ -725,16 +725,6 @@ func (s *Service) handleCoreNamespace(key string, args []any) string {
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 ""
}

View file

@ -140,16 +140,16 @@ func TestPluralization(t *testing.T) {
require.NoError(t, err)
SetDefault(svc)
// Singular - uses core.count.* magic
result := svc.T("core.count.item", 1)
// Singular - uses i18n.count.* magic
result := svc.T("i18n.count.item", 1)
assert.Equal(t, "1 item", result)
// Plural
result = svc.T("core.count.item", 5)
result = svc.T("i18n.count.item", 5)
assert.Equal(t, "5 items", result)
// Zero uses plural
result = svc.T("core.count.item", 0)
result = svc.T("i18n.count.item", 0)
assert.Equal(t, "0 items", result)
}
@ -320,7 +320,7 @@ func TestDebugMode(t *testing.T) {
})
}
func TestCoreNamespaceMagic(t *testing.T) {
func TestI18nNamespaceMagic(t *testing.T) {
svc, err := New()
require.NoError(t, err)
SetDefault(svc)
@ -331,16 +331,16 @@ func TestCoreNamespaceMagic(t *testing.T) {
args []any
expected string
}{
{"label", "core.label.status", nil, "Status:"},
{"label version", "core.label.version", nil, "Version:"},
{"progress", "core.progress.build", nil, "Building..."},
{"progress check", "core.progress.check", nil, "Checking..."},
{"progress with subject", "core.progress.check", []any{"config"}, "Checking config..."},
{"count singular", "core.count.file", []any{1}, "1 file"},
{"count plural", "core.count.file", []any{5}, "5 files"},
{"done", "core.done.delete", []any{"file"}, "File deleted"},
{"done build", "core.done.build", []any{"project"}, "Project built"},
{"fail", "core.fail.delete", []any{"file"}, "Failed to delete file"},
{"label", "i18n.label.status", nil, "Status:"},
{"label version", "i18n.label.version", nil, "Version:"},
{"progress", "i18n.progress.build", nil, "Building..."},
{"progress check", "i18n.progress.check", nil, "Checking..."},
{"progress with subject", "i18n.progress.check", []any{"config"}, "Checking config..."},
{"count singular", "i18n.count.file", []any{1}, "1 file"},
{"count plural", "i18n.count.file", []any{5}, "5 files"},
{"done", "i18n.done.delete", []any{"file"}, "File deleted"},
{"done build", "i18n.done.build", []any{"project"}, "Project built"},
{"fail", "i18n.fail.delete", []any{"file"}, "Failed to delete file"},
}
for _, tt := range tests {
@ -351,15 +351,15 @@ func TestCoreNamespaceMagic(t *testing.T) {
}
}
func TestRawBypassesCoreNamespace(t *testing.T) {
func TestRawBypassesI18nNamespace(t *testing.T) {
svc, err := New()
require.NoError(t, err)
// Raw() should return key as-is since core.label.status isn't in JSON
result := svc.Raw("core.label.status")
assert.Equal(t, "core.label.status", result)
// Raw() should return key as-is since i18n.label.status isn't in JSON
result := svc.Raw("i18n.label.status")
assert.Equal(t, "i18n.label.status", result)
// T() should compose it
result = svc.T("core.label.status")
result = svc.T("i18n.label.status")
assert.Equal(t, "Status:", result)
}

View file

@ -265,12 +265,12 @@ func TestIntentT_Integration(t *testing.T) {
svc, err := New()
require.NoError(t, err)
// Using T with core.* prefix and Subject should return Question form
result := svc.T("core.delete", S("file", "config.yaml"))
assert.Equal(t, "Delete config.yaml?", result)
// Using C with intent key and Subject
composed := svc.C("core.delete", S("file", "config.yaml"))
assert.Equal(t, "Delete config.yaml?", composed.Question)
// Using T with regular key should work normally
result = svc.T("cmd.dev.short")
result := svc.T("cmd.dev.short")
assert.Equal(t, "Multi-repo development workflow", result)
}