Skip to content

Commit ab14efa

Browse files
committed
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
1 parent 236f3c0 commit ab14efa

18 files changed

+954
-50
lines changed

Makefile

+5-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ lint:
2121

2222
test: install_deps
2323
$(info ******************** running tests ********************)
24-
go test -v ./...
24+
LANGUAGE="en" go test -v ./...
2525

2626
richtest: install_deps
2727
$(info ******************** running tests with kyoh86/richgo ********************)
@@ -33,3 +33,7 @@ install_deps:
3333

3434
clean:
3535
rm -rf $(BIN)
36+
37+
i18n_extract:
38+
$(info ******************** extracting translation files ********************)
39+
xgotext -v -in . -out locales

args.go

+8-7
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ package cobra
1616

1717
import (
1818
"fmt"
19+
"github.com/leonelquinteros/gotext"
1920
"strings"
2021
)
2122

@@ -33,15 +34,15 @@ func legacyArgs(cmd *Command, args []string) error {
3334

3435
// root command with subcommands, do subcommand checking.
3536
if !cmd.HasParent() && len(args) > 0 {
36-
return fmt.Errorf("unknown command %q for %q%s", args[0], cmd.CommandPath(), cmd.findSuggestions(args[0]))
37+
return fmt.Errorf(gotext.Get("LegacyArgsValidationError"), args[0], cmd.CommandPath(), cmd.findSuggestions(args[0]))
3738
}
3839
return nil
3940
}
4041

4142
// NoArgs returns an error if any args are included.
4243
func NoArgs(cmd *Command, args []string) error {
4344
if len(args) > 0 {
44-
return fmt.Errorf("unknown command %q for %q", args[0], cmd.CommandPath())
45+
return fmt.Errorf(gotext.Get("NoArgsValidationError"), args[0], cmd.CommandPath())
4546
}
4647
return nil
4748
}
@@ -58,7 +59,7 @@ func OnlyValidArgs(cmd *Command, args []string) error {
5859
}
5960
for _, v := range args {
6061
if !stringInSlice(v, validArgs) {
61-
return fmt.Errorf("invalid argument %q for %q%s", v, cmd.CommandPath(), cmd.findSuggestions(args[0]))
62+
return fmt.Errorf(gotext.Get("OnlyValidArgsValidationError"), v, cmd.CommandPath(), cmd.findSuggestions(args[0]))
6263
}
6364
}
6465
}
@@ -74,7 +75,7 @@ func ArbitraryArgs(cmd *Command, args []string) error {
7475
func MinimumNArgs(n int) PositionalArgs {
7576
return func(cmd *Command, args []string) error {
7677
if len(args) < n {
77-
return fmt.Errorf("requires at least %d arg(s), only received %d", n, len(args))
78+
return fmt.Errorf(gotext.GetN("MinimumNArgsValidationError", "MinimumNArgsValidationErrorPlural", n), n, len(args))
7879
}
7980
return nil
8081
}
@@ -84,7 +85,7 @@ func MinimumNArgs(n int) PositionalArgs {
8485
func MaximumNArgs(n int) PositionalArgs {
8586
return func(cmd *Command, args []string) error {
8687
if len(args) > n {
87-
return fmt.Errorf("accepts at most %d arg(s), received %d", n, len(args))
88+
return fmt.Errorf(gotext.GetN("MaximumNArgsValidationError", "MaximumNArgsValidationErrorPlural", n), n, len(args))
8889
}
8990
return nil
9091
}
@@ -94,7 +95,7 @@ func MaximumNArgs(n int) PositionalArgs {
9495
func ExactArgs(n int) PositionalArgs {
9596
return func(cmd *Command, args []string) error {
9697
if len(args) != n {
97-
return fmt.Errorf("accepts %d arg(s), received %d", n, len(args))
98+
return fmt.Errorf(gotext.GetN("ExactArgsValidationError", "ExactArgsValidationErrorPlural", n), n, len(args))
9899
}
99100
return nil
100101
}
@@ -104,7 +105,7 @@ func ExactArgs(n int) PositionalArgs {
104105
func RangeArgs(min int, max int) PositionalArgs {
105106
return func(cmd *Command, args []string) error {
106107
if len(args) < min || len(args) > max {
107-
return fmt.Errorf("accepts between %d and %d arg(s), received %d", min, max, len(args))
108+
return fmt.Errorf(gotext.GetN("RangeArgsValidationError", "RangeArgsValidationErrorPlural", max), min, max, len(args))
108109
}
109110
return nil
110111
}

args_test.go

+4-4
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ func minimumNArgsWithLessArgs(err error, t *testing.T) {
6868
t.Fatal("Expected an error")
6969
}
7070
got := err.Error()
71-
expected := "requires at least 2 arg(s), only received 1"
71+
expected := "requires at least 2 args, only received 1"
7272
if got != expected {
7373
t.Fatalf("Expected %q, got %q", expected, got)
7474
}
@@ -79,7 +79,7 @@ func maximumNArgsWithMoreArgs(err error, t *testing.T) {
7979
t.Fatal("Expected an error")
8080
}
8181
got := err.Error()
82-
expected := "accepts at most 2 arg(s), received 3"
82+
expected := "accepts at most 2 args, received 3"
8383
if got != expected {
8484
t.Fatalf("Expected %q, got %q", expected, got)
8585
}
@@ -90,7 +90,7 @@ func exactArgsWithInvalidCount(err error, t *testing.T) {
9090
t.Fatal("Expected an error")
9191
}
9292
got := err.Error()
93-
expected := "accepts 2 arg(s), received 3"
93+
expected := "accepts 2 args, received 3"
9494
if got != expected {
9595
t.Fatalf("Expected %q, got %q", expected, got)
9696
}
@@ -101,7 +101,7 @@ func rangeArgsWithInvalidCount(err error, t *testing.T) {
101101
t.Fatal("Expected an error")
102102
}
103103
got := err.Error()
104-
expected := "accepts between 2 and 4 arg(s), received 1"
104+
expected := "accepts between 2 and 4 args, received 1"
105105
if got != expected {
106106
t.Fatalf("Expected %q, got %q", expected, got)
107107
}

cobra.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ package cobra
1919

2020
import (
2121
"fmt"
22+
"github.com/leonelquinteros/gotext"
2223
"io"
2324
"os"
2425
"reflect"
@@ -230,7 +231,7 @@ func stringInSlice(a string, list []string) bool {
230231
// CheckErr prints the msg with the prefix 'Error:' and exits with error code 1. If the msg is nil, it does nothing.
231232
func CheckErr(msg interface{}) {
232233
if msg != nil {
233-
fmt.Fprintln(os.Stderr, "Error:", msg)
234+
fmt.Fprintln(os.Stderr, gotext.Get("Error")+":", msg)
234235
os.Exit(1)
235236
}
236237
}

command.go

+33-23
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"context"
2222
"errors"
2323
"fmt"
24+
"github.com/leonelquinteros/gotext"
2425
"io"
2526
"os"
2627
"path/filepath"
@@ -44,6 +45,12 @@ type Group struct {
4445
Title string
4546
}
4647

48+
// CommandUsageTemplateData is the data passed to the template of command usage
49+
type CommandUsageTemplateData struct {
50+
*Command
51+
I18n *i18nCommandGlossary
52+
}
53+
4754
// Command is just that, a command for your application.
4855
// E.g. 'go run ...' - 'run' is the command. Cobra requires
4956
// you to define the usage and description as part of your command
@@ -432,7 +439,11 @@ func (c *Command) UsageFunc() (f func(*Command) error) {
432439
}
433440
return func(c *Command) error {
434441
c.mergePersistentFlags()
435-
err := tmpl(c.OutOrStderr(), c.UsageTemplate(), c)
442+
data := CommandUsageTemplateData{
443+
Command: c,
444+
I18n: getCommandGlossary(),
445+
}
446+
err := tmpl(c.OutOrStderr(), c.UsageTemplate(), data)
436447
if err != nil {
437448
c.PrintErrln(err)
438449
}
@@ -549,35 +560,35 @@ func (c *Command) UsageTemplate() string {
549560
if c.HasParent() {
550561
return c.parent.UsageTemplate()
551562
}
552-
return `Usage:{{if .Runnable}}
563+
return `{{.I18n.SectionUsage}}:{{if .Runnable}}
553564
{{.UseLine}}{{end}}{{if .HasAvailableSubCommands}}
554565
{{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}}
555566
556-
Aliases:
567+
{{.I18n.SectionAliases}}:
557568
{{.NameAndAliases}}{{end}}{{if .HasExample}}
558569
559-
Examples:
570+
{{.I18n.SectionExamples}}:
560571
{{.Example}}{{end}}{{if .HasAvailableSubCommands}}{{$cmds := .Commands}}{{if eq (len .Groups) 0}}
561572
562-
Available Commands:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
573+
{{.I18n.SectionAvailableCommands}}:{{range $cmds}}{{if (or .IsAvailableCommand (eq .Name "help"))}}
563574
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{else}}{{range $group := .Groups}}
564575
565576
{{.Title}}{{range $cmds}}{{if (and (eq .GroupID $group.ID) (or .IsAvailableCommand (eq .Name "help")))}}
566577
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if not .AllChildCommandsHaveGroup}}
567578
568-
Additional Commands:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
579+
{{.I18n.SectionAdditionalCommands}}:{{range $cmds}}{{if (and (eq .GroupID "") (or .IsAvailableCommand (eq .Name "help")))}}
569580
{{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}}
570581
571-
Flags:
582+
{{.I18n.SectionFlags}}:
572583
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}}
573584
574-
Global Flags:
585+
{{.I18n.SectionGlobalFlags}}:
575586
{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}}
576587
577-
Additional help topics:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
588+
{{.I18n.SectionAdditionalHelpTopics}}:{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}}
578589
{{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}}
579590
580-
Use "{{.CommandPath}} [command] --help" for more information about a command.{{end}}
591+
{{.I18n.Use}} "{{.CommandPath}} [command] --help" {{.I18n.ForInfoAboutCommand}}.{{end}}
581592
`
582593
}
583594

@@ -756,7 +767,7 @@ func (c *Command) findSuggestions(arg string) string {
756767
}
757768
var sb strings.Builder
758769
if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 {
759-
sb.WriteString("\n\nDid you mean this?\n")
770+
sb.WriteString("\n\n" + gotext.Get("DidYouMeanThis") + "\n")
760771
for _, s := range suggestions {
761772
_, _ = fmt.Fprintf(&sb, "\t%v\n", s)
762773
}
@@ -877,7 +888,7 @@ func (c *Command) execute(a []string) (err error) {
877888
}
878889

879890
if len(c.Deprecated) > 0 {
880-
c.Printf("Command %q is deprecated, %s\n", c.Name(), c.Deprecated)
891+
c.Printf(gotext.Get("CommandDeprecatedWarning")+"\n", c.Name(), c.Deprecated)
881892
}
882893

883894
// initialize help and version flag at the last point possible to allow for user
@@ -1096,7 +1107,7 @@ func (c *Command) ExecuteC() (cmd *Command, err error) {
10961107
}
10971108
if !c.SilenceErrors {
10981109
c.PrintErrln(c.ErrPrefix(), err.Error())
1099-
c.PrintErrf("Run '%v --help' for usage.\n", c.CommandPath())
1110+
c.PrintErrf(gotext.Get("RunHelpTip")+"\n", c.CommandPath())
11001111
}
11011112
return c, err
11021113
}
@@ -1162,7 +1173,7 @@ func (c *Command) ValidateRequiredFlags() error {
11621173
})
11631174

11641175
if len(missingFlagNames) > 0 {
1165-
return fmt.Errorf(`required flag(s) "%s" not set`, strings.Join(missingFlagNames, `", "`))
1176+
return fmt.Errorf(gotext.GetN("FlagNotSetError", "FlagNotSetErrorPlural", len(missingFlagNames)), strings.Join(missingFlagNames, `", "`))
11661177
}
11671178
return nil
11681179
}
@@ -1186,9 +1197,9 @@ func (c *Command) checkCommandGroups() {
11861197
func (c *Command) InitDefaultHelpFlag() {
11871198
c.mergePersistentFlags()
11881199
if c.Flags().Lookup("help") == nil {
1189-
usage := "help for "
1200+
usage := gotext.Get("HelpFor") + " "
11901201
if c.Name() == "" {
1191-
usage += "this command"
1202+
usage += gotext.Get("ThisCommand")
11921203
} else {
11931204
usage += c.Name()
11941205
}
@@ -1208,9 +1219,9 @@ func (c *Command) InitDefaultVersionFlag() {
12081219

12091220
c.mergePersistentFlags()
12101221
if c.Flags().Lookup("version") == nil {
1211-
usage := "version for "
1222+
usage := gotext.Get("VersionFor") + " "
12121223
if c.Name() == "" {
1213-
usage += "this command"
1224+
usage += gotext.Get("ThisCommand")
12141225
} else {
12151226
usage += c.Name()
12161227
}
@@ -1233,10 +1244,9 @@ func (c *Command) InitDefaultHelpCmd() {
12331244

12341245
if c.helpCommand == nil {
12351246
c.helpCommand = &Command{
1236-
Use: "help [command]",
1237-
Short: "Help about any command",
1238-
Long: `Help provides help for any command in the application.
1239-
Simply type ` + c.Name() + ` help [path to command] for full details.`,
1247+
Use: fmt.Sprintf("help [%s]", gotext.Get("command")),
1248+
Short: gotext.Get("CommandHelpShort"),
1249+
Long: fmt.Sprintf(gotext.Get("CommandHelpLong"), c.Name()+fmt.Sprintf(" help [%s]", gotext.Get("command"))),
12401250
ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) {
12411251
var completions []string
12421252
cmd, _, e := c.Root().Find(args)
@@ -1259,7 +1269,7 @@ Simply type ` + c.Name() + ` help [path to command] for full details.`,
12591269
Run: func(c *Command, args []string) {
12601270
cmd, _, e := c.Root().Find(args)
12611271
if cmd == nil || e != nil {
1262-
c.Printf("Unknown help topic %#q\n", args)
1272+
c.Printf(gotext.Get("CommandHelpUnknownTopicError")+"\n", args)
12631273
CheckErr(c.Root().Usage())
12641274
} else {
12651275
cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown

command_test.go

+17-2
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,21 @@ func TestPersistentFlagsOnChild(t *testing.T) {
815815
}
816816
}
817817

818+
func TestRequiredFlag(t *testing.T) {
819+
c := &Command{Use: "c", Run: emptyRun}
820+
c.Flags().String("foo1", "", "")
821+
assertNoErr(t, c.MarkFlagRequired("foo1"))
822+
823+
expected := fmt.Sprintf("required flag %q is not set", "foo1")
824+
825+
_, err := executeCommand(c)
826+
got := err.Error()
827+
828+
if got != expected {
829+
t.Errorf("Expected error: %q, got: %q", expected, got)
830+
}
831+
}
832+
818833
func TestRequiredFlags(t *testing.T) {
819834
c := &Command{Use: "c", Run: emptyRun}
820835
c.Flags().String("foo1", "", "")
@@ -823,7 +838,7 @@ func TestRequiredFlags(t *testing.T) {
823838
assertNoErr(t, c.MarkFlagRequired("foo2"))
824839
c.Flags().String("bar", "", "")
825840

826-
expected := fmt.Sprintf("required flag(s) %q, %q not set", "foo1", "foo2")
841+
expected := fmt.Sprintf("required flags %q, %q are not set", "foo1", "foo2")
827842

828843
_, err := executeCommand(c)
829844
got := err.Error()
@@ -850,7 +865,7 @@ func TestPersistentRequiredFlags(t *testing.T) {
850865

851866
parent.AddCommand(child)
852867

853-
expected := fmt.Sprintf("required flag(s) %q, %q, %q, %q not set", "bar1", "bar2", "foo1", "foo2")
868+
expected := fmt.Sprintf("required flags %q, %q, %q, %q are not set", "bar1", "bar2", "foo1", "foo2")
854869

855870
_, err := executeCommand(parent, "child")
856871
if err.Error() != expected {

0 commit comments

Comments
 (0)