Files
Fabric/internal/plugins/plugin.go
Kayvan Sylvan 7570e7930b feat: localize setup process and add funding configuration
- Add GitHub and Buy Me a Coffee funding configuration.
- Localize setup prompts and error messages across multiple languages.
- Implement helper for localized questions with static environment keys.
- Update environment variable builder to handle hyphenated plugin names.
- Replace hardcoded console output with localized i18n translation strings.
- Expand locale files with comprehensive pattern and strategy translations.
- Add new i18n keys for optional and required markers
- Remove hardcoded `[required]` markers from description strings
- Add custom patterns, Jina AI, YouTube, and language labels
- Switch plugin descriptions to use i18n translation keys
- Append markers dynamically to setup descriptions in Go code
- Remove trailing newlines from plugin question prompt strings
- Standardize all locale files with consistent formatting changes
2025-12-22 09:39:02 -08:00

335 lines
7.8 KiB
Go

package plugins
import (
"bytes"
"fmt"
"os"
"strings"
"github.com/danielmiessler/fabric/internal/i18n"
)
const AnswerReset = "reset"
const SettingTypeBool = "bool"
type Plugin interface {
GetName() string
GetSetupDescription() string
IsConfigured() bool
Configure() error
Setup() error
SetupFillEnvFileContent(*bytes.Buffer)
}
type PluginBase struct {
Settings
SetupQuestions
Name string
SetupDescription string
EnvNamePrefix string
ConfigureCustom func() error
}
func (o *PluginBase) GetName() string {
return o.Name
}
func (o *PluginBase) GetSetupDescription() (ret string) {
if ret = o.SetupDescription; ret == "" {
ret = o.GetName()
}
return
}
func (o *PluginBase) AddSetting(name string, required bool) (ret *Setting) {
ret = NewSetting(fmt.Sprintf("%v%v", o.EnvNamePrefix, BuildEnvVariable(name)), required)
o.Settings = append(o.Settings, ret)
return
}
func (o *PluginBase) AddSetupQuestion(name string, required bool) (ret *SetupQuestion) {
return o.AddSetupQuestionCustom(name, required, "")
}
func (o *PluginBase) AddSetupQuestionCustom(name string, required bool, question string) (ret *SetupQuestion) {
setting := o.AddSetting(name, required)
ret = &SetupQuestion{Setting: setting, Question: question}
if ret.Question == "" {
ret.Question = fmt.Sprintf(i18n.T("plugin_enter_value"), o.Name, strings.ToUpper(name))
}
o.SetupQuestions = append(o.SetupQuestions, ret)
return
}
// AddSetupQuestionWithEnvName creates a setup question with an explicit environment variable name.
// This is useful when you want the environment variable name to remain constant across languages.
// The envVarName is used for the environment variable, while the question is localized.
func (o *PluginBase) AddSetupQuestionWithEnvName(envVarName string, required bool, question string) (ret *SetupQuestion) {
setting := o.AddSetting(envVarName, required)
ret = &SetupQuestion{Setting: setting, Question: question}
o.SetupQuestions = append(o.SetupQuestions, ret)
return
}
func (o *PluginBase) AddSetupQuestionBool(name string, required bool) (ret *SetupQuestion) {
return o.AddSetupQuestionCustomBool(name, required, "")
}
func (o *PluginBase) AddSetupQuestionCustomBool(name string, required bool, question string) (ret *SetupQuestion) {
setting := o.AddSetting(name, required)
setting.Type = SettingTypeBool
ret = &SetupQuestion{Setting: setting, Question: question}
if ret.Question == "" {
ret.Question = fmt.Sprintf(i18n.T("plugin_enable_bool_question"), o.Name, strings.ToUpper(name))
}
o.SetupQuestions = append(o.SetupQuestions, ret)
return
}
func (o *PluginBase) Configure() (err error) {
if err = o.Settings.Configure(); err != nil {
return
}
if o.ConfigureCustom != nil {
err = o.ConfigureCustom()
}
return
}
func (o *PluginBase) Setup() (err error) {
if err = o.Ask(o.Name); err != nil {
return
}
// After Setup, run ConfigureCustom if present, but skip re-validation
// since Ask() already validated user input (or allowed explicit reset)
if o.ConfigureCustom != nil {
err = o.ConfigureCustom()
}
return
}
func (o *PluginBase) SetupOrSkip() (err error) {
if err = o.Setup(); err != nil {
fmt.Printf(i18n.T("plugin_setup_skipped"), o.GetName())
}
return
}
func (o *PluginBase) SetupFillEnvFileContent(fileEnvFileContent *bytes.Buffer) {
o.Settings.FillEnvFileContent(fileEnvFileContent)
}
func NewSetting(envVariable string, required bool) *Setting {
return &Setting{
EnvVariable: envVariable,
Required: required,
}
}
// In plugins/plugin.go
type Setting struct {
EnvVariable string
Value string
Required bool
Type string // "string" (default), "bool"
}
func (o *Setting) IsValid() bool {
if o.Type == SettingTypeBool {
_, err := ParseBool(o.Value)
return (err == nil) || !o.Required
}
return o.IsDefined() || !o.Required
}
func (o *Setting) Print() {
if o.Type == SettingTypeBool {
v, _ := ParseBool(o.Value)
fmt.Printf("%v: %v\n", o.EnvVariable, v)
} else {
fmt.Printf("%v: %v\n", o.EnvVariable, o.Value)
}
}
func (o *Setting) FillEnvFileContent(buffer *bytes.Buffer) {
if o.IsDefined() {
buffer.WriteString(o.EnvVariable)
buffer.WriteString("=")
if o.Type == SettingTypeBool {
v, _ := ParseBool(o.Value)
buffer.WriteString(fmt.Sprintf("%v", v))
} else {
buffer.WriteString(o.Value)
}
buffer.WriteString("\n")
}
}
func ParseBoolElseFalse(val string) (ret bool) {
ret, _ = ParseBool(val)
return
}
func ParseBool(val string) (bool, error) {
switch strings.ToLower(strings.TrimSpace(val)) {
case "1", "true", "yes", "on":
return true, nil
case "0", "false", "no", "off":
return false, nil
}
return false, fmt.Errorf(i18n.T("plugin_invalid_bool"), val)
}
type SetupQuestion struct {
*Setting
Question string
}
func (o *SetupQuestion) Ask(label string) (err error) {
var prefix string
if label != "" {
prefix = fmt.Sprintf("[%v] ", label)
} else {
prefix = ""
}
fmt.Println()
if o.Type == SettingTypeBool {
current := "false"
if v, err := ParseBool(o.Value); err == nil && v {
current = "true"
}
fmt.Printf(i18n.T("plugin_question_bool"), prefix, o.Question, current, AnswerReset)
} else if o.Value != "" {
fmt.Printf(i18n.T("plugin_question_with_default"), prefix, o.Question, o.Value, AnswerReset)
} else {
fmt.Printf(i18n.T("plugin_question_optional"), prefix, o.Question)
}
var answer string
fmt.Scanln(&answer)
answer = strings.TrimRight(answer, "\n")
isReset := strings.ToLower(answer) == AnswerReset
if answer == "" {
answer = o.Value
} else if isReset {
answer = ""
}
err = o.OnAnswerWithReset(answer, isReset)
return
}
func (o *SetupQuestion) OnAnswer(answer string) (err error) {
return o.OnAnswerWithReset(answer, false)
}
func (o *SetupQuestion) OnAnswerWithReset(answer string, isReset bool) (err error) {
if o.Type == SettingTypeBool {
if answer == "" {
o.Value = ""
} else {
_, err := ParseBool(answer)
if err != nil {
return fmt.Errorf(i18n.T("plugin_invalid_boolean_value"), answer)
}
o.Value = strings.ToLower(answer)
}
} else {
o.Value = answer
}
if o.EnvVariable != "" {
if err = os.Setenv(o.EnvVariable, o.Value); err != nil {
return
}
}
// Skip validation when explicitly resetting a value - the user intentionally
// wants to clear the value even if it's required
if isReset {
return nil
}
err = o.IsValidErr()
return
}
func (o *Setting) IsValidErr() (err error) {
if !o.IsValid() {
err = fmt.Errorf(i18n.T("plugin_setting_not_valid"), o.EnvVariable, o.Value)
}
return
}
func (o *Setting) IsDefined() bool {
return o.Value != ""
}
func (o *Setting) Configure() error {
envValue := os.Getenv(o.EnvVariable)
if envValue != "" {
o.Value = envValue
}
return o.IsValidErr()
}
func NewSetupQuestion(question string) *SetupQuestion {
return &SetupQuestion{Setting: &Setting{}, Question: question}
}
type Settings []*Setting
func (o Settings) IsConfigured() (ret bool) {
ret = true
for _, setting := range o {
if ret = setting.IsValid(); !ret {
break
}
}
return
}
func (o Settings) Configure() (err error) {
for _, setting := range o {
if err = setting.Configure(); err != nil {
break
}
}
return
}
func (o Settings) FillEnvFileContent(buffer *bytes.Buffer) {
for _, setting := range o {
setting.FillEnvFileContent(buffer)
}
}
type SetupQuestions []*SetupQuestion
func (o SetupQuestions) Ask(label string) (err error) {
fmt.Println()
fmt.Printf("[%v]\n", label)
for _, question := range o {
if err = question.Ask(""); err != nil {
break
}
}
return
}
func BuildEnvVariablePrefix(name string) (ret string) {
ret = BuildEnvVariable(name)
if ret != "" {
ret += "_"
}
return
}
func BuildEnvVariable(name string) string {
name = strings.TrimSpace(name)
name = strings.ToUpper(name)
name = strings.ReplaceAll(name, " ", "_")
name = strings.ReplaceAll(name, "-", "_")
return name
}