diff --git a/Makefile b/Makefile index 0da8d7aa0..2c75209ac 100644 --- a/Makefile +++ b/Makefile @@ -22,14 +22,22 @@ lint: test: install_deps $(info ******************** running tests ********************) go test -v ./... + LANGUAGE="en" go test -tags locales -v ./... 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) diff --git a/args.go b/args.go index ed1e70cea..ae378b5a8 100644 --- a/args.go +++ b/args.go @@ -17,6 +17,8 @@ package cobra import ( "fmt" "strings" + + "github.com/leonelquinteros/gotext" ) type PositionalArgs func(cmd *Command, args []string) error @@ -33,7 +35,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 +43,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 +60,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 +76,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 +86,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 +96,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 +106,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 90d174cce..c156b4757 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 e0b0947b0..1e3193a81 100644 --- a/cobra.go +++ b/cobra.go @@ -27,6 +27,8 @@ import ( "text/template" "time" "unicode" + + "github.com/leonelquinteros/gotext" ) var templateFuncs = template.FuncMap{ @@ -230,7 +232,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 11a3e9c99..b3ca42485 100644 --- a/command.go +++ b/command.go @@ -27,6 +27,8 @@ import ( "sort" "strings" + "github.com/leonelquinteros/gotext" + flag "github.com/spf13/pflag" ) @@ -44,6 +46,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 @@ -432,7 +440,11 @@ func (c *Command) UsageFunc() (f func(*Command) error) { } return func(c *Command) error { c.mergePersistentFlags() - err := tmpl(c.OutOrStderr(), c.UsageTemplate(), c) + data := CommandUsageTemplateData{ + Command: c, + I18n: getCommandGlossary(), + } + err := tmpl(c.OutOrStderr(), c.UsageTemplate(), data) if err != nil { c.PrintErrln(err) } @@ -549,35 +561,35 @@ func (c *Command) UsageTemplate() string { if c.HasParent() { return c.parent.UsageTemplate() } - return `Usage:{{if .Runnable}} + return `{{.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}} ` } @@ -756,7 +768,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) } @@ -877,7 +889,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 @@ -1096,7 +1108,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 } @@ -1162,7 +1174,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 } @@ -1186,9 +1198,9 @@ func (c *Command) checkCommandGroups() { func (c *Command) InitDefaultHelpFlag() { c.mergePersistentFlags() if c.Flags().Lookup("help") == nil { - usage := "help for " + usage := gotext.Get("HelpFor") + " " if c.Name() == "" { - usage += "this command" + usage += gotext.Get("ThisCommand") } else { usage += c.Name() } @@ -1208,9 +1220,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.Name() } @@ -1233,10 +1245,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.Name() + ` 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.Name()+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) @@ -1259,7 +1270,7 @@ Simply type ` + c.Name() + ` 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 diff --git a/command_test.go b/command_test.go index 9f686d65e..a09c972d0 100644 --- a/command_test.go +++ b/command_test.go @@ -815,6 +815,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", "", "") @@ -823,7 +838,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() @@ -850,7 +865,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 b0e41df0c..d2aabedcf 100644 --- a/completions.go +++ b/completions.go @@ -20,6 +20,8 @@ import ( "strings" "sync" + "github.com/leonelquinteros/gotext" + "github.com/spf13/pflag" ) @@ -48,7 +50,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 ( @@ -97,7 +99,6 @@ const ( // Constants for the completion command compCmdName = "completion" compCmdNoDescFlagName = "no-descriptions" - compCmdNoDescFlagDesc = "disable completion descriptions" compCmdNoDescFlagDefault = false ) @@ -199,9 +200,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 { @@ -248,7 +248,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) @@ -742,7 +742,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{ @@ -781,7 +781,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{ @@ -806,7 +806,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{ @@ -832,7 +832,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 2be3b18b1..ae2fa5bb3 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -19,6 +19,8 @@ import ( "sort" "strings" + "github.com/leonelquinteros/gotext" + flag "github.com/spf13/pflag" ) @@ -201,7 +203,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 a79e66a13..3e4fa2eb6 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.3 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 871c3a8af..cb5507787 100644 --- a/go.sum +++ b/go.sum @@ -2,10 +2,37 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0q github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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 000000000..90da71d53 --- /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 000000000..6644be0b5 --- /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 000000000..b7ab9e04e Binary files /dev/null and b/locales/default/en.mo differ diff --git a/locales/default/en.po b/locales/default/en.po new file mode 100644 index 000000000..a676bb5cb --- /dev/null +++ b/locales/default/en.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: en\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 "Command %q is deprecated, %s" + +#: command.go:1249 +msgid "CommandHelpLong" +msgstr "" +"Help provides help for any command in the application.\n" +"Simply type %s for full details." + +#: command.go:1248 +msgid "CommandHelpShort" +msgstr "Help about any command" + +#: command.go:1272 +msgid "CommandHelpUnknownTopicError" +msgstr "Unknown help topic %#q" + +#: completions.go:250 +msgid "CompletionCommandShellDirectiveTip" +msgstr "Completion ended with directive: %s" + +#: completions.go:203 +msgid "CompletionCommandShellLong" +msgstr "" +"%s is a special command that is used by the shell completion logic\n" +"to request completion choices for the specified command-line." + +#: completions.go:202 +msgid "CompletionCommandShellShort" +msgstr "Request shell completion choices for the specified command-line" + +#: completions.go:745 completions.go:784 completions.go:809 completions.go:835 +msgid "CompletionSubcommandNoDescFlagDesc" +msgstr "disable completion descriptions" + +#: completions.go:52 +msgid "CompletionSubcommandUnsupportedFlagError" +msgstr "subcommand '%s' does not support flag '%s'" + +#: command.go:770 +msgid "DidYouMeanThis" +msgstr "Did you mean this?" + +#: cobra.go:234 +msgid "Error" +msgstr "Error" + +#: args.go:98 +msgid "ExactArgsValidationError" +msgid_plural "ExactArgsValidationErrorPlural" +msgstr[0] "accepts %d arg, received %d" +msgstr[1] "accepts %d args, received %d" + +#: flag_groups.go:205 +msgid "ExclusiveFlagsValidationError" +msgstr "" +"if any flags in the group [%v] are set none of the others can be; %v were " +"all set" + +#: command.go:1176 +msgid "FlagNotSetError" +msgid_plural "FlagNotSetErrorPlural" +msgstr[0] "required flag \"%s\" is not set" +msgstr[1] "required flags \"%s\" are not set" + +#: localizer.go:66 +msgid "ForInfoAboutCommand" +msgstr "for more information about a command" + +#: command.go:1200 +msgid "HelpFor" +msgstr "help for" + +#: args.go:37 +msgid "LegacyArgsValidationError" +msgstr "unknown command %q for %q%s" + +#: args.go:88 +msgid "MaximumNArgsValidationError" +msgid_plural "MaximumNArgsValidationErrorPlural" +msgstr[0] "accepts at most %d arg, received %d" +msgstr[1] "accepts at most %d args, received %d" + +#: args.go:78 +msgid "MinimumNArgsValidationError" +msgid_plural "MinimumNArgsValidationErrorPlural" +msgstr[0] "requires at least %d arg, only received %d" +msgstr[1] "requires at least %d args, only received %d" + +#: args.go:45 +msgid "NoArgsValidationError" +msgstr "unknown command %q for %q" + +#: args.go:62 +msgid "OnlyValidArgsValidationError" +msgstr "invalid argument %q for %q%s" + +#: args.go:108 +msgid "RangeArgsValidationError" +msgid_plural "RangeArgsValidationErrorPlural" +msgstr[0] "accepts between %d and %d arg, received %d" +msgstr[1] "accepts between %d and %d args, received %d" + +#: command.go:1110 +msgid "RunHelpTip" +msgstr "Run '%v --help' for usage." + +#: localizer.go:61 +msgid "SectionAdditionalCommands" +msgstr "Additional Commands" + +#: localizer.go:64 +msgid "SectionAdditionalHelpTopics" +msgstr "Additional Help Topics" + +#: localizer.go:58 +msgid "SectionAliases" +msgstr "Aliases" + +#: localizer.go:60 +msgid "SectionAvailableCommands" +msgstr "Available Commands" + +#: localizer.go:59 +msgid "SectionExamples" +msgstr "Examples" + +#: localizer.go:62 +msgid "SectionFlags" +msgstr "Flags" + +#: localizer.go:63 +msgid "SectionGlobalFlags" +msgstr "Global Flags" + +#: localizer.go:57 +msgid "SectionUsage" +msgstr "Usage" + +#: command.go:1202 command.go:1224 +msgid "ThisCommand" +msgstr "this command" + +#: localizer.go:65 +msgid "Use" +msgstr "Use" + +#: command.go:1222 +msgid "VersionFor" +msgstr "version for" + +#: command.go:1247 command.go:1249 +msgid "command" +msgstr "command" + +#~ msgid "PathToCommand" +#~ msgstr "path to command" diff --git a/locales/default/fr.mo b/locales/default/fr.mo new file mode 100644 index 000000000..da9097e8f Binary files /dev/null and b/locales/default/fr.mo differ diff --git a/locales/default/fr.po b/locales/default/fr.po new file mode 100644 index 000000000..9bb83c22a --- /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 000000000..21dea7d43 --- /dev/null +++ b/localizer.go @@ -0,0 +1,145 @@ +// 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 ( + "fmt" + "os" + + "golang.org/x/text/language" + + "github.com/leonelquinteros/gotext" +) + +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", +} + +// 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_locales.go b/localizer_locales.go new file mode 100644 index 000000000..be33bbb2f --- /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 000000000..cafb9ffe2 --- /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 new file mode 100644 index 000000000..41e28faa4 --- /dev/null +++ b/localizer_test.go @@ -0,0 +1,179 @@ +// 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 ( + "os" + "testing" + + "github.com/leonelquinteros/gotext" +) + +// 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 + } + } + }) + } +}