From dfa07b5f3a79badd90e2b3b928cfe6107d0fcd02 Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 04:17:45 +0200 Subject: [PATCH 1/9] feat(i18n): implement localization using gettext files - Recipe to extract new translations from the Go code: `make i18n_extract` - Embedded `MO` files - Detect language from environment variables - Some strings were pluralized --- Makefile | 6 +- args.go | 15 ++-- args_test.go | 8 +- cobra.go | 3 +- command.go | 57 ++++++++------ command_test.go | 19 ++++- completions.go | 19 +++-- flag_groups.go | 3 +- go.mod | 4 +- go.sum | 27 +++++++ locales/README.md | 38 ++++++++++ locales/default.pot | 163 +++++++++++++++++++++++++++++++++++++++ locales/default/en.mo | Bin 0 -> 3185 bytes locales/default/en.po | 172 ++++++++++++++++++++++++++++++++++++++++++ locales/default/fr.mo | Bin 0 -> 3415 bytes locales/default/fr.po | 172 ++++++++++++++++++++++++++++++++++++++++++ localizer.go | 138 +++++++++++++++++++++++++++++++++ localizer_test.go | 161 +++++++++++++++++++++++++++++++++++++++ 18 files changed, 955 insertions(+), 50 deletions(-) create mode 100644 locales/README.md create mode 100644 locales/default.pot create mode 100644 locales/default/en.mo create mode 100644 locales/default/en.po create mode 100644 locales/default/fr.mo create mode 100644 locales/default/fr.po create mode 100644 localizer.go create mode 100644 localizer_test.go diff --git a/Makefile b/Makefile index 0da8d7aa..53262b6e 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,7 @@ lint: test: install_deps $(info ******************** running tests ********************) - go test -v ./... + LANGUAGE="en" go test -v ./... richtest: install_deps $(info ******************** running tests with kyoh86/richgo ********************) @@ -33,3 +33,7 @@ install_deps: clean: rm -rf $(BIN) + +i18n_extract: + $(info ******************** extracting translation files ********************) + xgotext -v -in . -out locales diff --git a/args.go b/args.go index ed1e70ce..6170a58e 100644 --- a/args.go +++ b/args.go @@ -16,6 +16,7 @@ package cobra import ( "fmt" + "github.com/leonelquinteros/gotext" "strings" ) @@ -33,7 +34,7 @@ func legacyArgs(cmd *Command, args []string) error { // root command with subcommands, do subcommand checking. if !cmd.HasParent() && len(args) > 0 { - return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0])) + return fmt.Errorf(gotext.Get("LegacyArgsValidationError"), args[0], cmd.CommandPath(), cmd.findSuggestions(args[0])) } return nil } @@ -41,7 +42,7 @@ func legacyArgs(cmd *Command, args []string) error { // NoArgs returns an error if any args are included. func NoArgs(cmd *Command, args []string) error { if len(args) > 0 { - return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath()) + return fmt.Errorf(gotext.Get("NoArgsValidationError"), args[0], cmd.CommandPath()) } return nil } @@ -58,7 +59,7 @@ func OnlyValidArgs(cmd *Command, args []string) error { } for _, v := range args { if !stringInSlice(v, validArgs) { - return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.findSuggestions(args[0])) + return fmt.Errorf(gotext.Get("OnlyValidArgsValidationError"), v, cmd.CommandPath(), cmd.findSuggestions(args[0])) } } } @@ -74,7 +75,7 @@ func ArbitraryArgs(cmd *Command, args []string) error { func MinimumNArgs(n int) PositionalArgs { return func(cmd *Command, args []string) error { if len(args) < n { - return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args)) + return fmt.Errorf(gotext.GetN("MinimumNArgsValidationError", "MinimumNArgsValidationErrorPlural", n), n, len(args)) } return nil } @@ -84,7 +85,7 @@ func MinimumNArgs(n int) PositionalArgs { func MaximumNArgs(n int) PositionalArgs { return func(cmd *Command, args []string) error { if len(args) > n { - return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args)) + return fmt.Errorf(gotext.GetN("MaximumNArgsValidationError", "MaximumNArgsValidationErrorPlural", n), n, len(args)) } return nil } @@ -94,7 +95,7 @@ func MaximumNArgs(n int) PositionalArgs { func ExactArgs(n int) PositionalArgs { return func(cmd *Command, args []string) error { if len(args) != n { - return fmt.Errorf("accepts %d arg(s), received %d", n, len(args)) + return fmt.Errorf(gotext.GetN("ExactArgsValidationError", "ExactArgsValidationErrorPlural", n), n, len(args)) } return nil } @@ -104,7 +105,7 @@ func ExactArgs(n int) PositionalArgs { func RangeArgs(min int, max int) PositionalArgs { return func(cmd *Command, args []string) error { if len(args) < min || len(args) > max { - return fmt.Errorf("accepts between %d and %d arg(s), received %d", min, max, len(args)) + return fmt.Errorf(gotext.GetN("RangeArgsValidationError", "RangeArgsValidationErrorPlural", max), min, max, len(args)) } return nil } diff --git a/args_test.go b/args_test.go index 90d174cc..c156b475 100644 --- a/args_test.go +++ b/args_test.go @@ -68,7 +68,7 @@ func minimumNArgsWithLessArgs(err error, t *testing.T) { t.Fatal("Expected an error") } got := err.Error() - expected := "requires at least 2 arg(s), only received 1" + expected := "requires at least 2 args, only received 1" if got != expected { t.Fatalf("Expected %q, got %q", expected, got) } @@ -79,7 +79,7 @@ func maximumNArgsWithMoreArgs(err error, t *testing.T) { t.Fatal("Expected an error") } got := err.Error() - expected := "accepts at most 2 arg(s), received 3" + expected := "accepts at most 2 args, received 3" if got != expected { t.Fatalf("Expected %q, got %q", expected, got) } @@ -90,7 +90,7 @@ func exactArgsWithInvalidCount(err error, t *testing.T) { t.Fatal("Expected an error") } got := err.Error() - expected := "accepts 2 arg(s), received 3" + expected := "accepts 2 args, received 3" if got != expected { t.Fatalf("Expected %q, got %q", expected, got) } @@ -101,7 +101,7 @@ func rangeArgsWithInvalidCount(err error, t *testing.T) { t.Fatal("Expected an error") } got := err.Error() - expected := "accepts between 2 and 4 arg(s), received 1" + expected := "accepts between 2 and 4 args, received 1" if got != expected { t.Fatalf("Expected %q, got %q", expected, got) } diff --git a/cobra.go b/cobra.go index d9cd2414..38581704 100644 --- a/cobra.go +++ b/cobra.go @@ -19,6 +19,7 @@ package cobra import ( "fmt" + "github.com/leonelquinteros/gotext" "io" "os" "reflect" @@ -234,7 +235,7 @@ func stringInSlice(a string, list []string) bool { // CheckErr prints the msg with the prefix 'Error:' and exits with error code 1. If the msg is nil, it does nothing. func CheckErr(msg interface{}) { if msg != nil { - fmt.Fprintln(os.Stderr, "Error:", msg) + fmt.Fprintln(os.Stderr, gotext.Get("Error")+":", msg) os.Exit(1) } } diff --git a/command.go b/command.go index 6904bfba..1c3e048a 100644 --- a/command.go +++ b/command.go @@ -21,8 +21,10 @@ import ( "context" "errors" "fmt" + "github.com/leonelquinteros/gotext" "io" "os" + "path/filepath" "sort" "strings" @@ -46,6 +48,12 @@ type Group struct { Title string } +// CommandUsageTemplateData is the data passed to the template of command usage +type CommandUsageTemplateData struct { + *Command + I18n *i18nCommandGlossary +} + // Command is just that, a command for your application. // E.g. 'go run ...' - 'run' is the command. Cobra requires // you to define the usage and description as part of your command @@ -438,7 +446,11 @@ func (c *Command) UsageFunc() (f func(*Command) error) { return func(c *Command) error { c.mergePersistentFlags() fn := c.getUsageTemplateFunc() - err := fn(c.OutOrStderr(), c) + data := CommandUsageTemplateData{ + Command: c, + I18n: getCommandGlossary(), + } + err := fn(c.OutOrStderr(), data) if err != nil { c.PrintErrln(err) } @@ -774,7 +786,7 @@ func (c *Command) findSuggestions(arg string) string { } var sb strings.Builder if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 { - sb.WriteString("\n\nDid you mean this?\n") + sb.WriteString("\n\n" + gotext.Get("DidYouMeanThis") + "\n") for _, s := range suggestions { _, _ = fmt.Fprintf(&sb, "\t%v\n", s) } @@ -895,7 +907,7 @@ func (c *Command) execute(a []string) (err error) { } if len(c.Deprecated) > 0 { - c.Printf("Command %q is deprecated, %s\n", c.Name(), c.Deprecated) + c.Printf(gotext.Get("CommandDeprecatedWarning")+"\n", c.Name(), c.Deprecated) } // initialize help and version flag at the last point possible to allow for user @@ -1118,7 +1130,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) { } if !c.SilenceErrors { c.PrintErrln(c.ErrPrefix(), err.Error()) - c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath()) + c.PrintErrf(gotext.Get("RunHelpTip")+"\n", c.CommandPath()) } return c, err } @@ -1184,7 +1196,7 @@ func (c *Command) ValidateRequiredFlags() error { }) if len(missingFlagNames) > 0 { - return fmt.Errorf(`required flag(s) "%s" not set`, strings.Join(missingFlagNames, `", "`)) + return fmt.Errorf(gotext.GetN("FlagNotSetError", "FlagNotSetErrorPlural", len(missingFlagNames)), strings.Join(missingFlagNames, `", "`)) } return nil } @@ -1208,10 +1220,10 @@ func (c *Command) checkCommandGroups() { func (c *Command) InitDefaultHelpFlag() { c.mergePersistentFlags() if c.Flags().Lookup(helpFlagName) == nil { - usage := "help for " + usage := gotext.Get("HelpFor") + " " name := c.DisplayName() if name == "" { - usage += "this command" + usage += gotext.Get("ThisCommand") } else { usage += name } @@ -1231,9 +1243,9 @@ func (c *Command) InitDefaultVersionFlag() { c.mergePersistentFlags() if c.Flags().Lookup("version") == nil { - usage := "version for " + usage := gotext.Get("VersionFor") + " " if c.Name() == "" { - usage += "this command" + usage += gotext.Get("ThisCommand") } else { usage += c.DisplayName() } @@ -1256,10 +1268,9 @@ func (c *Command) InitDefaultHelpCmd() { if c.helpCommand == nil { c.helpCommand = &Command{ - Use: "help [command]", - Short: "Help about any command", - Long: `Help provides help for any command in the application. -Simply type ` + c.DisplayName() + ` help [path to command] for full details.`, + Use: fmt.Sprintf("help [%s]", gotext.Get("command")), + Short: gotext.Get("CommandHelpShort"), + Long: fmt.Sprintf(gotext.Get("CommandHelpLong"), c.DisplayName()+fmt.Sprintf(" help [%s]", gotext.Get("command"))), ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) { var completions []string cmd, _, e := c.Root().Find(args) @@ -1282,7 +1293,7 @@ Simply type ` + c.DisplayName() + ` help [path to command] for full details.`, Run: func(c *Command, args []string) { cmd, _, e := c.Root().Find(args) if cmd == nil || e != nil { - c.Printf("Unknown help topic %#q\n", args) + c.Printf(gotext.Get("CommandHelpUnknownTopicError")+"\n", args) CheckErr(c.Root().Usage()) } else { cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown @@ -1923,35 +1934,35 @@ type tmplFunc struct { fn func(io.Writer, interface{}) error } -var defaultUsageTemplate = `Usage:{{if .Runnable}} +var defaultUsageTemplate = `{{.I18n.SectionUsage}}:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} -Aliases: +{{.I18n.SectionAliases}}: {{.NameAndAliases}}{{end}}{{if .HasExample}} -Examples: +{{.I18n.SectionExamples}}: {{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} -Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} +{{.I18n.SectionAvailableCommands}}:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} {{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} -Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} +{{.I18n.SectionAdditionalCommands}}:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} -Flags: +{{.I18n.SectionFlags}}: {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} -Global Flags: +{{.I18n.SectionGlobalFlags}}: {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} -Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} +{{.I18n.SectionAdditionalHelpTopics}}:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} -Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} +{{.I18n.Use}} "{{.CommandPath}} [command] --help" {{.I18n.ForInfoAboutCommand}}.{{end}} ` // defaultUsageFunc is equivalent to executing defaultUsageTemplate. The two should be changed in sync. diff --git a/command_test.go b/command_test.go index 0b0d6c66..b50afda5 100644 --- a/command_test.go +++ b/command_test.go @@ -850,6 +850,21 @@ func TestPersistentFlagsOnChild(t *testing.T) { } } +func TestRequiredFlag(t *testing.T) { + c := &Command{Use: "c", Run: emptyRun} + c.Flags().String("foo1", "", "") + assertNoErr(t, c.MarkFlagRequired("foo1")) + + expected := fmt.Sprintf("required flag %q is not set", "foo1") + + _, err := executeCommand(c) + got := err.Error() + + if got != expected { + t.Errorf("Expected error: %q, got: %q", expected, got) + } +} + func TestRequiredFlags(t *testing.T) { c := &Command{Use: "c", Run: emptyRun} c.Flags().String("foo1", "", "") @@ -858,7 +873,7 @@ func TestRequiredFlags(t *testing.T) { assertNoErr(t, c.MarkFlagRequired("foo2")) c.Flags().String("bar", "", "") - expected := fmt.Sprintf("required flag(s) %q, %q not set", "foo1", "foo2") + expected := fmt.Sprintf("required flags %q, %q are not set", "foo1", "foo2") _, err := executeCommand(c) got := err.Error() @@ -885,7 +900,7 @@ func TestPersistentRequiredFlags(t *testing.T) { parent.AddCommand(child) - expected := fmt.Sprintf("required flag(s) %q, %q, %q, %q not set", "bar1", "bar2", "foo1", "foo2") + expected := fmt.Sprintf("required flags %q, %q, %q, %q are not set", "bar1", "bar2", "foo1", "foo2") _, err := executeCommand(parent, "child") if err.Error() != expected { diff --git a/completions.go b/completions.go index cd899c73..f1e7b2b6 100644 --- a/completions.go +++ b/completions.go @@ -16,6 +16,7 @@ package cobra import ( "fmt" + "github.com/leonelquinteros/gotext" "os" "regexp" "strconv" @@ -50,7 +51,7 @@ type flagCompError struct { } func (e *flagCompError) Error() string { - return "Subcommand '" + e.subCommand + "' does not support flag '" + e.flagName + "'" + return fmt.Sprintf(gotext.Get("CompletionSubcommandUnsupportedFlagError"), e.subCommand, e.flagName) } const ( @@ -99,7 +100,6 @@ const ( // Constants for the completion command compCmdName = "completion" compCmdNoDescFlagName = "no-descriptions" - compCmdNoDescFlagDesc = "disable completion descriptions" compCmdNoDescFlagDefault = false ) @@ -213,9 +213,8 @@ func (c *Command) initCompleteCmd(args []string) { Hidden: true, DisableFlagParsing: true, Args: MinimumNArgs(1), - Short: "Request shell completion choices for the specified command-line", - Long: fmt.Sprintf("%[2]s is a special command that is used by the shell completion logic\n%[1]s", - "to request completion choices for the specified command-line.", ShellCompRequestCmd), + Short: gotext.Get("CompletionCommandShellShort"), + Long: fmt.Sprintf(gotext.Get("CompletionCommandShellLong"), ShellCompRequestCmd), Run: func(cmd *Command, args []string) { finalCmd, completions, directive, err := cmd.getCompletions(args) if err != nil { @@ -267,7 +266,7 @@ func (c *Command) initCompleteCmd(args []string) { // Print some helpful info to stderr for the user to understand. // Output from stderr must be ignored by the completion script. - fmt.Fprintf(finalCmd.ErrOrStderr(), "Completion ended with directive: %s\n", directive.string()) + fmt.Fprintf(finalCmd.ErrOrStderr(), fmt.Sprintf(gotext.Get("CompletionCommandShellDirectiveTip"), directive.string())+"\n") }, } c.AddCommand(completeCmd) @@ -773,7 +772,7 @@ You will need to start a new shell for this setup to take effect. }, } if haveNoDescFlag { - bash.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) + bash.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, gotext.Get("CompletionSubcommandNoDescFlagDesc")) } zsh := &Command{ @@ -812,7 +811,7 @@ You will need to start a new shell for this setup to take effect. }, } if haveNoDescFlag { - zsh.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) + zsh.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, gotext.Get("CompletionSubcommandNoDescFlagDesc")) } fish := &Command{ @@ -837,7 +836,7 @@ You will need to start a new shell for this setup to take effect. }, } if haveNoDescFlag { - fish.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) + fish.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, gotext.Get("CompletionSubcommandNoDescFlagDesc")) } powershell := &Command{ @@ -863,7 +862,7 @@ to your powershell profile. }, } if haveNoDescFlag { - powershell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, compCmdNoDescFlagDesc) + powershell.Flags().BoolVar(&noDesc, compCmdNoDescFlagName, compCmdNoDescFlagDefault, gotext.Get("CompletionSubcommandNoDescFlagDesc")) } completionCmd.AddCommand(bash, zsh, fish, powershell) diff --git a/flag_groups.go b/flag_groups.go index 560612fd..4eea0e1e 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -16,6 +16,7 @@ package cobra import ( "fmt" + "github.com/leonelquinteros/gotext" "sort" "strings" @@ -201,7 +202,7 @@ func validateExclusiveFlagGroups(data map[string]map[string]bool) error { // Sort values, so they can be tested/scripted against consistently. sort.Strings(set) - return fmt.Errorf("if any flags in the group [%v] are set none of the others can be; %v were all set", flagList, set) + return fmt.Errorf(gotext.Get("ExclusiveFlagsValidationError"), flagList, set) } return nil } diff --git a/go.mod b/go.mod index 3959690c..64443192 100644 --- a/go.mod +++ b/go.mod @@ -1,10 +1,12 @@ module github.com/spf13/cobra -go 1.15 +go 1.16 require ( github.com/cpuguy83/go-md2man/v2 v2.0.6 github.com/inconshreveable/mousetrap v1.1.0 + github.com/leonelquinteros/gotext v1.5.3-0.20231003122255-12a99145a351 github.com/spf13/pflag v1.0.5 + golang.org/x/text v0.4.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/go.sum b/go.sum index 1be80282..84bef12f 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,37 @@ github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/leonelquinteros/gotext v1.5.3-0.20231003122255-12a99145a351 h1:Rk+RkO4xEZMkEok69CbeA6cgXKyVCsgF3qGGGR46pd8= +github.com/leonelquinteros/gotext v1.5.3-0.20231003122255-12a99145a351/go.mod h1:qQRISjoonXYFdRGrTG1LARQ38Gpibad0IPeB4hpvyyM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0 h1:BrVqGRd7+k1DiOgtnFvAkoQEWQvBc25ouMJM6429SFg= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/locales/README.md b/locales/README.md new file mode 100644 index 00000000..90da71d5 --- /dev/null +++ b/locales/README.md @@ -0,0 +1,38 @@ +# Locales + +Localization uses embedded _gettext_ files, defaulting to English +when locale cannot be guessed from environment variables. + + +## Development Flow + +1. Add calls to `gotext.Get(…)` somewhere in the codebase +2. Run `make i18n_extract` +3. Update the `PO` files with some software like [Poedit] +4. Make sure your software has also updated the `MO` files + +[Poedit]: https://poedit.net/ + +## Overview + +### POT files + +The `*.pot` file(s) are automatically generated by the following command : + + make i18n_extract + +They are named `.pot`, and when the domain is not specified, it is `default`. + +### PO & MO files + +The actual translation files, in _gettext_ format (`*.po` and `*.mo`), are in the directory `/`. +They are named `.po` and `.mo`. + +The supported `` formats are : +- [ISO 639-3](https://fr.wikipedia.org/wiki/ISO_639-3) _(eg: eng, fra, …)_ +- [BCP 47](https://fr.wiktionary.org/wiki/Wiktionnaire:BCP_47/language-2) _(eg: en, fr, …)_ + +The `*.po` files are plain text, and are the authoritative sources of translations. + +The `*.mo` files are the ones actually packaged in cobra as embedded files, because they are smaller. + diff --git a/locales/default.pot b/locales/default.pot new file mode 100644 index 00000000..6644be0b --- /dev/null +++ b/locales/default.pot @@ -0,0 +1,163 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: \n" +"X-Generator: xgotext\n" + +#: command.go:891 +msgid "CommandDeprecatedWarning" +msgstr "" + +#: command.go:1249 +msgid "CommandHelpLong" +msgstr "" + +#: command.go:1248 +msgid "CommandHelpShort" +msgstr "" + +#: command.go:1272 +msgid "CommandHelpUnknownTopicError" +msgstr "" + +#: completions.go:250 +msgid "CompletionCommandShellDirectiveTip" +msgstr "" + +#: completions.go:203 +msgid "CompletionCommandShellLong" +msgstr "" + +#: completions.go:202 +msgid "CompletionCommandShellShort" +msgstr "" + +#: completions.go:745 +#: completions.go:784 +#: completions.go:809 +#: completions.go:835 +msgid "CompletionSubcommandNoDescFlagDesc" +msgstr "" + +#: completions.go:52 +msgid "CompletionSubcommandUnsupportedFlagError" +msgstr "" + +#: command.go:770 +msgid "DidYouMeanThis" +msgstr "" + +#: cobra.go:234 +msgid "Error" +msgstr "" + +#: args.go:98 +msgid "ExactArgsValidationError" +msgid_plural "ExactArgsValidationErrorPlural" +msgstr[0] "" +msgstr[1] "" + +#: flag_groups.go:205 +msgid "ExclusiveFlagsValidationError" +msgstr "" + +#: command.go:1176 +msgid "FlagNotSetError" +msgid_plural "FlagNotSetErrorPlural" +msgstr[0] "" +msgstr[1] "" + +#: localizer.go:66 +msgid "ForInfoAboutCommand" +msgstr "" + +#: command.go:1200 +msgid "HelpFor" +msgstr "" + +#: args.go:37 +msgid "LegacyArgsValidationError" +msgstr "" + +#: args.go:88 +msgid "MaximumNArgsValidationError" +msgid_plural "MaximumNArgsValidationErrorPlural" +msgstr[0] "" +msgstr[1] "" + +#: args.go:78 +msgid "MinimumNArgsValidationError" +msgid_plural "MinimumNArgsValidationErrorPlural" +msgstr[0] "" +msgstr[1] "" + +#: args.go:45 +msgid "NoArgsValidationError" +msgstr "" + +#: args.go:62 +msgid "OnlyValidArgsValidationError" +msgstr "" + +#: args.go:108 +msgid "RangeArgsValidationError" +msgid_plural "RangeArgsValidationErrorPlural" +msgstr[0] "" +msgstr[1] "" + +#: command.go:1110 +msgid "RunHelpTip" +msgstr "" + +#: localizer.go:61 +msgid "SectionAdditionalCommands" +msgstr "" + +#: localizer.go:64 +msgid "SectionAdditionalHelpTopics" +msgstr "" + +#: localizer.go:58 +msgid "SectionAliases" +msgstr "" + +#: localizer.go:60 +msgid "SectionAvailableCommands" +msgstr "" + +#: localizer.go:59 +msgid "SectionExamples" +msgstr "" + +#: localizer.go:62 +msgid "SectionFlags" +msgstr "" + +#: localizer.go:63 +msgid "SectionGlobalFlags" +msgstr "" + +#: localizer.go:57 +msgid "SectionUsage" +msgstr "" + +#: command.go:1202 +#: command.go:1224 +msgid "ThisCommand" +msgstr "" + +#: localizer.go:65 +msgid "Use" +msgstr "" + +#: command.go:1222 +msgid "VersionFor" +msgstr "" + +#: command.go:1247 +#: command.go:1249 +msgid "command" +msgstr "" \ No newline at end of file diff --git a/locales/default/en.mo b/locales/default/en.mo new file mode 100644 index 0000000000000000000000000000000000000000..b7ab9e04ed1205546edb24722622907e60988406 GIT binary patch literal 3185 zcmb7`NpBoQ6vr!&unbE`0)#D4aWaaD+=ESo2sTz^c}a|H$FV&L0dlD6u9+!PQ`PRO z9^2vo%7vUbAaOvz4NiOlPKXNz)A4$f)}9V8SK|V^5avG^nD9f zz~4c7_ZLY19C|3Xe+r~{MnH;Z7JMA6gU7)}u}{Iru)hN$1ojz7dOruruW!JA!0$k^ z|1}P8fIooGg2x`oISih`{u1~J7=o9;ZSWQF?4yjG2WLRCZ$Y@s?tmA;yWlnOCy?Sl z0VmFa*FdOXU6A7Y7`y_01zrcYL5hC@L6CoSkj@j3>hudpdEN%e-+w`h>nM^zew+kJ z|9O!7xd7r1d#l*b75fE{@^ce}3U&)5JqkoCWp7AdxlZCAa4pL{ozY$DpVPD%Sk!(< zZMGB<(xS}ux%Vy5*pK8l+b_0bdfC|}eY$M8|F zQarRczL)V)-?M4k=?D|e$T*Rq@G`m~5~ED36kL%yUa&j&t8JV3!3%YB+t>}WW@8o3 zCW%c*7;EX3HHCrIw$yq?!H8E~xu)U+u9UX+dEGNx$X3%vn0vouXQT_~v}h3rH21I6 zjZ5Pge#?mNVkS12tSTJF|?MxZ9{UCioQ8n~=&L+BkxbthVqQ74t9t2@cA%f!J&s$iIRD_c(N zhX^2;kAgxv&MV7TgB96TbT^p64js#!CYoTQT%`x#?evwNPe)hi#EK=_l)JbHT;_J+jr5Nib_d8(|8J^NS7T zD|rC|#L#i$+{9VKyUEulc>d4HVZ%>P^08MZD(?oDrIAVQ@3NIB5Wi3xt&LU6fx)Zm zTsa=~Ce8@2x~$82oY=04q~jPCF>l(03$w+Gv~gv)Z%Z!XSSySwB&t?fRVdLG_XrSL zvM0?{YiReVs;lLC1^qk?>SD#j$_;sH4_-a9J}^0u&`3snL-{t3%CR+0p27^=7u>}% zR6_UM^KIctk~+L=z$f_X5J+~T7+GA~mI^E0@LFI zdeCz%F(Z1aEXMARR3CH|8u#2i(08UxmwpveCDRV($-ZGVq~DOzWWE@x^TCDR_v)@7 zG3?P|T^O5 zae)&DBvdNI1%V*q$RE%{5dQ!N4j>Lls0VIHJtFb_d1kVkWK&T~v!Aj3^Y7)KJ^RPL z-JdhGLwKIT^U^NHPJr*+h99(Jw=?!QI11hmz6L%3UI0n{5;z2Y0`3QY0G|YZ&d2`* z52L?lH)HpK6>twY4!!`M1`mKAf@Sb3_z-vv+zb8!J^}s)j)OOH9*2@g&|d+`kB>pp z_YGJAe+OyZb&&kobw_6Z07&tSfD}&^ya%j<`@lxtPr{F2Reg=|XUx7Ek zZ^47$ml#aJYv2*^#GRR)_ras+cfqUR_h1ct^{&jXE8ubTe+0>|>mXEmV^hFh7(ENl zf)~ImAnE%DgeV(=lTU*q;3D`sNOE6;l$U?=`TYp;J@j7$p98-DDc+yJd%<5pq>B9o zQeOWC;VRpONs6}w(!TElN$wy>`*{Q;xsxE4u!+1s3!;?#7}+V>V|an0`H=LQy*WfV;+7)ew|)>OQMNMUQh>J78`+(Oz2Gw)~3q;%m_ zBwEBi%{`I2c4-{LZ`mZd%*#pDe9NRWQs~;Uax5FHc14&>*p^!qk!lKx;pP+bQEElR zs@*V39fBkM+ZNN{tVtGRk}2EL&vi^0drmKziH1p&JaLS60OM?0wnVr($gvsGRqeDr zdy8|q%^9V4UcK{NzIxUSvO1@uRiC~=ljntQ$y=GoO`T6Q<$`Kwfy&6}iDpv~MU*GY zZJ+T=s4#nTkrGb!x*ee+(TL<$9Vi4U;tfH+uZ4j#k!gr%(@@=smSj{(#n#oGWQ)=| zxJWw~X4T5(t+|K*f^*FvpB>|+`MKb{>?o2ACUFokG%Z{b)U41h5{a>XM7L5{32IWb z`}$*i$(Cl$%~X2|M=Qrlr;SdehK|)3`V!eqj>eHt`V^C+uh*u6lRY`IwIpp& z)uCzP78v6v8!9Pf8xSCdwj1L*_66_8pFhR3*YU%ezsyIEoGQH;oRM1E%-wk-u|NEH zVWEH+BU}%wwm-mp zCB|fr)(lkOaQSM8p#DN!e!Zkb{-4MBf*=+)yKyu(6aLh z=vU5NOsNovEKO9T+{Wr*u~j>HO6k2|nPaXIGBNoQ3|iE<2qIFmZc z9({>qwr!bD_$cWnQs__##>@LzwrJ8w$;&|p3w>&NC_}|xp(|XbmWeQwae_K)a$#E| zye$+Gj$Pqau78(ee2a)1C__2O%sT#dx$`a@)d`oyMPDrH7b|x-$)F0>*YZl1h_UeA z%9T_($%o2r2o+6-fTE*Q;ILJa_3O%oT`ny%^PA)y^I69+q%BeScG$K*1AM0v-Z)Tf zBjGrWc^&6Rt-OK~EpiA&Uj6^brVmgS3ql z)7_(VhraEDqJ@O@b%VqIKO){PKG?OYb7IwBNgW;p_%9T1xtTWZo=Wk@3blv(Tgxu$ z2iJ>5-(NrI(Fd$gY*M+{Xn(ZvFGOY^t-1Vo<>c813%at;R45aik0N8N?r$7v8BQpD Lg8UV+Q6cO<3Owi` literal 0 HcmV?d00001 diff --git a/locales/default/fr.po b/locales/default/fr.po new file mode 100644 index 00000000..9bb83c22 --- /dev/null +++ b/locales/default/fr.po @@ -0,0 +1,172 @@ +msgid "" +msgstr "" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: \n" +"Language-Team: \n" +"Language: fr\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"X-Generator: Poedit 3.0.1\n" + +#: command.go:891 +msgid "CommandDeprecatedWarning" +msgstr "La commande %q est dépréciée, %s" + +#: command.go:1249 +msgid "CommandHelpLong" +msgstr "" +"Help fournit de l'aide pour n'importe quelle commande de l'application.\n" +"Tapez '%s' pour obtenir une aide détaillée." + +#: command.go:1248 +msgid "CommandHelpShort" +msgstr "Obtenir de l'aide au sujet d'une commande" + +#: command.go:1272 +msgid "CommandHelpUnknownTopicError" +msgstr "Sujet d'aide %#q inconnu" + +#: completions.go:250 +msgid "CompletionCommandShellDirectiveTip" +msgstr "Auto-complétion achevée par la directive : %s" + +#: completions.go:203 +msgid "CompletionCommandShellLong" +msgstr "" +"%s est une commande spéciale utilisée par l'auto-complétion de la console\n" +"pour récupérer les différents choix possibles pour une certaine commande." + +#: completions.go:202 +msgid "CompletionCommandShellShort" +msgstr "" +"Obtenir les différentes possibilités d'auto-complétion pour une certaine " +"commande" + +#: completions.go:745 completions.go:784 completions.go:809 completions.go:835 +msgid "CompletionSubcommandNoDescFlagDesc" +msgstr "désactiver les desriptions" + +#: completions.go:52 +msgid "CompletionSubcommandUnsupportedFlagError" +msgstr "la sous-commande '%s' ne comprend pas l'option '%s'" + +#: command.go:770 +msgid "DidYouMeanThis" +msgstr "Vouliez-vous dire ceci ?" + +#: cobra.go:234 +msgid "Error" +msgstr "Erreur" + +#: args.go:98 +msgid "ExactArgsValidationError" +msgid_plural "ExactArgsValidationErrorPlural" +msgstr[0] "accepte %d arg, mais en a reçu %d" +msgstr[1] "accepte %d args, mais en a reçu %d" + +#: flag_groups.go:205 +msgid "ExclusiveFlagsValidationError" +msgstr "les options [%v] sont exclusives, mais les options %v ont été fournies" + +#: command.go:1176 +msgid "FlagNotSetError" +msgid_plural "FlagNotSetErrorPlural" +msgstr[0] "l'option requise \"%s\" n'est pas présente" +msgstr[1] "les options requises \"%s\" ne sont pas présentes" + +#: localizer.go:66 +msgid "ForInfoAboutCommand" +msgstr "pour plus d'information au sujet d'une commande" + +#: command.go:1200 +msgid "HelpFor" +msgstr "aide pour" + +#: args.go:37 +msgid "LegacyArgsValidationError" +msgstr "commande %q inconnue pour %q%s" + +#: args.go:88 +msgid "MaximumNArgsValidationError" +msgid_plural "MaximumNArgsValidationErrorPlural" +msgstr[0] "accepte au plus %d arg, mais en a reçu %d" +msgstr[1] "accepte au plus %d args, mais en a reçu %d" + +#: args.go:78 +msgid "MinimumNArgsValidationError" +msgid_plural "MinimumNArgsValidationErrorPlural" +msgstr[0] "requiert au moins %d arg, mais en a reçu %d" +msgstr[1] "requiert au moins %d args, mais en a reçu %d" + +#: args.go:45 +msgid "NoArgsValidationError" +msgstr "commande %q inconnue pour %q" + +#: args.go:62 +msgid "OnlyValidArgsValidationError" +msgstr "argument %q invalide pour %q%s" + +#: args.go:108 +msgid "RangeArgsValidationError" +msgid_plural "RangeArgsValidationErrorPlural" +msgstr[0] "accepte entre %d et %d arg, mais en a reçu %d" +msgstr[1] "accepte entre %d et %d args, mais en a reçu %d" + +#: command.go:1110 +msgid "RunHelpTip" +msgstr "Essayez '%v --help' pour obtenir de l'aide." + +#: localizer.go:61 +msgid "SectionAdditionalCommands" +msgstr "Commandes Connexes" + +#: localizer.go:64 +msgid "SectionAdditionalHelpTopics" +msgstr "Autres Sujets" + +#: localizer.go:58 +msgid "SectionAliases" +msgstr "Alias" + +#: localizer.go:60 +msgid "SectionAvailableCommands" +msgstr "Commandes Disponibles" + +#: localizer.go:59 +msgid "SectionExamples" +msgstr "Exemples" + +#: localizer.go:62 +msgid "SectionFlags" +msgstr "Options" + +#: localizer.go:63 +msgid "SectionGlobalFlags" +msgstr "Options Globales" + +#: localizer.go:57 +msgid "SectionUsage" +msgstr "Usage" + +#: command.go:1202 command.go:1224 +msgid "ThisCommand" +msgstr "cette commande" + +#: localizer.go:65 +msgid "Use" +msgstr "Utiliser" + +#: command.go:1222 +msgid "VersionFor" +msgstr "version pour" + +#: command.go:1247 command.go:1249 +msgid "command" +msgstr "commande" + +#~ msgid "PathToCommand" +#~ msgstr "command" diff --git a/localizer.go b/localizer.go new file mode 100644 index 00000000..90a94a83 --- /dev/null +++ b/localizer.go @@ -0,0 +1,138 @@ +package cobra + +import ( + "embed" + "fmt" + "os" + + "github.com/leonelquinteros/gotext" + "golang.org/x/text/language" +) + +var defaultLanguage = language.English + +// envVariablesHoldingLocale is sorted by decreasing priority. +// These environment variables are expected to hold a parsable locale (fr_FR, es, en-US, …) +var envVariablesHoldingLocale = []string{ + "LANGUAGE", + "LC_ALL", + "LC_MESSAGES", + "LANG", +} + +// availableLocalizationDomains holds all the domains used in localization. +// Each domain MUST have its own locales/.pot file and locales// dir. +// Therefore, please only use short, ^[a-z]+$ strings as domains. +var availableLocalizationDomains = []string{ + "default", +} + +// localeFS points to an embedded filesystem of binary gettext translation files. +// For performance and smaller builds, only the binary MO files are included. +// Their sibling PO files should still be considered their authoritative source. +// +//go:embed locales/*/*.mo +var localeFS embed.FS + +// i18nCommandGlossary wraps the translated strings passed to the command usage template. +// This is used in CommandUsageTemplateData. +type i18nCommandGlossary struct { + SectionUsage string + SectionAliases string + SectionExamples string + SectionAvailableCommands string + SectionAdditionalCommands string + SectionFlags string + SectionGlobalFlags string + SectionAdditionalHelpTopics string + Use string + ForInfoAboutCommand string +} + +var commonCommandGlossary *i18nCommandGlossary + +func getCommandGlossary() *i18nCommandGlossary { + if commonCommandGlossary == nil { + commonCommandGlossary = &i18nCommandGlossary{ + SectionUsage: gotext.Get("SectionUsage"), + SectionAliases: gotext.Get("SectionAliases"), + SectionExamples: gotext.Get("SectionExamples"), + SectionAvailableCommands: gotext.Get("SectionAvailableCommands"), + SectionAdditionalCommands: gotext.Get("SectionAdditionalCommands"), + SectionFlags: gotext.Get("SectionFlags"), + SectionGlobalFlags: gotext.Get("SectionGlobalFlags"), + SectionAdditionalHelpTopics: gotext.Get("SectionAdditionalHelpTopics"), + Use: gotext.Get("Use"), + ForInfoAboutCommand: gotext.Get("ForInfoAboutCommand"), + } + } + return commonCommandGlossary +} + +func setupLocalization() { + for _, localeIdentifier := range detectLangs() { + locale := gotext.NewLocale("", localeIdentifier) + + allDomainsFound := true + for _, domain := range availableLocalizationDomains { + + //localeFilepath := fmt.Sprintf("locales/%s/%s.po", domain, localeIdentifier) + localeFilepath := fmt.Sprintf("locales/%s/%s.mo", domain, localeIdentifier) + localeFile, err := localeFS.ReadFile(localeFilepath) + if err != nil { + allDomainsFound = false + break + } + + //translator := gotext.NewPo() + translator := gotext.NewMo() + translator.Parse(localeFile) + + locale.AddTranslator(domain, translator) + } + + if !allDomainsFound { + continue + } + + gotext.SetStorage(locale) + break + } +} + +func detectLangs() []string { + var detectedLangs []string + + // From environment + for _, envKey := range envVariablesHoldingLocale { + lang := os.Getenv(envKey) + if lang != "" { + detectedLang := language.Make(lang) + appendLang(&detectedLangs, detectedLang) + } + } + + // Lastly, from defaults + appendLang(&detectedLangs, defaultLanguage) + + return detectedLangs +} + +func appendLang(langs *[]string, lang language.Tag) { + if lang.IsRoot() { + return + } + + langString := lang.String() + *langs = append(*langs, langString) + + langBase, confidentInBase := lang.Base() + if confidentInBase != language.No { + *langs = append(*langs, langBase.ISO3()) + *langs = append(*langs, langBase.String()) + } +} + +func init() { + setupLocalization() +} diff --git a/localizer_test.go b/localizer_test.go new file mode 100644 index 00000000..16efdc01 --- /dev/null +++ b/localizer_test.go @@ -0,0 +1,161 @@ +package cobra + +import ( + "github.com/leonelquinteros/gotext" + "os" + "testing" +) + +// resetLocalization resets to the vendor defaults +// Ideally this would be done using gotext.SetStorage(nil) +func resetLocalization() { + locale := gotext.NewLocale("/usr/local/share/locale", "en_US") + locale.AddDomain("default") + locale.SetDomain("default") + gotext.SetStorage(locale) +} + +func TestLocalization(t *testing.T) { + tests := []struct { + rule string + env map[string]string + expectedLanguage string + message string + expectedTranslation string + }{ + { + rule: "default language is english", + expectedLanguage: "en", + }, + { + rule: "section example (en)", + env: map[string]string{ + "LANGUAGE": "en", + }, + expectedLanguage: "en", + message: "SectionExamples", + expectedTranslation: "Examples", + }, + { + rule: "section example (fr)", + env: map[string]string{ + "LANGUAGE": "fr", + }, + expectedLanguage: "fr", + message: "SectionExamples", + expectedTranslation: "Exemples", + }, + { + rule: "untranslated string stays as-is", + message: "AtelophobiacCoder", + expectedTranslation: "AtelophobiacCoder", + }, + { + rule: "fr_FR falls back to fr", + env: map[string]string{ + "LANGUAGE": "fr_FR", + }, + expectedLanguage: "fr", + }, + { + rule: "fr-FR falls back to fr", + env: map[string]string{ + "LANGUAGE": "fr-FR", + }, + expectedLanguage: "fr", + }, + { + rule: "fr_FR@UTF-8 falls back to fr", + env: map[string]string{ + "LANGUAGE": "fr_FR@UTF-8", + }, + expectedLanguage: "fr", + }, + { + rule: "fr_FR.UTF-8 falls back to fr", + env: map[string]string{ + "LANGUAGE": "fr_FR.UTF-8", + }, + expectedLanguage: "fr", + }, + { + rule: "LANGUAGE > LC_ALL", + env: map[string]string{ + "LANGUAGE": "fr", + "LC_ALL": "en", + "LC_MESSAGES": "en", + "LANG": "en", + }, + expectedLanguage: "fr", + }, + { + rule: "LC_ALL > LC_MESSAGES", + env: map[string]string{ + "LC_ALL": "fr", + "LC_MESSAGES": "en", + "LANG": "en", + }, + expectedLanguage: "fr", + }, + { + rule: "LC_MESSAGES > LANG", + env: map[string]string{ + "LC_MESSAGES": "fr", + "LANG": "en", + }, + expectedLanguage: "fr", + }, + { + rule: "LANG is supported", + env: map[string]string{ + "LANG": "fr", + }, + expectedLanguage: "fr", + }, + { + rule: "Fall back to another env if a language is not supported", + env: map[string]string{ + "LANGUAGE": "xx", + "LC_ALL": "fr", + }, + expectedLanguage: "fr", + }, + } + for _, tt := range tests { + t.Run(tt.rule, func(t *testing.T) { + // I. Prepare the environment + os.Clearenv() + if tt.env != nil { + for envKey, envValue := range tt.env { + err := os.Setenv(envKey, envValue) + if err != nil { + t.Errorf("os.Setenv() failed for %s=%s", envKey, envValue) + return + } + } + } + + // II. Run the initialization of localization + resetLocalization() + setupLocalization() + + // III. Assert that language was detected correctly + if tt.expectedLanguage != "" { + actualLanguage := gotext.GetLanguage() + if actualLanguage != tt.expectedLanguage { + t.Errorf("Expected language `%v' but got `%v'.", tt.expectedLanguage, actualLanguage) + return + } + } + + // IV. Assert that the message was translated adequately + if tt.message != "" { + actualTranslation := gotext.Get(tt.message) + if actualTranslation != tt.expectedTranslation { + t.Errorf("Expected translation `%v' but got `%v'.", tt.expectedTranslation, actualTranslation) + return + } + } + }) + } +} From d5cb9699a36c4be6e2656f933bc3e8745c76181c Mon Sep 17 00:00:00 2001 From: Goutte Date: Wed, 10 Jan 2024 16:07:48 +0100 Subject: [PATCH 2/9] feat(i18n): only embed locale files when using the build tag 'locales' --- localizer.go | 22 ++++++++++++++-------- localizer_locales.go | 29 +++++++++++++++++++++++++++++ localizer_notlocales.go | 30 ++++++++++++++++++++++++++++++ localizer_test.go | 14 ++++++++++++++ 4 files changed, 87 insertions(+), 8 deletions(-) create mode 100644 localizer_locales.go create mode 100644 localizer_notlocales.go diff --git a/localizer.go b/localizer.go index 90a94a83..9cc54d4c 100644 --- a/localizer.go +++ b/localizer.go @@ -1,7 +1,20 @@ +// Copyright 2013-2024 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( - "embed" "fmt" "os" @@ -27,13 +40,6 @@ var availableLocalizationDomains = []string{ "default", } -// localeFS points to an embedded filesystem of binary gettext translation files. -// For performance and smaller builds, only the binary MO files are included. -// Their sibling PO files should still be considered their authoritative source. -// -//go:embed locales/*/*.mo -var localeFS embed.FS - // i18nCommandGlossary wraps the translated strings passed to the command usage template. // This is used in CommandUsageTemplateData. type i18nCommandGlossary struct { diff --git a/localizer_locales.go b/localizer_locales.go new file mode 100644 index 00000000..be33bbb2 --- /dev/null +++ b/localizer_locales.go @@ -0,0 +1,29 @@ +// Copyright 2013-2024 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build locales +// +build locales + +package cobra + +import ( + "embed" +) + +// localeFS points to an embedded filesystem of binary gettext translation files. +// For performance and smaller builds, only the binary MO files are included. +// Their sibling PO files should still be considered their authoritative source. +// +//go:embed locales/*/*.mo +var localeFS embed.FS diff --git a/localizer_notlocales.go b/localizer_notlocales.go new file mode 100644 index 00000000..cafb9ffe --- /dev/null +++ b/localizer_notlocales.go @@ -0,0 +1,30 @@ +// Copyright 2013-2024 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//go:build !locales +// +build !locales + +package cobra + +import ( + "embed" +) + +// localeFS points to an embedded filesystem of binary gettext translation files, +// but only for the default (english) language, as the locales build tag was not set. +// For performance and smaller builds, only the binary MO files are included. +// Their sibling PO files should still be considered their authoritative source. +// +//go:embed locales/*/en.mo +var localeFS embed.FS diff --git a/localizer_test.go b/localizer_test.go index 16efdc01..94dfa434 100644 --- a/localizer_test.go +++ b/localizer_test.go @@ -1,3 +1,17 @@ +// Copyright 2013-2024 The Cobra Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package cobra import ( From 3896533e763169178a5f73305bbbb17a2bcf897b Mon Sep 17 00:00:00 2001 From: Goutte Date: Wed, 10 Jan 2024 16:13:34 +0100 Subject: [PATCH 3/9] feat(i18n): add a Makefile recipe to install i18n extraction dependencies --- Makefile | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 53262b6e..87d1228b 100644 --- a/Makefile +++ b/Makefile @@ -27,13 +27,16 @@ richtest: install_deps $(info ******************** running tests with kyoh86/richgo ********************) richgo test -v ./... +i18n_extract: install_i18n_deps + $(info ******************** extracting translation files ********************) + xgotext -v -in . -out locales + install_deps: $(info ******************** downloading dependencies ********************) go get -v ./... +install_i18n_deps: + go install github.com/leonelquinteros/gotext/cli/xgotext + clean: rm -rf $(BIN) - -i18n_extract: - $(info ******************** extracting translation files ********************) - xgotext -v -in . -out locales From 4a45b27a3972888ec64527a53baf8fb3e772a297 Mon Sep 17 00:00:00 2001 From: Goutte Date: Wed, 10 Jan 2024 16:37:15 +0100 Subject: [PATCH 4/9] chore(i18n): lint --- args.go | 3 ++- cobra.go | 3 ++- command.go | 3 ++- completions.go | 3 ++- flag_groups.go | 3 ++- localizer.go | 3 ++- localizer_test.go | 3 ++- 7 files changed, 14 insertions(+), 7 deletions(-) diff --git a/args.go b/args.go index 6170a58e..ae378b5a 100644 --- a/args.go +++ b/args.go @@ -16,8 +16,9 @@ package cobra import ( "fmt" - "github.com/leonelquinteros/gotext" "strings" + + "github.com/leonelquinteros/gotext" ) type PositionalArgs func(cmd *Command, args []string) error diff --git a/cobra.go b/cobra.go index 38581704..360d1c2e 100644 --- a/cobra.go +++ b/cobra.go @@ -19,7 +19,6 @@ package cobra import ( "fmt" - "github.com/leonelquinteros/gotext" "io" "os" "reflect" @@ -28,6 +27,8 @@ import ( "text/template" "time" "unicode" + + "github.com/leonelquinteros/gotext" ) var templateFuncs = template.FuncMap{ diff --git a/command.go b/command.go index 1c3e048a..8c505a58 100644 --- a/command.go +++ b/command.go @@ -21,13 +21,14 @@ import ( "context" "errors" "fmt" - "github.com/leonelquinteros/gotext" "io" "os" "path/filepath" "sort" "strings" + "github.com/leonelquinteros/gotext" + flag "github.com/spf13/pflag" ) diff --git a/completions.go b/completions.go index f1e7b2b6..e02aa2c3 100644 --- a/completions.go +++ b/completions.go @@ -16,13 +16,14 @@ package cobra import ( "fmt" - "github.com/leonelquinteros/gotext" "os" "regexp" "strconv" "strings" "sync" + "github.com/leonelquinteros/gotext" + "github.com/spf13/pflag" ) diff --git a/flag_groups.go b/flag_groups.go index 4eea0e1e..714f2ca2 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -16,10 +16,11 @@ package cobra import ( "fmt" - "github.com/leonelquinteros/gotext" "sort" "strings" + "github.com/leonelquinteros/gotext" + flag "github.com/spf13/pflag" ) diff --git a/localizer.go b/localizer.go index 9cc54d4c..21dea7d4 100644 --- a/localizer.go +++ b/localizer.go @@ -18,8 +18,9 @@ import ( "fmt" "os" - "github.com/leonelquinteros/gotext" "golang.org/x/text/language" + + "github.com/leonelquinteros/gotext" ) var defaultLanguage = language.English diff --git a/localizer_test.go b/localizer_test.go index 94dfa434..44ef594c 100644 --- a/localizer_test.go +++ b/localizer_test.go @@ -15,9 +15,10 @@ package cobra import ( - "github.com/leonelquinteros/gotext" "os" "testing" + + "github.com/leonelquinteros/gotext" ) // resetLocalization resets to the vendor defaults From d995d9fd4eedad96477ab74abfbd24d6b12b19b7 Mon Sep 17 00:00:00 2001 From: Goutte Date: Wed, 10 Jan 2024 16:44:52 +0100 Subject: [PATCH 5/9] feat(i18n): test the locales using the appropriate build flag --- Makefile | 3 ++- localizer_test.go | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 87d1228b..2c75209a 100644 --- a/Makefile +++ b/Makefile @@ -21,7 +21,8 @@ lint: test: install_deps $(info ******************** running tests ********************) - LANGUAGE="en" go test -v ./... + go test -v ./... + LANGUAGE="en" go test -tags locales -v ./... richtest: install_deps $(info ******************** running tests with kyoh86/richgo ********************) diff --git a/localizer_test.go b/localizer_test.go index 44ef594c..41e28faa 100644 --- a/localizer_test.go +++ b/localizer_test.go @@ -12,6 +12,9 @@ // See the License for the specific language governing permissions and // limitations under the License. +//go:build locales +// +build locales + package cobra import ( From de0723fc080d0a709cd2163923bbee452e64eae1 Mon Sep 17 00:00:00 2001 From: Goutte Date: Sun, 2 Feb 2025 11:51:31 +0100 Subject: [PATCH 6/9] chore: fix a rebase blooper --- command.go | 1 - 1 file changed, 1 deletion(-) diff --git a/command.go b/command.go index 8c505a58..50173b92 100644 --- a/command.go +++ b/command.go @@ -23,7 +23,6 @@ import ( "fmt" "io" "os" - "path/filepath" "sort" "strings" From 411323dcca31a91c1da966b88244a281fe6f2902 Mon Sep 17 00:00:00 2001 From: Goutte Date: Sun, 2 Feb 2025 12:24:40 +0100 Subject: [PATCH 7/9] fix: make the test suite happy again I've had to disable the usage template i18n. --- command.go | 15 +++++++++------ locales/default.pot | 6 +++++- locales/default/en.mo | Bin 3185 -> 3231 bytes locales/default/en.po | 4 ++-- locales/default/fr.mo | Bin 3415 -> 3469 bytes locales/default/fr.po | 4 ++-- 6 files changed, 18 insertions(+), 11 deletions(-) diff --git a/command.go b/command.go index 50173b92..a1512d14 100644 --- a/command.go +++ b/command.go @@ -446,11 +446,14 @@ func (c *Command) UsageFunc() (f func(*Command) error) { return func(c *Command) error { c.mergePersistentFlags() fn := c.getUsageTemplateFunc() - data := CommandUsageTemplateData{ - Command: c, - I18n: getCommandGlossary(), - } - err := fn(c.OutOrStderr(), data) + // FIXME: this breaks the template func signature ; we need another approach ? + //data := CommandUsageTemplateData{ + // Command: c, + // I18n: getCommandGlossary(), + //} + //err := fn(c.OutOrStderr(), data) + ////////////////////////////////////////////////////////////////////////////// + err := fn(c.OutOrStderr(), c) if err != nil { c.PrintErrln(err) } @@ -1270,7 +1273,7 @@ func (c *Command) InitDefaultHelpCmd() { c.helpCommand = &Command{ Use: fmt.Sprintf("help [%s]", gotext.Get("command")), Short: gotext.Get("CommandHelpShort"), - Long: fmt.Sprintf(gotext.Get("CommandHelpLong"), c.DisplayName()+fmt.Sprintf(" help [%s]", gotext.Get("command"))), + Long: fmt.Sprintf(gotext.Get("CommandHelpLong"), c.DisplayName()+fmt.Sprintf(" help [%s]", gotext.Get("PathToCommand"))), ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) { var completions []string cmd, _, e := c.Root().Find(args) diff --git a/locales/default.pot b/locales/default.pot index 6644be0b..67e39981 100644 --- a/locales/default.pot +++ b/locales/default.pot @@ -160,4 +160,8 @@ msgstr "" #: command.go:1247 #: command.go:1249 msgid "command" -msgstr "" \ No newline at end of file +msgstr "" + + +msgid "PathToCommand" +msgstr "" diff --git a/locales/default/en.mo b/locales/default/en.mo index b7ab9e04ed1205546edb24722622907e60988406..4c1622954e94132977bcb4b207cca165ceb863b8 100644 GIT binary patch delta 790 zcmXZa%}Z2K9LDkA-0RIuCv^sMrcr0qj7l&PL_sv)5(xq$=?z*;3Poh2IICQYu0$jd zu3`kzKp+Gylxb7C(m%kR3PBPsTiL?EmEXsi!<_k?bLQT2pYxl09Qhp0{0+?qezQEOHq8hT3=?6>t+za0eCSCyP&TAFtr;i+KmJk@*Xp!CAb^ z^UWzSi(?<&!k4H3ACT~t#SUD zi`SqA#={-a7Cq6|uY9d*JkRG@pP2R%gvOCle8RhXv>^Y^Gq%^_Rb0&1TX+igd1m^hES_!{%rzyaQG znJ%*|K0_t2g(Piz_!KKxz#6IpF|x>D0olXeqAD!oB7VeI*g$n)mMWBB5w-pX^{KvN zS*>m`QN}l@$|AHwiS(mhkVPdlh5T$Lh@S`XOH_xJku7Wuwa*4}oLbLlzyG733AyP! z`R|-APv>XIKXg)e)h7+5oAGN-EIq(X#p#G>@b!&8v_{fzI-{OSjQc}w%o}9(usY+G a9KY_xtG~Q@$ekFUtbXnM>H43;fByl|ZAB9R diff --git a/locales/default/en.po b/locales/default/en.po index a676bb5c..abcd679d 100644 --- a/locales/default/en.po +++ b/locales/default/en.po @@ -168,5 +168,5 @@ msgstr "version for" msgid "command" msgstr "command" -#~ msgid "PathToCommand" -#~ msgstr "path to command" +msgid "PathToCommand" +msgstr "path to command" diff --git a/locales/default/fr.mo b/locales/default/fr.mo index da9097e8f646c14dcd2f8fe2c315ceac6cbf75a2..16fa459d0a4904e4ffef129c158e0454323a98dd 100644 GIT binary patch delta 799 zcmXZaJxE(o6u|M5mt138V`GA7d}?CaKtsTSMHGr9MG+AMK~Z$5hBTlhANbLBkdg{r zO6l}ao6S{lP@;qAAT2nyxF`q;rK6kR;?(~!x$yEk_vOBO&pF)lmhX1)D&ScYMvSMG zXTT-WjaiEyM$#?PjIZ%APNN^+p{`%SDE`0}JVYDMs_$>`8S_SuNIkwngCp1@Qj{@* zHeP%~O=1g!_y@iC4?}p3eQ5Yv?Z-x3MNMP_eYl5gEI;uf-l2YPYAT6^Q4@KI&73d8 z1X@`d>u?DJ__;cF@DcM(q!`&o-FOEz!9%>oU-$$Mc=;YLF^*%kl@5Nw=gil!geN%8 z`7-;UlG#4KV1A0)a>FU?DeX9jF&x4yPUAl6hJLc+6G>t_4&wyQqpmwf?T}4%`hPDr z;UpH{5EKX!_#3tIJJeSC_@R4Zqqe#ONm_bPD^H@{g(1{+Q>d+-L0y+ewvvz4`4{AR z-M`aw7+N3IIty0NX!D}ZSIhQ2CDIJ_Uc{VTE7%qx(i${>9bAU{nl-kazgEKNYnNTCXt%YJP#h46a>8K~$8g~_;5`sZlBb1d$Pnqn1(O#g z>k$cI57y%pHee35{|Y8>8^d^o5xjTX->{Rq1*{$cX zhWaM52-!m&cpo*f6D;8cw&O94dAz|c?5q5nXbHQiuVWD}aTZ6bNR0XAkf0YG)XYAR zG0U?}fR9-xaRzgEh&tdm(p3^<8N)Qr<1}i&Gt`~=aleOI&ftBGd<3=@6_w8*2auq+y+HB diff --git a/locales/default/fr.po b/locales/default/fr.po index 9bb83c22..c1010fef 100644 --- a/locales/default/fr.po +++ b/locales/default/fr.po @@ -168,5 +168,5 @@ msgstr "version pour" msgid "command" msgstr "commande" -#~ msgid "PathToCommand" -#~ msgstr "command" +msgid "PathToCommand" +msgstr "chemin vers la commande" From 8764826b7ffe993704318306aaf2c40204c4af93 Mon Sep 17 00:00:00 2001 From: Goutte Date: Sun, 2 Feb 2025 12:54:52 +0100 Subject: [PATCH 8/9] feat(i18n): localize the default usage function --- command.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/command.go b/command.go index a1512d14..645fddb3 100644 --- a/command.go +++ b/command.go @@ -1971,25 +1971,25 @@ var defaultUsageTemplate = `{{.I18n.SectionUsage}}:{{if .Runnable}} // defaultUsageFunc is equivalent to executing defaultUsageTemplate. The two should be changed in sync. func defaultUsageFunc(w io.Writer, in interface{}) error { c := in.(*Command) - fmt.Fprint(w, "Usage:") + fmt.Fprintf(w, "%s:", gotext.Get("SectionUsage")) if c.Runnable() { fmt.Fprintf(w, "\n %s", c.UseLine()) } if c.HasAvailableSubCommands() { - fmt.Fprintf(w, "\n %s [command]", c.CommandPath()) + fmt.Fprintf(w, "\n %s [%s]", c.CommandPath(), gotext.Get("command")) } if len(c.Aliases) > 0 { - fmt.Fprintf(w, "\n\nAliases:\n") + fmt.Fprintf(w, "\n\n%s:\n", gotext.Get("SectionAliases")) fmt.Fprintf(w, " %s", c.NameAndAliases()) } if c.HasExample() { - fmt.Fprintf(w, "\n\nExamples:\n") + fmt.Fprintf(w, "\n\n%s:\n", gotext.Get("SectionExamples")) fmt.Fprintf(w, "%s", c.Example) } if c.HasAvailableSubCommands() { cmds := c.Commands() if len(c.Groups()) == 0 { - fmt.Fprintf(w, "\n\nAvailable Commands:") + fmt.Fprintf(w, "\n\n%s:", gotext.Get("SectionAvailableCommands")) for _, subcmd := range cmds { if subcmd.IsAvailableCommand() || subcmd.Name() == helpCommandName { fmt.Fprintf(w, "\n %s %s", rpad(subcmd.Name(), subcmd.NamePadding()), subcmd.Short) @@ -2005,7 +2005,7 @@ func defaultUsageFunc(w io.Writer, in interface{}) error { } } if !c.AllChildCommandsHaveGroup() { - fmt.Fprintf(w, "\n\nAdditional Commands:") + fmt.Fprintf(w, "\n\n%s:", gotext.Get("SectionAdditionalCommands")) for _, subcmd := range cmds { if subcmd.GroupID == "" && (subcmd.IsAvailableCommand() || subcmd.Name() == helpCommandName) { fmt.Fprintf(w, "\n %s %s", rpad(subcmd.Name(), subcmd.NamePadding()), subcmd.Short) @@ -2015,15 +2015,15 @@ func defaultUsageFunc(w io.Writer, in interface{}) error { } } if c.HasAvailableLocalFlags() { - fmt.Fprintf(w, "\n\nFlags:\n") + fmt.Fprintf(w, "\n\n%s:\n", gotext.Get("SectionFlags")) fmt.Fprint(w, trimRightSpace(c.LocalFlags().FlagUsages())) } if c.HasAvailableInheritedFlags() { - fmt.Fprintf(w, "\n\nGlobal Flags:\n") + fmt.Fprintf(w, "\n\n%s:\n", gotext.Get("SectionGlobalFlags")) fmt.Fprint(w, trimRightSpace(c.InheritedFlags().FlagUsages())) } if c.HasHelpSubCommands() { - fmt.Fprintf(w, "\n\nAdditional help topcis:") + fmt.Fprintf(w, "\n\n%s:", gotext.Get("SectionAdditionalHelpTopics")) for _, subcmd := range c.Commands() { if subcmd.IsAdditionalHelpTopicCommand() { fmt.Fprintf(w, "\n %s %s", rpad(subcmd.CommandPath(), subcmd.CommandPathPadding()), subcmd.Short) @@ -2031,7 +2031,7 @@ func defaultUsageFunc(w io.Writer, in interface{}) error { } } if c.HasAvailableSubCommands() { - fmt.Fprintf(w, "\n\nUse \"%s [command] --help\" for more information about a command.", c.CommandPath()) + fmt.Fprintf(w, "\n\n%s \"%s [command] --help\" %s.", gotext.Get("Use"), c.CommandPath(), gotext.Get("ForInfoAboutCommand")) } fmt.Fprintln(w) return nil From 5b955a4f191bace882278f6d83f97a7abcf4c834 Mon Sep 17 00:00:00 2001 From: Goutte Date: Sun, 2 Feb 2025 12:58:42 +0100 Subject: [PATCH 9/9] chore: remove support for i18n of the default usage template string (we use the default usage function instead) --- command.go | 31 +++++++++---------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/command.go b/command.go index 645fddb3..7cb55b3e 100644 --- a/command.go +++ b/command.go @@ -48,12 +48,6 @@ type Group struct { Title string } -// CommandUsageTemplateData is the data passed to the template of command usage -type CommandUsageTemplateData struct { - *Command - I18n *i18nCommandGlossary -} - // Command is just that, a command for your application. // E.g. 'go run ...' - 'run' is the command. Cobra requires // you to define the usage and description as part of your command @@ -446,13 +440,6 @@ func (c *Command) UsageFunc() (f func(*Command) error) { return func(c *Command) error { c.mergePersistentFlags() fn := c.getUsageTemplateFunc() - // FIXME: this breaks the template func signature ; we need another approach ? - //data := CommandUsageTemplateData{ - // Command: c, - // I18n: getCommandGlossary(), - //} - //err := fn(c.OutOrStderr(), data) - ////////////////////////////////////////////////////////////////////////////// err := fn(c.OutOrStderr(), c) if err != nil { c.PrintErrln(err) @@ -1937,35 +1924,35 @@ type tmplFunc struct { fn func(io.Writer, interface{}) error } -var defaultUsageTemplate = `{{.I18n.SectionUsage}}:{{if .Runnable}} +var defaultUsageTemplate = `Usage:{{if .Runnable}} {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} -{{.I18n.SectionAliases}}: +Aliases: {{.NameAndAliases}}{{end}}{{if .HasExample}} -{{.I18n.SectionExamples}}: +Examples: {{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}} -{{.I18n.SectionAvailableCommands}}:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} +Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}} {{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}} -{{.I18n.SectionAdditionalCommands}}:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} +Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}} {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} -{{.I18n.SectionFlags}}: +Flags: {{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} -{{.I18n.SectionGlobalFlags}}: +Global Flags: {{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} -{{.I18n.SectionAdditionalHelpTopics}}:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} +Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} -{{.I18n.Use}} "{{.CommandPath}} [command] --help" {{.I18n.ForInfoAboutCommand}}.{{end}} +Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}} ` // defaultUsageFunc is equivalent to executing defaultUsageTemplate. The two should be changed in sync.