refactor(i18n): remove C() and move intents to test-only
Breaking change: Remove C() semantic intent composition API. - Remove C() global function and Service.C() method - Remove IntentBuilder fluent API (I(), For(), Compose(), etc.) - Move coreIntents from intents.go to intents_test.go (test data only) - Remove C() from Translator interface Replace intent-based CLI helpers with grammar composition: - ConfirmIntent → ConfirmAction(verb, subject) - ConfirmDangerous → ConfirmDangerousAction(verb, subject) - QuestionIntent → QuestionAction(verb, subject) - ChooseIntent → ChooseAction(verb, subject, items) - ChooseMultiIntent → ChooseMultiAction(verb, subject, items) The intent definitions now serve purely as test data to verify the grammar engine can compose identical strings. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
106a751511
commit
04d8772cba
12 changed files with 706 additions and 1299 deletions
|
|
@ -149,42 +149,27 @@ func Confirm(prompt string, opts ...ConfirmOption) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// ConfirmIntent prompts for confirmation using a semantic intent.
|
||||
// The intent determines the question text, danger level, and default response.
|
||||
// ConfirmAction prompts for confirmation of an action using grammar composition.
|
||||
//
|
||||
// if ConfirmIntent("core.delete", i18n.S("file", "config.yaml")) { ... }
|
||||
func ConfirmIntent(intent string, subject *i18n.Subject, opts ...ConfirmOption) bool {
|
||||
result := i18n.C(intent, subject)
|
||||
|
||||
// Apply intent metadata to options
|
||||
if result.Meta.Dangerous {
|
||||
opts = append([]ConfirmOption{Required()}, opts...)
|
||||
}
|
||||
if result.Meta.Default == "yes" {
|
||||
opts = append([]ConfirmOption{DefaultYes()}, opts...)
|
||||
}
|
||||
|
||||
return Confirm(result.Question, opts...)
|
||||
// if ConfirmAction("delete", "config.yaml") { ... }
|
||||
// if ConfirmAction("save", "changes", DefaultYes()) { ... }
|
||||
func ConfirmAction(verb, subject string, opts ...ConfirmOption) bool {
|
||||
question := i18n.Title(verb) + " " + subject + "?"
|
||||
return Confirm(question, opts...)
|
||||
}
|
||||
|
||||
// ConfirmDangerous prompts for confirmation of a dangerous action.
|
||||
// Shows both the question and a confirmation prompt, requiring explicit "yes".
|
||||
// ConfirmDangerousAction prompts for double confirmation of a dangerous action.
|
||||
// Shows initial question, then a "Really verb subject?" confirmation.
|
||||
//
|
||||
// if ConfirmDangerous("core.delete", i18n.S("file", "config.yaml")) { ... }
|
||||
func ConfirmDangerous(intent string, subject *i18n.Subject) bool {
|
||||
result := i18n.C(intent, subject)
|
||||
|
||||
// Show initial question
|
||||
if !Confirm(result.Question, Required()) {
|
||||
// if ConfirmDangerousAction("delete", "config.yaml") { ... }
|
||||
func ConfirmDangerousAction(verb, subject string) bool {
|
||||
question := i18n.Title(verb) + " " + subject + "?"
|
||||
if !Confirm(question, Required()) {
|
||||
return false
|
||||
}
|
||||
|
||||
// For dangerous actions, show confirmation prompt
|
||||
if result.Meta.Dangerous && result.Confirm != "" {
|
||||
return Confirm(result.Confirm, Required())
|
||||
}
|
||||
|
||||
return true
|
||||
confirm := "Really " + verb + " " + subject + "?"
|
||||
return Confirm(confirm, Required())
|
||||
}
|
||||
|
||||
// QuestionOption configures Question behaviour.
|
||||
|
|
@ -262,12 +247,12 @@ func Question(prompt string, opts ...QuestionOption) string {
|
|||
}
|
||||
}
|
||||
|
||||
// QuestionIntent prompts for text input using a semantic intent.
|
||||
// QuestionAction prompts for text input using grammar composition.
|
||||
//
|
||||
// name := QuestionIntent("core.rename", i18n.S("file", "old.txt"))
|
||||
func QuestionIntent(intent string, subject *i18n.Subject, opts ...QuestionOption) string {
|
||||
result := i18n.C(intent, subject)
|
||||
return Question(result.Question, opts...)
|
||||
// name := QuestionAction("rename", "old.txt")
|
||||
func QuestionAction(verb, subject string, opts ...QuestionOption) string {
|
||||
question := i18n.Title(verb) + " " + subject + "?"
|
||||
return Question(question, opts...)
|
||||
}
|
||||
|
||||
// ChooseOption configures Choose behaviour.
|
||||
|
|
@ -372,12 +357,12 @@ func Choose[T any](prompt string, items []T, opts ...ChooseOption[T]) T {
|
|||
}
|
||||
}
|
||||
|
||||
// ChooseIntent prompts for selection using a semantic intent.
|
||||
// ChooseAction prompts for selection using grammar composition.
|
||||
//
|
||||
// file := ChooseIntent("core.select", i18n.S("file", ""), files)
|
||||
func ChooseIntent[T any](intent string, subject *i18n.Subject, items []T, opts ...ChooseOption[T]) T {
|
||||
result := i18n.C(intent, subject)
|
||||
return Choose(result.Question, items, opts...)
|
||||
// file := ChooseAction("select", "file", files)
|
||||
func ChooseAction[T any](verb, subject string, items []T, opts ...ChooseOption[T]) T {
|
||||
question := i18n.Title(verb) + " " + subject + ":"
|
||||
return Choose(question, items, opts...)
|
||||
}
|
||||
|
||||
// ChooseMulti prompts the user to select multiple items from a list.
|
||||
|
|
@ -486,12 +471,12 @@ func parseMultiSelection(input string, maxItems int) ([]int, error) {
|
|||
return result, nil
|
||||
}
|
||||
|
||||
// ChooseMultiIntent prompts for multiple selections using a semantic intent.
|
||||
// ChooseMultiAction prompts for multiple selections using grammar composition.
|
||||
//
|
||||
// files := ChooseMultiIntent("core.select", i18n.S("files", ""), files)
|
||||
func ChooseMultiIntent[T any](intent string, subject *i18n.Subject, items []T, opts ...ChooseOption[T]) []T {
|
||||
result := i18n.C(intent, subject)
|
||||
return ChooseMulti(result.Question, items, opts...)
|
||||
// files := ChooseMultiAction("select", "files", files)
|
||||
func ChooseMultiAction[T any](verb, subject string, items []T, opts ...ChooseOption[T]) []T {
|
||||
question := i18n.Title(verb) + " " + subject + ":"
|
||||
return ChooseMulti(question, items, opts...)
|
||||
}
|
||||
|
||||
// FormatAge formats a time as a human-readable age string.
|
||||
|
|
|
|||
|
|
@ -221,91 +221,3 @@ func newTemplateData(s *Subject) templateData {
|
|||
}
|
||||
}
|
||||
|
||||
// --- Fluent Intent Builder API ---
|
||||
|
||||
// IntentBuilder provides a fluent API for composing semantic intents.
|
||||
// Use I() to start building an intent.
|
||||
//
|
||||
// I("core.delete").For(S("file", path)).Question()
|
||||
// I("core.save").For(S("changes", n).Count(n)).Compose()
|
||||
type IntentBuilder struct {
|
||||
intent string
|
||||
subject *Subject
|
||||
}
|
||||
|
||||
// I creates a new IntentBuilder for the given intent key.
|
||||
// This is the entry point for the fluent intent API.
|
||||
//
|
||||
// I("core.delete").For(S("file", "config.yaml")).Question()
|
||||
// I("core.commit").For(S("file", files).Count(len(files))).Success()
|
||||
func I(intent string) *IntentBuilder {
|
||||
return &IntentBuilder{intent: intent}
|
||||
}
|
||||
|
||||
// For sets the subject for this intent.
|
||||
// Returns the builder for chaining.
|
||||
//
|
||||
// I("core.delete").For(S("file", path))
|
||||
func (b *IntentBuilder) For(subject *Subject) *IntentBuilder {
|
||||
b.subject = subject
|
||||
return b
|
||||
}
|
||||
|
||||
// With is an alias for For() - sets the subject for this intent.
|
||||
//
|
||||
// I("core.delete").With(S("file", path))
|
||||
func (b *IntentBuilder) With(subject *Subject) *IntentBuilder {
|
||||
return b.For(subject)
|
||||
}
|
||||
|
||||
// Compose returns all output forms for the intent.
|
||||
// Uses the default service to resolve the intent.
|
||||
//
|
||||
// result := I("core.delete").For(subject).Compose()
|
||||
// fmt.Println(result.Question)
|
||||
func (b *IntentBuilder) Compose() *Composed {
|
||||
return C(b.intent, b.subject)
|
||||
}
|
||||
|
||||
// Question returns just the question form of the intent.
|
||||
//
|
||||
// question := I("core.delete").For(subject).Question()
|
||||
func (b *IntentBuilder) Question() string {
|
||||
return b.Compose().Question
|
||||
}
|
||||
|
||||
// Confirm returns just the confirmation form of the intent.
|
||||
//
|
||||
// confirm := I("core.delete").For(subject).Confirm()
|
||||
func (b *IntentBuilder) Confirm() string {
|
||||
return b.Compose().Confirm
|
||||
}
|
||||
|
||||
// Success returns just the success message form of the intent.
|
||||
//
|
||||
// success := I("core.delete").For(subject).Success()
|
||||
func (b *IntentBuilder) Success() string {
|
||||
return b.Compose().Success
|
||||
}
|
||||
|
||||
// Failure returns just the failure message form of the intent.
|
||||
//
|
||||
// failure := I("core.delete").For(subject).Failure()
|
||||
func (b *IntentBuilder) Failure() string {
|
||||
return b.Compose().Failure
|
||||
}
|
||||
|
||||
// Meta returns just the intent metadata.
|
||||
//
|
||||
// meta := I("core.delete").For(subject).Meta()
|
||||
// if meta.Dangerous { ... }
|
||||
func (b *IntentBuilder) Meta() IntentMeta {
|
||||
return b.Compose().Meta
|
||||
}
|
||||
|
||||
// IsDangerous returns true if the intent is marked as dangerous.
|
||||
//
|
||||
// if I("core.delete").IsDangerous() { ... }
|
||||
func (b *IntentBuilder) IsDangerous() bool {
|
||||
return b.Meta().Dangerous
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,16 +4,25 @@ import (
|
|||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// composeIntent executes intent templates with a subject for testing.
|
||||
// This is a test helper that replicates what C() used to do.
|
||||
func composeIntent(intent Intent, subject *Subject) *Composed {
|
||||
data := newTemplateData(subject)
|
||||
return &Composed{
|
||||
Question: executeIntentTemplate(intent.Question, data),
|
||||
Confirm: executeIntentTemplate(intent.Confirm, data),
|
||||
Success: executeIntentTemplate(intent.Success, data),
|
||||
Failure: executeIntentTemplate(intent.Failure, data),
|
||||
Meta: intent.Meta,
|
||||
}
|
||||
}
|
||||
|
||||
// TestGrammarComposition_MatchesIntents verifies that the grammar engine
|
||||
// can compose the same strings as the intent templates.
|
||||
// This turns the intents.go file into a comprehensive test suite.
|
||||
// This turns the intents definitions into a comprehensive test suite.
|
||||
func TestGrammarComposition_MatchesIntents(t *testing.T) {
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
// Test subjects for validation
|
||||
subjects := []struct {
|
||||
noun string
|
||||
|
|
@ -34,8 +43,8 @@ func TestGrammarComposition_MatchesIntents(t *testing.T) {
|
|||
for _, subj := range subjects {
|
||||
subject := S(subj.noun, subj.value)
|
||||
|
||||
// Compose using C()
|
||||
composed := svc.C(key, subject)
|
||||
// Compose using intent templates directly
|
||||
composed := composeIntent(intent, subject)
|
||||
|
||||
// Verify Success output matches ActionResult
|
||||
if intent.Success != "" && intent.Meta.Verb != "" {
|
||||
|
|
@ -415,11 +424,8 @@ func TestIntentConsistency(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
// TestComposedVsManual compares C() output with manual grammar composition.
|
||||
// TestComposedVsManual compares template output with manual grammar composition.
|
||||
func TestComposedVsManual(t *testing.T) {
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
tests := []struct {
|
||||
intentKey string
|
||||
noun string
|
||||
|
|
@ -436,18 +442,19 @@ func TestComposedVsManual(t *testing.T) {
|
|||
for _, tt := range tests {
|
||||
t.Run(tt.intentKey, func(t *testing.T) {
|
||||
subject := S(tt.noun, tt.value)
|
||||
composed := svc.C(tt.intentKey, subject)
|
||||
intent := getIntent(tt.intentKey)
|
||||
require.NotNil(t, intent)
|
||||
intent := coreIntents[tt.intentKey]
|
||||
|
||||
// Compose using intent templates
|
||||
composed := composeIntent(intent, subject)
|
||||
|
||||
// Manual composition using grammar functions
|
||||
manualSuccess := ActionResult(intent.Meta.Verb, tt.value)
|
||||
manualFailure := ActionFailed(intent.Meta.Verb, tt.value)
|
||||
|
||||
assert.Equal(t, manualSuccess, composed.Success,
|
||||
"C() Success should match ActionResult()")
|
||||
"Template Success should match ActionResult()")
|
||||
assert.Equal(t, manualFailure, composed.Failure,
|
||||
"C() Failure should match ActionFailed()")
|
||||
"Template Failure should match ActionFailed()")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -223,49 +223,3 @@ func TestSubject_Formality(t *testing.T) {
|
|||
})
|
||||
}
|
||||
|
||||
func TestIntentBuilder(t *testing.T) {
|
||||
// Initialize the default service for tests
|
||||
_ = Init()
|
||||
|
||||
t.Run("basic fluent API", func(t *testing.T) {
|
||||
builder := I("core.delete").For(S("file", "config.yaml"))
|
||||
assert.NotNil(t, builder)
|
||||
})
|
||||
|
||||
t.Run("With alias", func(t *testing.T) {
|
||||
builder := I("core.delete").With(S("file", "config.yaml"))
|
||||
assert.NotNil(t, builder)
|
||||
})
|
||||
|
||||
t.Run("Compose returns all forms", func(t *testing.T) {
|
||||
result := I("core.delete").For(S("file", "config.yaml")).Compose()
|
||||
assert.NotEmpty(t, result.Question)
|
||||
assert.NotEmpty(t, result.Success)
|
||||
assert.NotEmpty(t, result.Failure)
|
||||
})
|
||||
|
||||
t.Run("Question returns string", func(t *testing.T) {
|
||||
question := I("core.delete").For(S("file", "config.yaml")).Question()
|
||||
assert.Contains(t, question, "config.yaml")
|
||||
})
|
||||
|
||||
t.Run("Success returns string", func(t *testing.T) {
|
||||
success := I("core.delete").For(S("file", "config.yaml")).Success()
|
||||
assert.NotEmpty(t, success)
|
||||
})
|
||||
|
||||
t.Run("Failure returns string", func(t *testing.T) {
|
||||
failure := I("core.delete").For(S("file", "config.yaml")).Failure()
|
||||
assert.Contains(t, failure, "delete")
|
||||
})
|
||||
|
||||
t.Run("Meta returns metadata", func(t *testing.T) {
|
||||
meta := I("core.delete").Meta()
|
||||
assert.True(t, meta.Dangerous)
|
||||
})
|
||||
|
||||
t.Run("IsDangerous helper", func(t *testing.T) {
|
||||
assert.True(t, I("core.delete").IsDangerous())
|
||||
assert.False(t, I("core.save").IsDangerous())
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -89,23 +89,6 @@ func T(messageID string, args ...any) string {
|
|||
return messageID
|
||||
}
|
||||
|
||||
// C composes a semantic intent using the default service.
|
||||
// Returns all output forms (Question, Confirm, Success, Failure) for the intent.
|
||||
//
|
||||
// result := C("core.delete", S("file", "config.yaml"))
|
||||
// fmt.Println(result.Question) // "Delete config.yaml?"
|
||||
func C(intent string, subject *Subject) *Composed {
|
||||
if svc := Default(); svc != nil {
|
||||
return svc.C(intent, subject)
|
||||
}
|
||||
return &Composed{
|
||||
Question: intent,
|
||||
Confirm: intent,
|
||||
Success: intent,
|
||||
Failure: intent,
|
||||
}
|
||||
}
|
||||
|
||||
// _ 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.
|
||||
|
|
|
|||
|
|
@ -278,26 +278,6 @@ func TestDebugMode(t *testing.T) {
|
|||
assert.Equal(t, "Multi-repo development workflow", 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
|
||||
|
|
|
|||
|
|
@ -1,679 +0,0 @@
|
|||
// Package i18n provides internationalization for the CLI.
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
// coreIntents defines the built-in semantic intents for common operations.
|
||||
// These are accessed via the "core.*" namespace in T() and C() calls.
|
||||
//
|
||||
// Each intent provides templates for all output forms:
|
||||
// - Question: Initial prompt to the user
|
||||
// - Confirm: Secondary confirmation (for dangerous actions)
|
||||
// - Success: Message shown on successful completion
|
||||
// - Failure: Message shown on failure
|
||||
//
|
||||
// Templates use Go text/template syntax with the following data available:
|
||||
// - .Subject: Display value of the subject
|
||||
// - .Noun: The noun type (e.g., "file", "repo")
|
||||
// - .Count: Count for pluralization
|
||||
// - .Location: Location context
|
||||
//
|
||||
// Template functions available:
|
||||
// - title, lower, upper: Case transformations
|
||||
// - past, gerund: Verb conjugations
|
||||
// - plural, pluralForm: Noun pluralization
|
||||
// - article: Indefinite article selection (a/an)
|
||||
// - quote: Wrap in double quotes
|
||||
var coreIntents = map[string]Intent{
|
||||
// --- Destructive Actions ---
|
||||
|
||||
"core.delete": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "delete",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Delete {{.Subject}}?",
|
||||
Confirm: "Really delete {{.Subject}}? This cannot be undone.",
|
||||
Success: "{{.Subject | title}} deleted",
|
||||
Failure: "Failed to delete {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.remove": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "remove",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Remove {{.Subject}}?",
|
||||
Confirm: "Really remove {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} removed",
|
||||
Failure: "Failed to remove {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.discard": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "discard",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Discard {{.Subject}}?",
|
||||
Confirm: "Really discard {{.Subject}}? All changes will be lost.",
|
||||
Success: "{{.Subject | title}} discarded",
|
||||
Failure: "Failed to discard {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.reset": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "reset",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Reset {{.Subject}}?",
|
||||
Confirm: "Really reset {{.Subject}}? This cannot be undone.",
|
||||
Success: "{{.Subject | title}} reset",
|
||||
Failure: "Failed to reset {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.overwrite": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "overwrite",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Overwrite {{.Subject}}?",
|
||||
Confirm: "Really overwrite {{.Subject}}? Existing content will be lost.",
|
||||
Success: "{{.Subject | title}} overwritten",
|
||||
Failure: "Failed to overwrite {{.Subject}}",
|
||||
},
|
||||
|
||||
// --- Creation Actions ---
|
||||
|
||||
"core.create": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "create",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Create {{.Subject}}?",
|
||||
Confirm: "Create {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} created",
|
||||
Failure: "Failed to create {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.add": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "add",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Add {{.Subject}}?",
|
||||
Confirm: "Add {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} added",
|
||||
Failure: "Failed to add {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.clone": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "clone",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Clone {{.Subject}}?",
|
||||
Confirm: "Clone {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} cloned",
|
||||
Failure: "Failed to clone {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.copy": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "copy",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Copy {{.Subject}}?",
|
||||
Confirm: "Copy {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} copied",
|
||||
Failure: "Failed to copy {{.Subject}}",
|
||||
},
|
||||
|
||||
// --- Modification Actions ---
|
||||
|
||||
"core.save": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "save",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Save {{.Subject}}?",
|
||||
Confirm: "Save {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} saved",
|
||||
Failure: "Failed to save {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.update": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "update",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Update {{.Subject}}?",
|
||||
Confirm: "Update {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} updated",
|
||||
Failure: "Failed to update {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.rename": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "rename",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Rename {{.Subject}}?",
|
||||
Confirm: "Rename {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} renamed",
|
||||
Failure: "Failed to rename {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.move": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "move",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Move {{.Subject}}?",
|
||||
Confirm: "Move {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} moved",
|
||||
Failure: "Failed to move {{.Subject}}",
|
||||
},
|
||||
|
||||
// --- Git Actions ---
|
||||
|
||||
"core.commit": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "commit",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Commit {{.Subject}}?",
|
||||
Confirm: "Commit {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} committed",
|
||||
Failure: "Failed to commit {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.push": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "push",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Push {{.Subject}}?",
|
||||
Confirm: "Push {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} pushed",
|
||||
Failure: "Failed to push {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.pull": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "pull",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Pull {{.Subject}}?",
|
||||
Confirm: "Pull {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} pulled",
|
||||
Failure: "Failed to pull {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.merge": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "merge",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Merge {{.Subject}}?",
|
||||
Confirm: "Really merge {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} merged",
|
||||
Failure: "Failed to merge {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.rebase": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "rebase",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Rebase {{.Subject}}?",
|
||||
Confirm: "Really rebase {{.Subject}}? This rewrites history.",
|
||||
Success: "{{.Subject | title}} rebased",
|
||||
Failure: "Failed to rebase {{.Subject}}",
|
||||
},
|
||||
|
||||
// --- Network Actions ---
|
||||
|
||||
"core.install": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "install",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Install {{.Subject}}?",
|
||||
Confirm: "Install {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} installed",
|
||||
Failure: "Failed to install {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.download": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "download",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Download {{.Subject}}?",
|
||||
Confirm: "Download {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} downloaded",
|
||||
Failure: "Failed to download {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.upload": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "upload",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Upload {{.Subject}}?",
|
||||
Confirm: "Upload {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} uploaded",
|
||||
Failure: "Failed to upload {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.publish": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "publish",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Publish {{.Subject}}?",
|
||||
Confirm: "Really publish {{.Subject}}? This will be publicly visible.",
|
||||
Success: "{{.Subject | title}} published",
|
||||
Failure: "Failed to publish {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.deploy": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "deploy",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Deploy {{.Subject}}?",
|
||||
Confirm: "Really deploy {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} deployed",
|
||||
Failure: "Failed to deploy {{.Subject}}",
|
||||
},
|
||||
|
||||
// --- Process Actions ---
|
||||
|
||||
"core.start": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "start",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Start {{.Subject}}?",
|
||||
Confirm: "Start {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} started",
|
||||
Failure: "Failed to start {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.stop": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "stop",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Stop {{.Subject}}?",
|
||||
Confirm: "Stop {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} stopped",
|
||||
Failure: "Failed to stop {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.restart": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "restart",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Restart {{.Subject}}?",
|
||||
Confirm: "Restart {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} restarted",
|
||||
Failure: "Failed to restart {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.run": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "run",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Run {{.Subject}}?",
|
||||
Confirm: "Run {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} completed",
|
||||
Failure: "Failed to run {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.build": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "build",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Build {{.Subject}}?",
|
||||
Confirm: "Build {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} built",
|
||||
Failure: "Failed to build {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.test": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "test",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Test {{.Subject}}?",
|
||||
Confirm: "Test {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} passed",
|
||||
Failure: "{{.Subject | title}} failed",
|
||||
},
|
||||
|
||||
// --- Information Actions ---
|
||||
|
||||
"core.continue": {
|
||||
Meta: IntentMeta{
|
||||
Type: "question",
|
||||
Verb: "continue",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Continue?",
|
||||
Confirm: "Continue?",
|
||||
Success: "Continuing",
|
||||
Failure: "Aborted",
|
||||
},
|
||||
|
||||
"core.proceed": {
|
||||
Meta: IntentMeta{
|
||||
Type: "question",
|
||||
Verb: "proceed",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Proceed?",
|
||||
Confirm: "Proceed?",
|
||||
Success: "Proceeding",
|
||||
Failure: "Aborted",
|
||||
},
|
||||
|
||||
"core.confirm": {
|
||||
Meta: IntentMeta{
|
||||
Type: "question",
|
||||
Verb: "confirm",
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Are you sure?",
|
||||
Confirm: "Are you sure?",
|
||||
Success: "Confirmed",
|
||||
Failure: "Cancelled",
|
||||
},
|
||||
|
||||
// --- Additional Actions ---
|
||||
|
||||
"core.sync": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "sync",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Sync {{.Subject}}?",
|
||||
Confirm: "Sync {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} synced",
|
||||
Failure: "Failed to sync {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.boot": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "boot",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Boot {{.Subject}}?",
|
||||
Confirm: "Boot {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} booted",
|
||||
Failure: "Failed to boot {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.format": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "format",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Format {{.Subject}}?",
|
||||
Confirm: "Format {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} formatted",
|
||||
Failure: "Failed to format {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.analyse": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "analyse",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Analyse {{.Subject}}?",
|
||||
Confirm: "Analyse {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} analysed",
|
||||
Failure: "Failed to analyse {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.link": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "link",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Link {{.Subject}}?",
|
||||
Confirm: "Link {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} linked",
|
||||
Failure: "Failed to link {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.unlink": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "unlink",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Unlink {{.Subject}}?",
|
||||
Confirm: "Unlink {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} unlinked",
|
||||
Failure: "Failed to unlink {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.fetch": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "fetch",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Fetch {{.Subject}}?",
|
||||
Confirm: "Fetch {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} fetched",
|
||||
Failure: "Failed to fetch {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.generate": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "generate",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Generate {{.Subject}}?",
|
||||
Confirm: "Generate {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} generated",
|
||||
Failure: "Failed to generate {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.validate": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "validate",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Validate {{.Subject}}?",
|
||||
Confirm: "Validate {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} valid",
|
||||
Failure: "{{.Subject | title}} invalid",
|
||||
},
|
||||
|
||||
"core.check": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "check",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Check {{.Subject}}?",
|
||||
Confirm: "Check {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} OK",
|
||||
Failure: "{{.Subject | title}} failed",
|
||||
},
|
||||
|
||||
"core.scan": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "scan",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Scan {{.Subject}}?",
|
||||
Confirm: "Scan {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} scanned",
|
||||
Failure: "Failed to scan {{.Subject}}",
|
||||
},
|
||||
}
|
||||
|
||||
// customIntents holds user-registered intents.
|
||||
// Separated from coreIntents to allow thread-safe registration.
|
||||
var (
|
||||
customIntents = make(map[string]Intent)
|
||||
customIntentsMu sync.RWMutex
|
||||
)
|
||||
|
||||
// getIntent retrieves an intent by its key.
|
||||
// Checks custom intents first, then falls back to core intents.
|
||||
// Returns nil if the intent is not found.
|
||||
func getIntent(key string) *Intent {
|
||||
// Check custom intents first (thread-safe)
|
||||
customIntentsMu.RLock()
|
||||
if intent, ok := customIntents[key]; ok {
|
||||
customIntentsMu.RUnlock()
|
||||
return &intent
|
||||
}
|
||||
customIntentsMu.RUnlock()
|
||||
|
||||
// Fall back to core intents
|
||||
if intent, ok := coreIntents[key]; ok {
|
||||
return &intent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RegisterIntent adds a custom intent at runtime.
|
||||
// Use this to extend the built-in intents with application-specific ones.
|
||||
// This function is thread-safe.
|
||||
//
|
||||
// i18n.RegisterIntent("myapp.archive", i18n.Intent{
|
||||
// Meta: i18n.IntentMeta{Type: "action", Verb: "archive", Default: "yes"},
|
||||
// Question: "Archive {{.Subject}}?",
|
||||
// Success: "{{.Subject | title}} archived",
|
||||
// Failure: "Failed to archive {{.Subject}}",
|
||||
// })
|
||||
func RegisterIntent(key string, intent Intent) {
|
||||
customIntentsMu.Lock()
|
||||
defer customIntentsMu.Unlock()
|
||||
customIntents[key] = intent
|
||||
}
|
||||
|
||||
// RegisterIntents adds multiple custom intents at runtime.
|
||||
// This is more efficient than calling RegisterIntent multiple times.
|
||||
// This function is thread-safe.
|
||||
//
|
||||
// i18n.RegisterIntents(map[string]i18n.Intent{
|
||||
// "myapp.archive": {
|
||||
// Meta: i18n.IntentMeta{Type: "action", Verb: "archive"},
|
||||
// Question: "Archive {{.Subject}}?",
|
||||
// },
|
||||
// "myapp.export": {
|
||||
// Meta: i18n.IntentMeta{Type: "action", Verb: "export"},
|
||||
// Question: "Export {{.Subject}}?",
|
||||
// },
|
||||
// })
|
||||
func RegisterIntents(intents map[string]Intent) {
|
||||
customIntentsMu.Lock()
|
||||
defer customIntentsMu.Unlock()
|
||||
for k, v := range intents {
|
||||
customIntents[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
// UnregisterIntent removes a custom intent by key.
|
||||
// This only affects custom intents, not core intents.
|
||||
// This function is thread-safe.
|
||||
func UnregisterIntent(key string) {
|
||||
customIntentsMu.Lock()
|
||||
defer customIntentsMu.Unlock()
|
||||
delete(customIntents, key)
|
||||
}
|
||||
|
||||
// IntentKeys returns all registered intent keys (both core and custom).
|
||||
func IntentKeys() []string {
|
||||
customIntentsMu.RLock()
|
||||
defer customIntentsMu.RUnlock()
|
||||
|
||||
keys := make([]string, 0, len(coreIntents)+len(customIntents))
|
||||
for key := range coreIntents {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
for key := range customIntents {
|
||||
// Avoid duplicates if custom overrides core
|
||||
found := false
|
||||
for _, k := range keys {
|
||||
if k == key {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// HasIntent returns true if an intent with the given key exists.
|
||||
func HasIntent(key string) bool {
|
||||
return getIntent(key) != nil
|
||||
}
|
||||
|
||||
// GetIntent returns the intent for a key, or nil if not found.
|
||||
// This is the public API for retrieving intents.
|
||||
func GetIntent(key string) *Intent {
|
||||
return getIntent(key)
|
||||
}
|
||||
|
|
@ -1,303 +1,679 @@
|
|||
// Package i18n provides internationalization for the CLI.
|
||||
package i18n
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"sync"
|
||||
)
|
||||
|
||||
func TestGetIntent(t *testing.T) {
|
||||
t.Run("existing intent", func(t *testing.T) {
|
||||
intent := getIntent("core.delete")
|
||||
require.NotNil(t, intent)
|
||||
assert.Equal(t, "action", intent.Meta.Type)
|
||||
assert.Equal(t, "delete", intent.Meta.Verb)
|
||||
assert.True(t, intent.Meta.Dangerous)
|
||||
assert.Equal(t, "no", intent.Meta.Default)
|
||||
})
|
||||
// coreIntents defines the built-in semantic intents for common operations.
|
||||
// These are accessed via the "core.*" namespace in T() and C() calls.
|
||||
//
|
||||
// Each intent provides templates for all output forms:
|
||||
// - Question: Initial prompt to the user
|
||||
// - Confirm: Secondary confirmation (for dangerous actions)
|
||||
// - Success: Message shown on successful completion
|
||||
// - Failure: Message shown on failure
|
||||
//
|
||||
// Templates use Go text/template syntax with the following data available:
|
||||
// - .Subject: Display value of the subject
|
||||
// - .Noun: The noun type (e.g., "file", "repo")
|
||||
// - .Count: Count for pluralization
|
||||
// - .Location: Location context
|
||||
//
|
||||
// Template functions available:
|
||||
// - title, lower, upper: Case transformations
|
||||
// - past, gerund: Verb conjugations
|
||||
// - plural, pluralForm: Noun pluralization
|
||||
// - article: Indefinite article selection (a/an)
|
||||
// - quote: Wrap in double quotes
|
||||
var coreIntents = map[string]Intent{
|
||||
// --- Destructive Actions ---
|
||||
|
||||
t.Run("non-existent intent", func(t *testing.T) {
|
||||
intent := getIntent("nonexistent.intent")
|
||||
assert.Nil(t, intent)
|
||||
})
|
||||
}
|
||||
"core.delete": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "delete",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Delete {{.Subject}}?",
|
||||
Confirm: "Really delete {{.Subject}}? This cannot be undone.",
|
||||
Success: "{{.Subject | title}} deleted",
|
||||
Failure: "Failed to delete {{.Subject}}",
|
||||
},
|
||||
|
||||
func TestRegisterIntent(t *testing.T) {
|
||||
// Register a custom intent
|
||||
RegisterIntent("test.custom", Intent{
|
||||
"core.remove": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "remove",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Remove {{.Subject}}?",
|
||||
Confirm: "Really remove {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} removed",
|
||||
Failure: "Failed to remove {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.discard": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "discard",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Discard {{.Subject}}?",
|
||||
Confirm: "Really discard {{.Subject}}? All changes will be lost.",
|
||||
Success: "{{.Subject | title}} discarded",
|
||||
Failure: "Failed to discard {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.reset": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "reset",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Reset {{.Subject}}?",
|
||||
Confirm: "Really reset {{.Subject}}? This cannot be undone.",
|
||||
Success: "{{.Subject | title}} reset",
|
||||
Failure: "Failed to reset {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.overwrite": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "overwrite",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Overwrite {{.Subject}}?",
|
||||
Confirm: "Really overwrite {{.Subject}}? Existing content will be lost.",
|
||||
Success: "{{.Subject | title}} overwritten",
|
||||
Failure: "Failed to overwrite {{.Subject}}",
|
||||
},
|
||||
|
||||
// --- Creation Actions ---
|
||||
|
||||
"core.create": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "custom",
|
||||
Verb: "create",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Custom {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} customised",
|
||||
Failure: "Failed to customise {{.Subject}}",
|
||||
})
|
||||
Question: "Create {{.Subject}}?",
|
||||
Confirm: "Create {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} created",
|
||||
Failure: "Failed to create {{.Subject}}",
|
||||
},
|
||||
|
||||
// Verify it was registered
|
||||
intent := getIntent("test.custom")
|
||||
require.NotNil(t, intent)
|
||||
assert.Equal(t, "custom", intent.Meta.Verb)
|
||||
assert.Equal(t, "Custom {{.Subject}}?", intent.Question)
|
||||
|
||||
// Clean up
|
||||
UnregisterIntent("test.custom")
|
||||
}
|
||||
|
||||
func TestRegisterIntents_Batch(t *testing.T) {
|
||||
// Register multiple intents at once
|
||||
RegisterIntents(map[string]Intent{
|
||||
"test.batch1": {
|
||||
Meta: IntentMeta{Type: "action", Verb: "batch1", Default: "yes"},
|
||||
Question: "Batch 1?",
|
||||
"core.add": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "add",
|
||||
Default: "yes",
|
||||
},
|
||||
"test.batch2": {
|
||||
Meta: IntentMeta{Type: "action", Verb: "batch2", Default: "no"},
|
||||
Question: "Batch 2?",
|
||||
Question: "Add {{.Subject}}?",
|
||||
Confirm: "Add {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} added",
|
||||
Failure: "Failed to add {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.clone": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "clone",
|
||||
Default: "yes",
|
||||
},
|
||||
})
|
||||
Question: "Clone {{.Subject}}?",
|
||||
Confirm: "Clone {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} cloned",
|
||||
Failure: "Failed to clone {{.Subject}}",
|
||||
},
|
||||
|
||||
// Verify both were registered
|
||||
assert.True(t, HasIntent("test.batch1"))
|
||||
assert.True(t, HasIntent("test.batch2"))
|
||||
"core.copy": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "copy",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Copy {{.Subject}}?",
|
||||
Confirm: "Copy {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} copied",
|
||||
Failure: "Failed to copy {{.Subject}}",
|
||||
},
|
||||
|
||||
intent1 := GetIntent("test.batch1")
|
||||
require.NotNil(t, intent1)
|
||||
assert.Equal(t, "batch1", intent1.Meta.Verb)
|
||||
// --- Modification Actions ---
|
||||
|
||||
intent2 := GetIntent("test.batch2")
|
||||
require.NotNil(t, intent2)
|
||||
assert.Equal(t, "batch2", intent2.Meta.Verb)
|
||||
"core.save": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "save",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Save {{.Subject}}?",
|
||||
Confirm: "Save {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} saved",
|
||||
Failure: "Failed to save {{.Subject}}",
|
||||
},
|
||||
|
||||
// Clean up
|
||||
UnregisterIntent("test.batch1")
|
||||
UnregisterIntent("test.batch2")
|
||||
"core.update": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "update",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Update {{.Subject}}?",
|
||||
Confirm: "Update {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} updated",
|
||||
Failure: "Failed to update {{.Subject}}",
|
||||
},
|
||||
|
||||
// Verify cleanup
|
||||
assert.False(t, HasIntent("test.batch1"))
|
||||
assert.False(t, HasIntent("test.batch2"))
|
||||
"core.rename": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "rename",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Rename {{.Subject}}?",
|
||||
Confirm: "Rename {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} renamed",
|
||||
Failure: "Failed to rename {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.move": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "move",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Move {{.Subject}}?",
|
||||
Confirm: "Move {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} moved",
|
||||
Failure: "Failed to move {{.Subject}}",
|
||||
},
|
||||
|
||||
// --- Git Actions ---
|
||||
|
||||
"core.commit": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "commit",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Commit {{.Subject}}?",
|
||||
Confirm: "Commit {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} committed",
|
||||
Failure: "Failed to commit {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.push": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "push",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Push {{.Subject}}?",
|
||||
Confirm: "Push {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} pushed",
|
||||
Failure: "Failed to push {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.pull": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "pull",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Pull {{.Subject}}?",
|
||||
Confirm: "Pull {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} pulled",
|
||||
Failure: "Failed to pull {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.merge": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "merge",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Merge {{.Subject}}?",
|
||||
Confirm: "Really merge {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} merged",
|
||||
Failure: "Failed to merge {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.rebase": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "rebase",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Rebase {{.Subject}}?",
|
||||
Confirm: "Really rebase {{.Subject}}? This rewrites history.",
|
||||
Success: "{{.Subject | title}} rebased",
|
||||
Failure: "Failed to rebase {{.Subject}}",
|
||||
},
|
||||
|
||||
// --- Network Actions ---
|
||||
|
||||
"core.install": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "install",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Install {{.Subject}}?",
|
||||
Confirm: "Install {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} installed",
|
||||
Failure: "Failed to install {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.download": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "download",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Download {{.Subject}}?",
|
||||
Confirm: "Download {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} downloaded",
|
||||
Failure: "Failed to download {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.upload": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "upload",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Upload {{.Subject}}?",
|
||||
Confirm: "Upload {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} uploaded",
|
||||
Failure: "Failed to upload {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.publish": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "publish",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Publish {{.Subject}}?",
|
||||
Confirm: "Really publish {{.Subject}}? This will be publicly visible.",
|
||||
Success: "{{.Subject | title}} published",
|
||||
Failure: "Failed to publish {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.deploy": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "deploy",
|
||||
Dangerous: true,
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Deploy {{.Subject}}?",
|
||||
Confirm: "Really deploy {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} deployed",
|
||||
Failure: "Failed to deploy {{.Subject}}",
|
||||
},
|
||||
|
||||
// --- Process Actions ---
|
||||
|
||||
"core.start": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "start",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Start {{.Subject}}?",
|
||||
Confirm: "Start {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} started",
|
||||
Failure: "Failed to start {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.stop": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "stop",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Stop {{.Subject}}?",
|
||||
Confirm: "Stop {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} stopped",
|
||||
Failure: "Failed to stop {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.restart": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "restart",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Restart {{.Subject}}?",
|
||||
Confirm: "Restart {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} restarted",
|
||||
Failure: "Failed to restart {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.run": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "run",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Run {{.Subject}}?",
|
||||
Confirm: "Run {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} completed",
|
||||
Failure: "Failed to run {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.build": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "build",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Build {{.Subject}}?",
|
||||
Confirm: "Build {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} built",
|
||||
Failure: "Failed to build {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.test": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "test",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Test {{.Subject}}?",
|
||||
Confirm: "Test {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} passed",
|
||||
Failure: "{{.Subject | title}} failed",
|
||||
},
|
||||
|
||||
// --- Information Actions ---
|
||||
|
||||
"core.continue": {
|
||||
Meta: IntentMeta{
|
||||
Type: "question",
|
||||
Verb: "continue",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Continue?",
|
||||
Confirm: "Continue?",
|
||||
Success: "Continuing",
|
||||
Failure: "Aborted",
|
||||
},
|
||||
|
||||
"core.proceed": {
|
||||
Meta: IntentMeta{
|
||||
Type: "question",
|
||||
Verb: "proceed",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Proceed?",
|
||||
Confirm: "Proceed?",
|
||||
Success: "Proceeding",
|
||||
Failure: "Aborted",
|
||||
},
|
||||
|
||||
"core.confirm": {
|
||||
Meta: IntentMeta{
|
||||
Type: "question",
|
||||
Verb: "confirm",
|
||||
Default: "no",
|
||||
},
|
||||
Question: "Are you sure?",
|
||||
Confirm: "Are you sure?",
|
||||
Success: "Confirmed",
|
||||
Failure: "Cancelled",
|
||||
},
|
||||
|
||||
// --- Additional Actions ---
|
||||
|
||||
"core.sync": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "sync",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Sync {{.Subject}}?",
|
||||
Confirm: "Sync {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} synced",
|
||||
Failure: "Failed to sync {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.boot": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "boot",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Boot {{.Subject}}?",
|
||||
Confirm: "Boot {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} booted",
|
||||
Failure: "Failed to boot {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.format": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "format",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Format {{.Subject}}?",
|
||||
Confirm: "Format {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} formatted",
|
||||
Failure: "Failed to format {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.analyse": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "analyse",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Analyse {{.Subject}}?",
|
||||
Confirm: "Analyse {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} analysed",
|
||||
Failure: "Failed to analyse {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.link": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "link",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Link {{.Subject}}?",
|
||||
Confirm: "Link {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} linked",
|
||||
Failure: "Failed to link {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.unlink": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "unlink",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Unlink {{.Subject}}?",
|
||||
Confirm: "Unlink {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} unlinked",
|
||||
Failure: "Failed to unlink {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.fetch": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "fetch",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Fetch {{.Subject}}?",
|
||||
Confirm: "Fetch {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} fetched",
|
||||
Failure: "Failed to fetch {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.generate": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "generate",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Generate {{.Subject}}?",
|
||||
Confirm: "Generate {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} generated",
|
||||
Failure: "Failed to generate {{.Subject}}",
|
||||
},
|
||||
|
||||
"core.validate": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "validate",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Validate {{.Subject}}?",
|
||||
Confirm: "Validate {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} valid",
|
||||
Failure: "{{.Subject | title}} invalid",
|
||||
},
|
||||
|
||||
"core.check": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "check",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Check {{.Subject}}?",
|
||||
Confirm: "Check {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} OK",
|
||||
Failure: "{{.Subject | title}} failed",
|
||||
},
|
||||
|
||||
"core.scan": {
|
||||
Meta: IntentMeta{
|
||||
Type: "action",
|
||||
Verb: "scan",
|
||||
Default: "yes",
|
||||
},
|
||||
Question: "Scan {{.Subject}}?",
|
||||
Confirm: "Scan {{.Subject}}?",
|
||||
Success: "{{.Subject | title}} scanned",
|
||||
Failure: "Failed to scan {{.Subject}}",
|
||||
},
|
||||
}
|
||||
|
||||
func TestCustomIntentOverridesCoreIntent(t *testing.T) {
|
||||
// Custom intents should be checked before core intents
|
||||
RegisterIntent("core.delete", Intent{
|
||||
Meta: IntentMeta{Type: "action", Verb: "delete", Default: "yes"},
|
||||
Question: "Custom delete {{.Subject}}?",
|
||||
})
|
||||
// customIntents holds user-registered intents.
|
||||
// Separated from coreIntents to allow thread-safe registration.
|
||||
var (
|
||||
customIntents = make(map[string]Intent)
|
||||
customIntentsMu sync.RWMutex
|
||||
)
|
||||
|
||||
// Should get custom intent
|
||||
intent := getIntent("core.delete")
|
||||
require.NotNil(t, intent)
|
||||
assert.Equal(t, "Custom delete {{.Subject}}?", intent.Question)
|
||||
assert.Equal(t, "yes", intent.Meta.Default) // Changed from core's "no"
|
||||
// getIntent retrieves an intent by its key.
|
||||
// Checks custom intents first, then falls back to core intents.
|
||||
// Returns nil if the intent is not found.
|
||||
func getIntent(key string) *Intent {
|
||||
// Check custom intents first (thread-safe)
|
||||
customIntentsMu.RLock()
|
||||
if intent, ok := customIntents[key]; ok {
|
||||
customIntentsMu.RUnlock()
|
||||
return &intent
|
||||
}
|
||||
customIntentsMu.RUnlock()
|
||||
|
||||
// Clean up
|
||||
UnregisterIntent("core.delete")
|
||||
|
||||
// Now should get core intent again
|
||||
intent = getIntent("core.delete")
|
||||
require.NotNil(t, intent)
|
||||
assert.Equal(t, "Delete {{.Subject}}?", intent.Question)
|
||||
assert.Equal(t, "no", intent.Meta.Default) // Back to core default
|
||||
// Fall back to core intents
|
||||
if intent, ok := coreIntents[key]; ok {
|
||||
return &intent
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestHasIntent(t *testing.T) {
|
||||
assert.True(t, HasIntent("core.delete"))
|
||||
assert.True(t, HasIntent("core.create"))
|
||||
assert.False(t, HasIntent("nonexistent.intent"))
|
||||
// RegisterIntent adds a custom intent at runtime.
|
||||
// Use this to extend the built-in intents with application-specific ones.
|
||||
// This function is thread-safe.
|
||||
//
|
||||
// i18n.RegisterIntent("myapp.archive", i18n.Intent{
|
||||
// Meta: i18n.IntentMeta{Type: "action", Verb: "archive", Default: "yes"},
|
||||
// Question: "Archive {{.Subject}}?",
|
||||
// Success: "{{.Subject | title}} archived",
|
||||
// Failure: "Failed to archive {{.Subject}}",
|
||||
// })
|
||||
func RegisterIntent(key string, intent Intent) {
|
||||
customIntentsMu.Lock()
|
||||
defer customIntentsMu.Unlock()
|
||||
customIntents[key] = intent
|
||||
}
|
||||
|
||||
func TestGetIntent_Public(t *testing.T) {
|
||||
intent := GetIntent("core.delete")
|
||||
require.NotNil(t, intent)
|
||||
assert.Equal(t, "delete", intent.Meta.Verb)
|
||||
|
||||
// Non-existent intent
|
||||
intent = GetIntent("nonexistent.intent")
|
||||
assert.Nil(t, intent)
|
||||
}
|
||||
|
||||
func TestIntentKeys(t *testing.T) {
|
||||
keys := IntentKeys()
|
||||
|
||||
// Should contain core intents
|
||||
assert.Contains(t, keys, "core.delete")
|
||||
assert.Contains(t, keys, "core.create")
|
||||
assert.Contains(t, keys, "core.save")
|
||||
assert.Contains(t, keys, "core.commit")
|
||||
assert.Contains(t, keys, "core.push")
|
||||
|
||||
// Should have a reasonable number of intents
|
||||
assert.GreaterOrEqual(t, len(keys), 20)
|
||||
}
|
||||
|
||||
func TestCoreIntents_Structure(t *testing.T) {
|
||||
// Verify all core intents have required fields
|
||||
for key, intent := range coreIntents {
|
||||
t.Run(key, func(t *testing.T) {
|
||||
// Meta should be set
|
||||
assert.NotEmpty(t, intent.Meta.Type, "intent %s missing Type", key)
|
||||
assert.NotEmpty(t, intent.Meta.Verb, "intent %s missing Verb", key)
|
||||
assert.NotEmpty(t, intent.Meta.Default, "intent %s missing Default", key)
|
||||
|
||||
// At least Question and one output should be set
|
||||
assert.NotEmpty(t, intent.Question, "intent %s missing Question", key)
|
||||
|
||||
// Default should be valid
|
||||
assert.Contains(t, []string{"yes", "no"}, intent.Meta.Default,
|
||||
"intent %s has invalid Default: %s", key, intent.Meta.Default)
|
||||
|
||||
// Type should be valid
|
||||
assert.Contains(t, []string{"action", "question", "info"}, intent.Meta.Type,
|
||||
"intent %s has invalid Type: %s", key, intent.Meta.Type)
|
||||
})
|
||||
// RegisterIntents adds multiple custom intents at runtime.
|
||||
// This is more efficient than calling RegisterIntent multiple times.
|
||||
// This function is thread-safe.
|
||||
//
|
||||
// i18n.RegisterIntents(map[string]i18n.Intent{
|
||||
// "myapp.archive": {
|
||||
// Meta: i18n.IntentMeta{Type: "action", Verb: "archive"},
|
||||
// Question: "Archive {{.Subject}}?",
|
||||
// },
|
||||
// "myapp.export": {
|
||||
// Meta: i18n.IntentMeta{Type: "action", Verb: "export"},
|
||||
// Question: "Export {{.Subject}}?",
|
||||
// },
|
||||
// })
|
||||
func RegisterIntents(intents map[string]Intent) {
|
||||
customIntentsMu.Lock()
|
||||
defer customIntentsMu.Unlock()
|
||||
for k, v := range intents {
|
||||
customIntents[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreIntents_Categories(t *testing.T) {
|
||||
// Destructive actions should be dangerous
|
||||
destructive := []string{"core.delete", "core.remove", "core.discard", "core.reset", "core.overwrite"}
|
||||
for _, key := range destructive {
|
||||
intent := getIntent(key)
|
||||
require.NotNil(t, intent, "missing intent: %s", key)
|
||||
assert.True(t, intent.Meta.Dangerous, "%s should be marked as dangerous", key)
|
||||
assert.Equal(t, "no", intent.Meta.Default, "%s should default to no", key)
|
||||
}
|
||||
|
||||
// Creation actions should not be dangerous
|
||||
creation := []string{"core.create", "core.add", "core.clone", "core.copy"}
|
||||
for _, key := range creation {
|
||||
intent := getIntent(key)
|
||||
require.NotNil(t, intent, "missing intent: %s", key)
|
||||
assert.False(t, intent.Meta.Dangerous, "%s should not be marked as dangerous", key)
|
||||
assert.Equal(t, "yes", intent.Meta.Default, "%s should default to yes", key)
|
||||
}
|
||||
// UnregisterIntent removes a custom intent by key.
|
||||
// This only affects custom intents, not core intents.
|
||||
// This function is thread-safe.
|
||||
func UnregisterIntent(key string) {
|
||||
customIntentsMu.Lock()
|
||||
defer customIntentsMu.Unlock()
|
||||
delete(customIntents, key)
|
||||
}
|
||||
|
||||
func TestCoreIntents_Templates(t *testing.T) {
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
// IntentKeys returns all registered intent keys (both core and custom).
|
||||
func IntentKeys() []string {
|
||||
customIntentsMu.RLock()
|
||||
defer customIntentsMu.RUnlock()
|
||||
|
||||
tests := []struct {
|
||||
intent string
|
||||
subject *Subject
|
||||
expectedQ string
|
||||
expectedSuccess string
|
||||
}{
|
||||
{
|
||||
intent: "core.delete",
|
||||
subject: S("file", "config.yaml"),
|
||||
expectedQ: "Delete config.yaml?",
|
||||
expectedSuccess: "Config.Yaml deleted", // strings.Title capitalizes after dots
|
||||
},
|
||||
{
|
||||
intent: "core.create",
|
||||
subject: S("directory", "src"),
|
||||
expectedQ: "Create src?",
|
||||
expectedSuccess: "Src created",
|
||||
},
|
||||
{
|
||||
intent: "core.commit",
|
||||
subject: S("changes", "3 files"),
|
||||
expectedQ: "Commit 3 files?",
|
||||
expectedSuccess: "3 Files committed",
|
||||
},
|
||||
{
|
||||
intent: "core.push",
|
||||
subject: S("commits", "5 commits"),
|
||||
expectedQ: "Push 5 commits?",
|
||||
expectedSuccess: "5 Commits pushed",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.intent, func(t *testing.T) {
|
||||
result := svc.C(tt.intent, tt.subject)
|
||||
|
||||
assert.Equal(t, tt.expectedQ, result.Question)
|
||||
assert.Equal(t, tt.expectedSuccess, result.Success)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestCoreIntents_TemplateErrors(t *testing.T) {
|
||||
// Templates with invalid syntax should return the original template
|
||||
RegisterIntent("test.invalid", Intent{
|
||||
Meta: IntentMeta{Type: "action", Verb: "test", Default: "yes"},
|
||||
Question: "{{.Invalid", // Invalid template syntax
|
||||
Success: "Success",
|
||||
Failure: "Failure",
|
||||
})
|
||||
defer delete(coreIntents, "test.invalid")
|
||||
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
result := svc.C("test.invalid", S("item", "test"))
|
||||
// Should return the original invalid template
|
||||
assert.Equal(t, "{{.Invalid", result.Question)
|
||||
}
|
||||
|
||||
func TestCoreIntents_TemplateFunctions(t *testing.T) {
|
||||
// Register an intent that uses template functions
|
||||
RegisterIntent("test.funcs", Intent{
|
||||
Meta: IntentMeta{Type: "action", Verb: "test", Default: "yes"},
|
||||
Question: "Process {{.Subject | quote}}?",
|
||||
Success: "{{.Subject | upper}} processed",
|
||||
Failure: "{{.Subject | lower}} failed",
|
||||
})
|
||||
defer delete(coreIntents, "test.funcs")
|
||||
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
result := svc.C("test.funcs", S("item", "Test"))
|
||||
|
||||
assert.Equal(t, `Process "Test"?`, result.Question)
|
||||
assert.Equal(t, "TEST processed", result.Success)
|
||||
assert.Equal(t, "test failed", result.Failure)
|
||||
}
|
||||
|
||||
func TestIntentT_Integration(t *testing.T) {
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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")
|
||||
assert.Equal(t, "Multi-repo development workflow", result)
|
||||
}
|
||||
|
||||
func TestIntent_EmptyTemplates(t *testing.T) {
|
||||
RegisterIntent("test.empty", Intent{
|
||||
Meta: IntentMeta{Type: "info", Verb: "info", Default: "yes"},
|
||||
Question: "Question",
|
||||
Confirm: "", // Empty confirm
|
||||
Success: "", // Empty success
|
||||
Failure: "", // Empty failure
|
||||
})
|
||||
defer delete(coreIntents, "test.empty")
|
||||
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
result := svc.C("test.empty", S("item", "test"))
|
||||
|
||||
assert.Equal(t, "Question", result.Question)
|
||||
assert.Equal(t, "", result.Confirm)
|
||||
assert.Equal(t, "", result.Success)
|
||||
assert.Equal(t, "", result.Failure)
|
||||
}
|
||||
|
||||
func TestCoreIntents_AllKeysPrefixed(t *testing.T) {
|
||||
keys := make([]string, 0, len(coreIntents)+len(customIntents))
|
||||
for key := range coreIntents {
|
||||
assert.True(t, strings.HasPrefix(key, "core."),
|
||||
"intent key %q should be prefixed with 'core.'", key)
|
||||
keys = append(keys, key)
|
||||
}
|
||||
for key := range customIntents {
|
||||
// Avoid duplicates if custom overrides core
|
||||
found := false
|
||||
for _, k := range keys {
|
||||
if k == key {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// HasIntent returns true if an intent with the given key exists.
|
||||
func HasIntent(key string) bool {
|
||||
return getIntent(key) != nil
|
||||
}
|
||||
|
||||
// GetIntent returns the intent for a key, or nil if not found.
|
||||
// This is the public API for retrieving intents.
|
||||
func GetIntent(key string) *Intent {
|
||||
return getIntent(key)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,12 +32,6 @@ type Translator interface {
|
|||
// svc.T("cli.count.items", map[string]any{"Count": 5})
|
||||
T(messageID string, args ...any) string
|
||||
|
||||
// C composes a semantic intent with a subject.
|
||||
// Returns all output forms (Question, Confirm, Success, Failure).
|
||||
//
|
||||
// result := svc.C("core.delete", S("file", "config.yaml"))
|
||||
C(intent string, subject *Subject) *Composed
|
||||
|
||||
// SetLanguage sets the language for translations.
|
||||
// Returns an error if the language is not supported.
|
||||
SetLanguage(lang string) error
|
||||
|
|
@ -94,8 +88,8 @@ type MissingKeyHandler func(missing MissingKey)
|
|||
type MissingKey struct {
|
||||
Key string // The missing translation key
|
||||
Args map[string]any // Arguments passed to the translation
|
||||
CallerFile string // Source file where T()/C() was called
|
||||
CallerLine int // Line number where T()/C() was called
|
||||
CallerFile string // Source file where T() was called
|
||||
CallerLine int // Line number where T() was called
|
||||
}
|
||||
|
||||
// MissingKeyAction is an alias for backwards compatibility.
|
||||
|
|
|
|||
|
|
@ -38,15 +38,6 @@ func (m *MockTranslator) T(key string, args ...any) string {
|
|||
return key
|
||||
}
|
||||
|
||||
func (m *MockTranslator) C(intent string, subject *Subject) *Composed {
|
||||
return &Composed{
|
||||
Question: "Mock: " + intent,
|
||||
Confirm: "Mock confirm",
|
||||
Success: "Mock success",
|
||||
Failure: "Mock failure",
|
||||
}
|
||||
}
|
||||
|
||||
func (m *MockTranslator) SetLanguage(lang string) error {
|
||||
m.language = lang
|
||||
return nil
|
||||
|
|
@ -56,15 +47,17 @@ func (m *MockTranslator) Language() string {
|
|||
return m.language
|
||||
}
|
||||
|
||||
func (m *MockTranslator) SetMode(mode Mode) {}
|
||||
func (m *MockTranslator) Mode() Mode { return ModeNormal }
|
||||
func (m *MockTranslator) SetDebug(enabled bool) {}
|
||||
func (m *MockTranslator) Debug() bool { return false }
|
||||
func (m *MockTranslator) SetMode(mode Mode) {}
|
||||
func (m *MockTranslator) Mode() Mode { return ModeNormal }
|
||||
func (m *MockTranslator) SetDebug(enabled bool) {}
|
||||
func (m *MockTranslator) Debug() bool { return false }
|
||||
func (m *MockTranslator) SetFormality(f Formality) {}
|
||||
func (m *MockTranslator) Formality() Formality { return FormalityNeutral }
|
||||
func (m *MockTranslator) Formality() Formality { return FormalityNeutral }
|
||||
func (m *MockTranslator) Direction() TextDirection { return DirLTR }
|
||||
func (m *MockTranslator) IsRTL() bool { return false }
|
||||
func (m *MockTranslator) PluralCategory(n int) PluralCategory { return PluralOther }
|
||||
func (m *MockTranslator) IsRTL() bool { return false }
|
||||
func (m *MockTranslator) PluralCategory(n int) PluralCategory {
|
||||
return PluralOther
|
||||
}
|
||||
func (m *MockTranslator) AvailableLanguages() []string { return []string{"en-GB"} }
|
||||
|
||||
func TestMockTranslator(t *testing.T) {
|
||||
|
|
@ -78,7 +71,4 @@ func TestMockTranslator(t *testing.T) {
|
|||
assert.Equal(t, "Hello from mock", translator.T("test.hello"))
|
||||
assert.Equal(t, "test.missing", translator.T("test.missing"))
|
||||
assert.Equal(t, "en-GB", translator.Language())
|
||||
|
||||
result := translator.C("core.delete", S("file", "test.txt"))
|
||||
assert.Equal(t, "Mock: core.delete", result.Question)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,37 +160,3 @@ func TestModeCollect_MissingKey(t *testing.T) {
|
|||
assert.Greater(t, received.CallerLine, 0)
|
||||
}
|
||||
|
||||
func TestModeCollect_MissingIntent(t *testing.T) {
|
||||
// Reset handler after test
|
||||
defer SetActionHandler(nil)
|
||||
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
svc.SetMode(ModeCollect)
|
||||
|
||||
var received MissingKeyAction
|
||||
SetActionHandler(func(action MissingKeyAction) {
|
||||
received = action
|
||||
})
|
||||
|
||||
// Missing intent should dispatch action and return [key]
|
||||
result := svc.C("nonexistent.intent", S("file", "test.txt"))
|
||||
|
||||
assert.Equal(t, "[nonexistent.intent]", result.Question)
|
||||
assert.Equal(t, "[nonexistent.intent]", result.Success)
|
||||
assert.Equal(t, "[nonexistent.intent]", result.Failure)
|
||||
assert.Equal(t, "nonexistent.intent", received.Key)
|
||||
}
|
||||
|
||||
func TestModeStrict_MissingIntent(t *testing.T) {
|
||||
svc, err := New()
|
||||
require.NoError(t, err)
|
||||
|
||||
svc.SetMode(ModeStrict)
|
||||
|
||||
// Missing intent should panic
|
||||
assert.Panics(t, func() {
|
||||
svc.C("nonexistent.intent", S("file", "test.txt"))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -459,67 +459,6 @@ func (s *Service) handleMissingKey(key string, args []any) string {
|
|||
}
|
||||
}
|
||||
|
||||
// C composes a semantic intent with a subject.
|
||||
// Returns all output forms (Question, Confirm, Success, Failure) for the intent.
|
||||
//
|
||||
// result := svc.C("core.delete", S("file", "config.yaml"))
|
||||
// fmt.Println(result.Question) // "Delete config.yaml?"
|
||||
// fmt.Println(result.Success) // "Config.yaml deleted"
|
||||
func (s *Service) C(intent string, subject *Subject) *Composed {
|
||||
// Look up the intent definition
|
||||
intentDef := getIntent(intent)
|
||||
if intentDef == nil {
|
||||
// Intent not found, handle as missing key
|
||||
s.mu.RLock()
|
||||
mode := s.mode
|
||||
s.mu.RUnlock()
|
||||
|
||||
switch mode {
|
||||
case ModeStrict:
|
||||
panic(fmt.Sprintf("i18n: missing intent %q", intent))
|
||||
case ModeCollect:
|
||||
dispatchMissingKey(intent, nil)
|
||||
return &Composed{
|
||||
Question: "[" + intent + "]",
|
||||
Confirm: "[" + intent + "]",
|
||||
Success: "[" + intent + "]",
|
||||
Failure: "[" + intent + "]",
|
||||
}
|
||||
default:
|
||||
return &Composed{
|
||||
Question: intent,
|
||||
Confirm: intent,
|
||||
Success: intent,
|
||||
Failure: intent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create template data from subject
|
||||
data := newTemplateData(subject)
|
||||
|
||||
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 {
|
||||
result.Question = debugFormat(intent, result.Question)
|
||||
result.Confirm = debugFormat(intent, result.Confirm)
|
||||
result.Success = debugFormat(intent, result.Success)
|
||||
result.Failure = debugFormat(intent, result.Failure)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Raw is the raw translation helper without i18n.* namespace magic.
|
||||
// Use T() for smart i18n.* handling, Raw() for direct key lookup.
|
||||
func (s *Service) Raw(messageID string, args ...any) string {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue