Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 20 additions & 20 deletions docs/_v2/guides/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,39 @@ We recommend reading the guides in the following order.
### 1. The Basics
Start here to learn the fundamentals of building a CLI with `goopt`.

* **[Getting Started]({{ site.baseurl }}/v2/01-getting-started/):** Build your first application in 5 minutes.
* **[Core Concepts]({{ site.baseurl }}/v2/02-core-concepts/):** Understand the key building blocks and design philosophy of the library.
* **[Getting Started]({{ site.baseurl }}/v2/guides/01-getting-started/):** Build your first application in 5 minutes.
* **[Core Concepts]({{ site.baseurl }}/v2/guides/02-core-concepts/):** Understand the key building blocks and design philosophy of the library.

### 2. Structuring Your Application
These guides cover the patterns for defining your flags, commands, and arguments.

* **[Defining Your CLI]({{ site.baseurl }}/v2/03-defining-your-cli/index/):** An overview of the different ways to structure your application's interface.
* **[Struct Tags Reference]({{ site.baseurl }}/v2/03-defining-your-cli/01-struct-tags-reference/):** A quick reference for all available `goopt:"..."` tags.
* **[Command Patterns]({{ site.baseurl }}/v2/03-defining-your-cli/02-command-patterns/):** Learn to organize commands using nested structs, paths, and programmatic builders.
* **[Flag Patterns]({{ site.baseurl }}/v2/03-defining-your-cli/03-flag-patterns/):** Explore patterns for namespacing and reusing flag groups.
* **[Positional Arguments]({{ site.baseurl }}/v2/03-defining-your-cli/04-positional-arguments/):** A detailed guide on position-dependent arguments.
* **[Command Callbacks]({{ site.baseurl }}/v2/03-defining-your-cli/05-command-callbacks/):** Learn how to attach behavior to your commands.
* **[Defining Your CLI]({{ site.baseurl }}/v2/guides/03-defining-your-cli/index/):** An overview of the different ways to structure your application's interface.
* **[Struct Tags Reference]({{ site.baseurl }}/v2/guides/03-defining-your-cli/01-struct-tags-reference/):** A quick reference for all available `goopt:"..."` tags.
* **[Command Patterns]({{ site.baseurl }}/v2/guides/03-defining-your-cli/02-command-patterns/):** Learn to organize commands using nested structs, paths, and programmatic builders.
* **[Flag Patterns]({{ site.baseurl }}/v2/guides/03-defining-your-cli/03-flag-patterns/):** Explore patterns for namespacing and reusing flag groups.
* **[Positional Arguments]({{ site.baseurl }}/v2/guides/03-defining-your-cli/04-positional-arguments/):** A detailed guide on position-dependent arguments.
* **[Command Callbacks]({{ site.baseurl }}/v2/guides/03-defining-your-cli/05-command-callbacks/):** Learn how to attach behavior to your commands.

### 3. Advanced Features
Dive deeper into the powerful features that make `goopt` suitable for complex, production-grade applications.

* **[Advanced Features Overview]({{ site.baseurl }}/v2/04-advanced-features/index/)**
* **[Validation]({{ site.baseurl }}/v2/04-advanced-features/01-validation/):** Ensure data correctness with built-in and custom validators.
* **[Execution Hooks]({{ site.baseurl }}/v2/04-advanced-features/02-execution-hooks/):** Manage the command lifecycle with pre- and post-execution hooks.
* **[Error Handling]({{ site.baseurl }}/v2/04-advanced-features/03-error-handling/):** Best practices for robust error handling during setup.
* **[Flag Inheritance]({{ site.baseurl }}/v2/04-advanced-features/04-flag-inheritance/):** Understand how flags are resolved in nested command hierarchies.
* **[Advanced Features Overview]({{ site.baseurl }}/v2/guides/04-advanced-features/index/)**
* **[Validation]({{ site.baseurl }}/v2//guides/04-advanced-features/01-validation/):** Ensure data correctness with built-in and custom validators.
* **[Execution Hooks]({{ site.baseurl }}/v2/guides/04-advanced-features/02-execution-hooks/):** Manage the command lifecycle with pre- and post-execution hooks.
* **[Error Handling]({{ site.baseurl }}/v2/guides/04-advanced-features/03-error-handling/):** Best practices for robust error handling during setup.
* **[Flag Inheritance]({{ site.baseurl }}/v2/guides/04-advanced-features/04-flag-inheritance/):** Understand how flags are resolved in nested command hierarchies.

### 4. Built-in Functionality
Learn about the powerful "batteries-included" features that come with `goopt`.

* **[Built-in Features Overview]({{ site.baseurl }}/v2/05-built-in-features/index/)**
* **[The Help System]({{ site.baseurl }}/v2/05-built-in-features/01-help-system/):** Customize the adaptive and interactive help system.
* **[Version Flag Support]({{ site.baseurl }}/v2/05-built-in-features/02-version-support/):** Automatically add a `--version` flag.
* **[Shell Completion]({{ site.baseurl }}/v2/05-built-in-features/03-shell-completion/):** Generate completion scripts for popular shells.
* **[Environment & External Configuration]({{ site.baseurl }}/v2/05-built-in-features/04-environment-config/):** Load configuration from environment variables or files.
* **[Built-in Features Overview]({{ site.baseurl }}/v2/guides/05-built-in-features/index/)**
* **[The Help System]({{ site.baseurl }}/v2/guides/05-built-in-features/01-help-system/):** Customize the adaptive and interactive help system.
* **[Version Flag Support]({{ site.baseurl }}/v2/guides/05-built-in-features/02-version-support/):** Automatically add a `--version` flag.
* **[Shell Completion]({{ site.baseurl }}/v2/guides/05-built-in-features/03-shell-completion/):** Generate completion scripts for popular shells.
* **[Environment & External Configuration]({{ site.baseurl }}/v2/guides/05-built-in-features/04-environment-config/):** Load configuration from environment variables or files.

### 5. Internationalization (i18n)
A comprehensive guide to creating multi-language CLIs.

* **[Internationalization Overview]({{ site.baseurl }}/v2/06-internationalization/index/)**
* **[Tooling: `goopt-i18n-gen`]({{ site.baseurl }}/v2/06-internationalization/01-tooling-goopt-i18n-gen/):** A deep dive into the powerful code generation and workflow tool for i18n.
* **[Internationalization Overview]({{ site.baseurl }}/v2/guides/06-internationalization/index/)**
* **[Tooling: `goopt-i18n-gen`]({{ site.baseurl }}/v2/guides/06-internationalization/01-tooling-goopt-i18n-gen/):** A deep dive into the powerful code generation and workflow tool for i18n.
2 changes: 1 addition & 1 deletion goopt.go
Original file line number Diff line number Diff line change
Expand Up @@ -1147,7 +1147,7 @@ func (p *Parser) SetFlag(flag, value string, commandPath ...string) error {
p.options[flag] = value
key = flag
}
arg, err := p.GetArgument(key)
arg, err := p.GetArgument(key, commandPath...)
if err != nil {
return err
}
Expand Down
15 changes: 14 additions & 1 deletion v2/cmd/goopt-i18n-gen/ast/format_transformer.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ type FormatTransformer struct {
transformed bool // tracks if any transformations were made
detector *FormatDetector // generic format function detector
packagePath string // path to the messages package
varName string // variable name for generated constants (default: "Keys")
transformMode string // "user-facing", "with-comments", "all-marked", "all"
i18nTodoMap map[token.Pos]string // maps string literal positions to i18n-todo message keys
userFacingRegexes []*regexp.Regexp // regex patterns to identify user-facing functions
Expand All @@ -41,6 +42,7 @@ func NewFormatTransformer(stringMap map[string]string) *FormatTransformer {
transformed: false,
detector: NewFormatDetector(),
packagePath: "messages", // default value (will be resolved to full module path)
varName: "Keys", // default variable name
transformMode: "user-facing", // default to only transforming known user-facing functions
i18nTodoMap: make(map[token.Pos]string),
skipPositions: make(map[token.Pos]bool),
Expand All @@ -54,6 +56,13 @@ func (ft *FormatTransformer) SetMessagePackagePath(path string) {
ft.packagePath = path
}

// SetVarName sets the variable name for generated constants
func (ft *FormatTransformer) SetVarName(varName string) {
if varName != "" {
ft.varName = varName
}
}

// SetTransformMode sets the transformation mode
func (ft *FormatTransformer) SetTransformMode(mode string) {
ft.transformMode = mode
Expand Down Expand Up @@ -656,7 +665,11 @@ func (ft *FormatTransformer) createKeyExpr(key string) ast.Expr {
packageName = pathParts[len(pathParts)-1]
}

astKey := packageName + ".Keys." + goKey
varName := ft.varName
if varName == "" {
varName = "Keys"
}
astKey := packageName + "." + varName + "." + goKey
parts := strings.Split(astKey, ".")

// Start with the first part
Expand Down
19 changes: 15 additions & 4 deletions v2/cmd/goopt-i18n-gen/ast/string_extractor.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"regexp"
"strconv"
"strings"

"github.com/napalu/goopt/v2/i18n"
Expand Down Expand Up @@ -276,8 +277,12 @@ func (se *StringExtractor) extractFromFunction(fset *token.FileSet, filename, fu
}
case *ast.BasicLit:
if node.Kind == token.STRING {
// Get the actual string value (remove quotes)
value := strings.Trim(node.Value, "`\"")
// Get the actual string value (decode Go literal)
value, err := strconv.Unquote(node.Value)
if err != nil {
// Fallback: trim quotes in case of unexpected literal forms
value = strings.Trim(node.Value, "`\"")
}

// Apply filters
if !se.shouldExtract(value) {
Expand Down Expand Up @@ -457,7 +462,10 @@ func (se *StringExtractor) extractFromNode(fset *token.FileSet, filename, contex
if funcName == "errors.New" && len(x.Args) > 0 {
// Extract string from first argument
if lit, ok := x.Args[0].(*ast.BasicLit); ok && lit.Kind == token.STRING {
value := strings.Trim(lit.Value, "`\"")
value, err := strconv.Unquote(lit.Value)
if err != nil {
value = strings.Trim(lit.Value, "`\"")
}
if se.shouldExtract(value) {
pos := fset.Position(lit.Pos())
location := StringLocation{
Expand Down Expand Up @@ -485,7 +493,10 @@ func (se *StringExtractor) extractFromNode(fset *token.FileSet, filename, contex
case *ast.BasicLit:
if x.Kind == token.STRING {
// Extract raw string literals not in function calls
value := strings.Trim(x.Value, "`\"")
value, err := strconv.Unquote(x.Value)
if err != nil {
value = strings.Trim(x.Value, "`\"")
}
if se.shouldExtract(value) {
pos := fset.Position(x.Pos())
location := StringLocation{
Expand Down
15 changes: 12 additions & 3 deletions v2/cmd/goopt-i18n-gen/ast/transformation_replacer.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func (sr *TransformationReplacer) SetKeyMap(keyMap map[string]string) {

sr.formatTransformer = NewFormatTransformer(quotedKeyMap)
sr.formatTransformer.SetMessagePackagePath(sr.config.PackagePath)
sr.formatTransformer.SetVarName(sr.config.VarName)
sr.formatTransformer.SetTransformMode(sr.config.TransformMode)
sr.formatTransformer.SetTranslatorPattern(sr.config.TrPattern)

Expand Down Expand Up @@ -192,6 +193,8 @@ func (sr *TransformationReplacer) isInUserFacingFunction(lit *ast.BasicLit) bool
"fmt.Sprintf": true,
"fmt.Fprintf": true,
"fmt.Errorf": true,
"fmt.Println": true,
"fmt.Print": true,
"log.Printf": true,
"log.Fatalf": true,
"log.Panicf": true,
Expand Down Expand Up @@ -503,7 +506,13 @@ func (sr *TransformationReplacer) convertKeyToASTFormat(key string) string {
packageName = parts[len(parts)-1]
}

return packageName + ".Keys." + strings.Join(astParts, ".")
// Use configured variable name (default: "Keys")
varName := sr.config.VarName
if varName == "" {
varName = "Keys"
}

return packageName + "." + varName + "." + strings.Join(astParts, ".")
}

func (sr *TransformationReplacer) findI18nComments(fset *token.FileSet, filename string, node *ast.File) {
Expand Down Expand Up @@ -684,7 +693,6 @@ func (sr *TransformationReplacer) walkASTWithParents(node ast.Node, visit func(a
if x.Body != nil {
walk(x.Body)
}
// Add more node types as needed
}

return true
Expand Down Expand Up @@ -828,7 +836,8 @@ func (sr *TransformationReplacer) createFormatFunctionComment(key, value string,
// and the function should change from Msgf to Msg
argPlaceholders := make([]string, numArgs)
for i := 0; i < numArgs; i++ {
argPlaceholders[i] = "arg" + fmt.Sprintf("%d", i+1)
argPlaceholders[i] = "arg" +
fmt.Sprintf("%d", i+1)
}

comment := fmt.Sprintf("%s(%s, %s)", pattern, astKey, strings.Join(argPlaceholders, ", "))
Expand Down
1 change: 1 addition & 0 deletions v2/cmd/goopt-i18n-gen/common/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ type TransformationConfig struct {
TransformMode string // "user-facing", "with-comments", "all-marked", "all"
BackupDir string
PackagePath string
VarName string // Variable name for generated constants (default: "Keys")
UserFacingRegex []string // regex patterns to identify user-facing functions
FormatFunctionRegex []string // regex patterns with format arg index (pattern:index)
}
1 change: 1 addition & 0 deletions v2/cmd/goopt-i18n-gen/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"app.generate_locales_cmd.dry_run_desc": "Vorschau was generiert würde",
"app.generate_locales_cmd.output_desc": "Ausgabeverzeichnis für Locale-Pakete",
"app.generate_locales_cmd.package_base_desc": "Basis-Paketpfad",
"app.generate_cmd.var_name_desc": "Variablenname für generierte Konstanten",
"app.global.help_desc": "Hilfe anzeigen",
"app.global.input_desc": "Eingabe-JSON-Dateien (unterstützt Wildcards)",
"app.global.language_desc": "Sprache für Ausgabe (en, de, fr)",
Expand Down
1 change: 1 addition & 0 deletions v2/cmd/goopt-i18n-gen/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,7 @@
"app.generate_cmd.output_desc": "Output Go file",
"app.generate_cmd.package_desc": "Package name",
"app.generate_cmd.prefix_desc": "Optional prefix to strip from keys",
"app.generate_cmd.var_name_desc": "Variable name for generated constants",
"app.generate_locales_cmd.dry_run_desc": "Preview what would be generated",
"app.generate_locales_cmd.output_desc": "Output directory for locale packages",
"app.generate_locales_cmd.package_base_desc": "Base package path",
Expand Down
1 change: 1 addition & 0 deletions v2/cmd/goopt-i18n-gen/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -183,6 +183,7 @@
"app.generate_locales_cmd.dry_run_desc": "Aperçu de ce qui serait généré",
"app.generate_locales_cmd.output_desc": "Répertoire de sortie pour les packages de locale",
"app.generate_locales_cmd.package_base_desc": "Chemin du package de base",
"app.generate_cmd.var_name_desc": "Nom de variable pour les constantes générées",
"app.global.help_desc": "Afficher l'aide",
"app.global.input_desc": "Fichiers JSON d'entrée (supporte les wildcards)",
"app.global.language_desc": "Langue pour la sortie (en, de, fr)",
Expand Down
8 changes: 7 additions & 1 deletion v2/cmd/goopt-i18n-gen/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ import (
var localesFS embed.FS

func main() {
cfg := &options.AppConfig{}
cfg := &options.AppConfig{
Config: &options.Config{},
}

// Assign command functions
cfg.Generate.Exec = translations.Generate
Expand All @@ -33,6 +35,10 @@ func main() {
cfg.Extract.Exec = translations.Extract
cfg.Sync.Exec = translations.Sync
cfg.GenerateLocales.Exec = translations.GenerateLocales
// Shared options
cfg.Extract.Shared = cfg.Config
cfg.Generate.Shared = cfg.Config
cfg.Audit.Shared = cfg.Config

// Create i18n bundle
bundle, err := i18n.NewBundleWithFS(localesFS, "locales")
Expand Down
17 changes: 14 additions & 3 deletions v2/cmd/goopt-i18n-gen/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ import (

// GenerateCmd command configuration
type GenerateCmd struct {
Output string `goopt:"short:o;desc:Output Go file;required:true;descKey:app.generate_cmd.output_desc"`
Package string `goopt:"short:p;desc:Package name;default:messages;descKey:app.generate_cmd.package_desc"`
Prefix string `goopt:"desc:Optional prefix to strip from keys;descKey:app.generate_cmd.prefix_desc;default:app"`
Shared *Config `ignore:"true"`
Output string `goopt:"short:o;desc:Output Go file;required:true;descKey:app.generate_cmd.output_desc"`
Package string `goopt:"short:p;desc:Package name;default:messages;descKey:app.generate_cmd.package_desc"`
Prefix string `goopt:"desc:Optional prefix to strip from keys;descKey:app.generate_cmd.prefix_desc;default:app"`
VarName string `goopt:"desc:Variable name for generated constants;descKey:app.generate_cmd.var_name_desc;default:Keys"`
Exec goopt.CommandFunc
}

Expand All @@ -23,6 +25,7 @@ type ValidateCmd struct {

// AuditCmd command configuration
type AuditCmd struct {
Shared *Config `ignore:"true"`
Files []string `goopt:"desc:Go source files to scan (default: **/*.go);default:**/*.go;descKey:app.audit_cmd.files_desc"`
GenerateDescKeys bool `goopt:"short:d;desc:Generate descKey tags for fields that don't have them;descKey:app.audit_cmd.generate_desc_keys_desc"`
GenerateMissing bool `goopt:"short:g;desc:Generate stub entries for missing translation keys;descKey:app.audit_cmd.generate_missing_desc"`
Expand Down Expand Up @@ -50,6 +53,7 @@ type AddCmd struct {

// ExtractCmd handles the extract command configuration
type ExtractCmd struct {
Shared *Config `ignore:"true"`
Files []string `goopt:"short:s;desc:Go files to scan;default:**/*.go;descKey:app.extract_cmd.files_desc"`
MatchOnly string `goopt:"short:M;desc:Regex to match strings for inclusion;descKey:app.extract_cmd.match_only_desc"`
SkipMatch string `goopt:"short:S;desc:Regex to match strings for exclusion;descKey:app.extract_cmd.skip_match_desc"`
Expand Down Expand Up @@ -87,6 +91,7 @@ type GenerateLocalesCmd struct {

// AppConfig main application configuration
type AppConfig struct {
Config *Config
Input []string `goopt:"short:i;desc:Input JSON files (supports wildcards);required:true;descKey:app.app_config.input_desc"`
Verbose bool `goopt:"short:v;desc:Enable verbose output;descKey:app.app_config.verbose_desc"`
Language string `goopt:"short:l;desc:Language for output (en, de, fr);descKey:app.app_config.language_desc"`
Expand All @@ -101,3 +106,9 @@ type AppConfig struct {
GenerateLocales GenerateLocalesCmd `goopt:"kind:command;name:generate-locales;desc:Generate Go packages from locale JSON files;descKey:app.app_config.generate_locales_desc"`
TR i18n.Translator `ignore:"true"` // Translator for messages
}

type Config struct {
KeyPrefix string `goopt:"desc:Prefix for generated descKeys;default:app"`
VarName string `goopt:"desc:Variable name for generated constants;default:Keys"`
Package string `goopt:"desc:Package name for imports;default:messages"`
}
2 changes: 1 addition & 1 deletion v2/cmd/goopt-i18n-gen/translations/extract.go
Original file line number Diff line number Diff line change
Expand Up @@ -458,7 +458,7 @@ func cleanI18nComments(config *options.AppConfig) error {
}

// Resolve package path based on module context
packagePath, err := resolvePackagePath(extractCmd.Package, ".")
packagePath, err := resolvePackagePath(extractCmd.Shared.Package, ".")
if err != nil {
// If we can't resolve, use the package name as-is
packagePath = extractCmd.Package
Expand Down
Loading
Loading