From ccbdd223d12559eeb7f17d6a9f2d8144dd947c81 Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 04:17:45 +0200 Subject: [PATCH 01/12] feat(i18n): draft of i18n with embedded TOML files Supports rfc4646 language tags in env vars. If region is detected (en-US, fr-CA), will try to load regional files first. --- cobra.go | 2 +- flag_groups.go | 2 +- go.mod | 4 +- localizer.go | 94 +++++++++++++++++++++++++++++++++++++++ translations/main.en.toml | 7 +++ translations/main.fr.toml | 7 +++ 6 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 localizer.go create mode 100644 translations/main.en.toml create mode 100644 translations/main.fr.toml diff --git a/cobra.go b/cobra.go index a6b160ce5..5c3eaa2b0 100644 --- a/cobra.go +++ b/cobra.go @@ -232,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, i18nError()+":", msg) os.Exit(1) } } diff --git a/flag_groups.go b/flag_groups.go index 0671ec5f2..2fa327c4b 100644 --- a/flag_groups.go +++ b/flag_groups.go @@ -201,7 +201,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(i18nExclusiveFlagsValidationError(), flagList, set) } return nil } diff --git a/go.mod b/go.mod index a79e66a13..589429703 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/BurntSushi/toml v1.0.0 github.com/cpuguy83/go-md2man/v2 v2.0.3 github.com/inconshreveable/mousetrap v1.1.0 + github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/spf13/pflag v1.0.5 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/localizer.go b/localizer.go new file mode 100644 index 000000000..037c407e0 --- /dev/null +++ b/localizer.go @@ -0,0 +1,94 @@ +package cobra + +import ( + "embed" + "fmt" + "github.com/BurntSushi/toml" + "github.com/nicksnyder/go-i18n/v2/i18n" + "golang.org/x/text/language" + "os" +) + +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", + "LANG", +} + +// localeFS points to an embedded filesystem of TOML translation files +// +//go:embed translations/*.toml +var localeFS embed.FS + +// Localizer can be used to fetch localized messages +var localizer *i18n.Localizer + +func i18nError() string { + return localizeMessage(&i18n.Message{ + ID: "Error", + Description: "prefix of error messages", + Other: "Error", + }) +} + +func i18nExclusiveFlagsValidationError() string { + return localizeMessage(&i18n.Message{ + ID: "ExclusiveFlagsValidationError", + Description: "error shown when multiple exclusive flags are provided (group flags, offending flags)", + Other: "if any flags in the group [%v] are set none of the others can be; %v were all set", + }) +} + +// … lots more translations here + +func localizeMessage(message *i18n.Message) string { + localizedValue, err := localizer.Localize(&i18n.LocalizeConfig{ + DefaultMessage: message, + }) + if err != nil { + return message.Other + } + + return localizedValue +} + +func loadTranslationFiles(bundle *i18n.Bundle, langs []string) { + for _, lang := range langs { + _, _ = bundle.LoadMessageFileFS(localeFS, fmt.Sprintf("translations/main.%s.toml", lang)) + } +} + +func detectLangs() []string { + var detectedLangs []string + for _, envKey := range envVariablesHoldingLocale { + lang := os.Getenv(envKey) + if lang != "" { + detectedLang := language.Make(lang) + appendLang(&detectedLangs, detectedLang) + } + } + appendLang(&detectedLangs, defaultLanguage) + + return detectedLangs +} + +func appendLang(langs *[]string, lang language.Tag) { + langString := lang.String() + langBase, _ := lang.Base() + *langs = append(*langs, langString) + *langs = append(*langs, langBase.ISO3()) + *langs = append(*langs, langBase.String()) +} + +func init() { + bundle := i18n.NewBundle(defaultLanguage) + bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) + detectedLangs := detectLangs() + //fmt.Println("Detected languages", detectedLangs) + loadTranslationFiles(bundle, detectedLangs) + localizer = i18n.NewLocalizer(bundle, detectedLangs...) +} diff --git a/translations/main.en.toml b/translations/main.en.toml new file mode 100644 index 000000000..212e20078 --- /dev/null +++ b/translations/main.en.toml @@ -0,0 +1,7 @@ +[Error] +description = "prefix of error messages" +other = "Error" + +[ExclusiveFlagsValidationError] +description = "error shown when multiple exclusive flags are provided (group flags, offending flags)" +other = "if any flags in the group [%v] are set none of the others can be; %v were all set" diff --git a/translations/main.fr.toml b/translations/main.fr.toml new file mode 100644 index 000000000..10d2aa24d --- /dev/null +++ b/translations/main.fr.toml @@ -0,0 +1,7 @@ +[Error] +description = "prefix of error messages" +other = "Erreur" + +[ExclusiveFlagsValidationError] +description = "error shown when multiple exclusive flags are provided (group flags, offending flags)" +other = "les options [%v] sont exclusives, mais les options %v ont été fournies" From 6853f280edd64b528726ad8b703253dfe3792e73 Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 04:34:22 +0200 Subject: [PATCH 02/12] feat(i18n): translate command error and usage tip --- command.go | 2 +- localizer.go | 10 +++++++++- translations/{main.en.toml => active.en.toml} | 0 translations/{main.fr.toml => active.fr.toml} | 0 4 files changed, 10 insertions(+), 2 deletions(-) rename translations/{main.en.toml => active.en.toml} (100%) rename translations/{main.fr.toml => active.fr.toml} (100%) diff --git a/command.go b/command.go index 2fbe6c131..237d2238b 100644 --- a/command.go +++ b/command.go @@ -1096,7 +1096,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(i18nRunHelpTip()+"\n", c.CommandPath()) } return c, err } diff --git a/localizer.go b/localizer.go index 037c407e0..9a563eccf 100644 --- a/localizer.go +++ b/localizer.go @@ -35,6 +35,14 @@ func i18nError() string { }) } +func i18nRunHelpTip() string { + return localizeMessage(&i18n.Message{ + ID: "RunHelpTip", + Description: "tip shown when a command fails (command path)", + Other: "Run '%v --help' for usage.", + }) +} + func i18nExclusiveFlagsValidationError() string { return localizeMessage(&i18n.Message{ ID: "ExclusiveFlagsValidationError", @@ -58,7 +66,7 @@ func localizeMessage(message *i18n.Message) string { func loadTranslationFiles(bundle *i18n.Bundle, langs []string) { for _, lang := range langs { - _, _ = bundle.LoadMessageFileFS(localeFS, fmt.Sprintf("translations/main.%s.toml", lang)) + _, _ = bundle.LoadMessageFileFS(localeFS, fmt.Sprintf("translations/active.%s.toml", lang)) } } diff --git a/translations/main.en.toml b/translations/active.en.toml similarity index 100% rename from translations/main.en.toml rename to translations/active.en.toml diff --git a/translations/main.fr.toml b/translations/active.fr.toml similarity index 100% rename from translations/main.fr.toml rename to translations/active.fr.toml From cb76695584536da5e3ad651a34cec9c611a7a901 Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 04:35:32 +0200 Subject: [PATCH 03/12] feat(i18n): add `golang.org/x/text` that we need for some reason --- go.mod | 1 + 1 file changed, 1 insertion(+) diff --git a/go.mod b/go.mod index 589429703..799779c2a 100644 --- a/go.mod +++ b/go.mod @@ -8,5 +8,6 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/spf13/pflag v1.0.5 + golang.org/x/text v0.4.0 gopkg.in/yaml.v3 v3.0.1 ) From 2e90e5171054626163caeb24038babe04442c2a1 Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 04:46:33 +0200 Subject: [PATCH 04/12] feat(i18n): add `make i18n_extract` and `make i18n_merge` Flow is as follows: make i18n_extract make i18n_merge ? --- .gitignore | 3 +++ Makefile | 7 +++++++ translations/active.en.toml | 4 ++++ translations/active.fr.toml | 7 +++++++ 4 files changed, 21 insertions(+) diff --git a/.gitignore b/.gitignore index c7b459e4d..ed0966fee 100644 --- a/.gitignore +++ b/.gitignore @@ -19,6 +19,9 @@ _cgo_export.* _testmain.go +# Translation files +translations/translate.*.toml + # Vim files https://github.com/github/gitignore/blob/master/Global/Vim.gitignore # swap [._]*.s[a-w][a-z] diff --git a/Makefile b/Makefile index 0da8d7aa0..7098c2d8f 100644 --- a/Makefile +++ b/Makefile @@ -33,3 +33,10 @@ install_deps: clean: rm -rf $(BIN) + +i18n_extract: + goi18n extract -outdir translations + goi18n merge -outdir translations translations/* + +i18n_merge: + goi18n merge -outdir translations translations/* diff --git a/translations/active.en.toml b/translations/active.en.toml index 212e20078..8484267e7 100644 --- a/translations/active.en.toml +++ b/translations/active.en.toml @@ -5,3 +5,7 @@ other = "Error" [ExclusiveFlagsValidationError] description = "error shown when multiple exclusive flags are provided (group flags, offending flags)" other = "if any flags in the group [%v] are set none of the others can be; %v were all set" + +[RunHelpTip] +description = "tip shown when a command fails (command path)" +other = "Run '%v --help' for usage." diff --git a/translations/active.fr.toml b/translations/active.fr.toml index 10d2aa24d..a869d3323 100644 --- a/translations/active.fr.toml +++ b/translations/active.fr.toml @@ -1,7 +1,14 @@ [Error] description = "prefix of error messages" +hash = "sha1-7dcb56355a3ddc7ff7e5ccd6522507999ca7f238" other = "Erreur" [ExclusiveFlagsValidationError] description = "error shown when multiple exclusive flags are provided (group flags, offending flags)" +hash = "sha1-221b98bada52cfc2932f9aa5142b653b46baded6" other = "les options [%v] sont exclusives, mais les options %v ont été fournies" + +[RunHelpTip] +description = "tip shown when a command fails (command path)" +hash = "sha1-e1d2c4cccd484df365c3249347d5172981929b88" +other = "Essayez '%v --help' pour obtenir de l'aide." From d6ed0816a9e8f179fdf61e14b718171fca31adfe Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 05:33:08 +0200 Subject: [PATCH 05/12] feat(i18n): add translations for args validation --- args.go | 14 +++---- localizer.go | 78 ++++++++++++++++++++++++++++++++++++- translations/active.en.toml | 32 +++++++++++++++ translations/active.fr.toml | 39 +++++++++++++++++++ 4 files changed, 155 insertions(+), 8 deletions(-) diff --git a/args.go b/args.go index e79ec33a8..638307336 100644 --- a/args.go +++ b/args.go @@ -33,7 +33,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(i18nLegacyArgsValidationError(), args[0], cmd.CommandPath(), cmd.findSuggestions(args[0])) } return nil } @@ -41,7 +41,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(i18nNoArgsValidationError(), args[0], cmd.CommandPath()) } return nil } @@ -58,7 +58,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(i18nOnlyValidArgsValidationError(), v, cmd.CommandPath(), cmd.findSuggestions(args[0])) } } } @@ -74,7 +74,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(i18nMinimumNArgsValidationError(n), n, len(args)) } return nil } @@ -84,7 +84,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(i18nMaximumNArgsValidationError(n), n, len(args)) } return nil } @@ -94,7 +94,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(i18nExactArgsValidationError(n), n, len(args)) } return nil } @@ -104,7 +104,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(i18nRangeArgsValidationError(max), min, max, len(args)) } return nil } diff --git a/localizer.go b/localizer.go index 9a563eccf..3097eeb57 100644 --- a/localizer.go +++ b/localizer.go @@ -35,6 +35,66 @@ func i18nError() string { }) } +func i18nLegacyArgsValidationError() string { + return localizeMessage(&i18n.Message{ + ID: "LegacyArgsValidationError", + Description: "error shown when args are not understood (subcmd, cmd, suggestion)", + Other: "unknown command %q for %q%s", + }) +} + +func i18nNoArgsValidationError() string { + return localizeMessage(&i18n.Message{ + ID: "NoArgsValidationError", + Description: "error shown when args are present but should not (subcmd, cmd)", + Other: "unknown command %q for %q", + }) +} + +func i18nOnlyValidArgsValidationError() string { + return localizeMessage(&i18n.Message{ + ID: "OnlyValidArgsValidationError", + Description: "error shown when arg is invalid (arg, cmd, suggestion)", + Other: "invalid argument %q for %q%s", + }) +} + +func i18nMinimumNArgsValidationError(amountRequired int) string { + return localizeMessageWithPlural(&i18n.Message{ + ID: "MinimumNArgsValidationError", + Description: "error shown when arg count is too low (expected amount, actual amount)", + Other: "requires at least %d args, only received %d", + One: "requires at least %d arg, only received %d", + }, amountRequired) +} + +func i18nMaximumNArgsValidationError(amountRequired int) string { + return localizeMessageWithPlural(&i18n.Message{ + ID: "MaximumNArgsValidationError", + Description: "error shown when arg count is too low (expected amount, actual amount)", + Other: "accepts at most %d args, received %d", + One: "accepts at most %d arg, received %d", + }, amountRequired) +} + +func i18nExactArgsValidationError(amountRequired int) string { + return localizeMessageWithPlural(&i18n.Message{ + ID: "ExactArgsValidationError", + Description: "error shown when arg count is not exact (expected amount, actual amount)", + Other: "accepts %d args, received %d", + One: "accepts %d arg, received %d", + }, amountRequired) +} + +func i18nRangeArgsValidationError(amountMax int) string { + return localizeMessageWithPlural(&i18n.Message{ + ID: "RangeArgsValidationError", + Description: "error shown when arg count is not in range (expected min, expected max, actual amount)", + Other: "accepts between %d and %d args, received %d", + One: "accepts between %d and %d arg, received %d", + }, amountMax) +} + func i18nRunHelpTip() string { return localizeMessage(&i18n.Message{ ID: "RunHelpTip", @@ -64,6 +124,18 @@ func localizeMessage(message *i18n.Message) string { return localizedValue } +func localizeMessageWithPlural(message *i18n.Message, pluralCount int) string { + localizedValue, err := localizer.Localize(&i18n.LocalizeConfig{ + PluralCount: pluralCount, + DefaultMessage: message, + }) + if err != nil { + return message.Other + } + + return localizedValue +} + func loadTranslationFiles(bundle *i18n.Bundle, langs []string) { for _, lang := range langs { _, _ = bundle.LoadMessageFileFS(localeFS, fmt.Sprintf("translations/active.%s.toml", lang)) @@ -92,7 +164,7 @@ func appendLang(langs *[]string, lang language.Tag) { *langs = append(*langs, langBase.String()) } -func init() { +func setupLocalizer() { bundle := i18n.NewBundle(defaultLanguage) bundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) detectedLangs := detectLangs() @@ -100,3 +172,7 @@ func init() { loadTranslationFiles(bundle, detectedLangs) localizer = i18n.NewLocalizer(bundle, detectedLangs...) } + +func init() { + setupLocalizer() // FIXME: perhaps hook this somewhere else? (not init) +} diff --git a/translations/active.en.toml b/translations/active.en.toml index 8484267e7..9c7405de9 100644 --- a/translations/active.en.toml +++ b/translations/active.en.toml @@ -2,10 +2,42 @@ description = "prefix of error messages" other = "Error" +[ExactArgsValidationError] +description = "error shown when arg count is not exact (expected amount, actual amount)" +one = "accepts %d arg, received %d" +other = "accepts %d args, received %d" + [ExclusiveFlagsValidationError] description = "error shown when multiple exclusive flags are provided (group flags, offending flags)" other = "if any flags in the group [%v] are set none of the others can be; %v were all set" +[LegacyArgsValidationError] +description = "error shown when args are not understood (subcmd, cmd, suggestion)" +other = "unknown command %q for %q%s" + +[MaximumNArgsValidationError] +description = "error shown when arg count is too low (expected amount, actual amount)" +one = "accepts at most %d arg, received %d" +other = "accepts at most %d args, received %d" + +[MinimumNArgsValidationError] +description = "error shown when arg count is too low (expected amount, actual amount)" +one = "requires at least %d arg, only received %d" +other = "requires at least %d args, only received %d" + +[NoArgsValidationError] +description = "error shown when args are present but should not (subcmd, cmd)" +other = "unknown command %q for %q" + +[OnlyValidArgsValidationError] +description = "error shown when arg is invalid (arg, cmd, suggestion)" +other = "invalid argument %q for %q%s" + +[RangeArgsValidationError] +description = "error shown when arg count is not in range (expected min, expected max, actual amount)" +one = "accepts between %d and %d arg, received %d" +other = "accepts between %d and %d args, received %d" + [RunHelpTip] description = "tip shown when a command fails (command path)" other = "Run '%v --help' for usage." diff --git a/translations/active.fr.toml b/translations/active.fr.toml index a869d3323..e6b2005f8 100644 --- a/translations/active.fr.toml +++ b/translations/active.fr.toml @@ -3,11 +3,50 @@ description = "prefix of error messages" hash = "sha1-7dcb56355a3ddc7ff7e5ccd6522507999ca7f238" other = "Erreur" +[ExactArgsValidationError] +description = "error shown when arg count is not exact (expected amount, actual amount)" +hash = "sha1-207d771f1d5dc4ed5c4094dcd29a4c80e31a6260" +one = "accepte %d arg, mais en a reçu %d" +other = "accepte %d args, mais en en reçu %d" + [ExclusiveFlagsValidationError] description = "error shown when multiple exclusive flags are provided (group flags, offending flags)" hash = "sha1-221b98bada52cfc2932f9aa5142b653b46baded6" other = "les options [%v] sont exclusives, mais les options %v ont été fournies" +[LegacyArgsValidationError] +description = "error shown when args are not understood (subcmd, cmd, suggestion)" +hash = "sha1-c601c68bdcb9687109e793112b789b1858953b15" +other = "commande %q inconnue pour %q%s" + +[MaximumNArgsValidationError] +description = "error shown when arg count is too low (expected amount, actual amount)" +hash = "sha1-86d71b8bc054ea8aad7006808bc9d96551052ab4" +one = "accepte au plus %d arg, mais en a reçu %d" +other = "accepte au plus %d args, mais en a reçu %d" + +[MinimumNArgsValidationError] +description = "error shown when arg count is too low (expected amount, actual amount)" +hash = "sha1-7cc9093bf167d6f0601dd0763a8d3f10d71c8889" +one = "requiert au moins %d arg, mais en a reçu %d" +other = "requiert au moins %d args, mais en a reçu %d" + +[NoArgsValidationError] +description = "error shown when args are present but should not (subcmd, cmd)" +hash = "sha1-551d8d237dc2ab9a853fcfbe7ef85318a0f78720" +other = "commande %q inconnue pour %q" + +[OnlyValidArgsValidationError] +description = "error shown when arg is invalid (arg, cmd, suggestion)" +hash = "sha1-60b40e5782dd252c78ef3d585065cb99197ec22e" +other = "argument %q invalide pour %q%s" + +[RangeArgsValidationError] +description = "error shown when arg count is not in range (expected min, expected max, actual amount)" +hash = "sha1-aa81e0aee17a3439b479cdf47169eb194706cd14" +one = "accepte entre %d et %d arg, mais en a reçu %d" +other = "accepte entre %d et %d args, mais en a reçu %d" + [RunHelpTip] description = "tip shown when a command fails (command path)" hash = "sha1-e1d2c4cccd484df365c3249347d5172981929b88" From 0abea784c4da9dd4489a50a48e15085d2aae8c96 Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 09:04:29 +0200 Subject: [PATCH 06/12] feat(i18n): translate the usage template as well This should keep backwards compat'. --- args_test.go | 8 +-- command.go | 21 ++++--- localizer.go | 113 ++++++++++++++++++++++++++++++++++++ translations/active.en.toml | 40 +++++++++++++ translations/active.fr.toml | 50 ++++++++++++++++ 5 files changed, 219 insertions(+), 13 deletions(-) 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/command.go b/command.go index 237d2238b..92b659b40 100644 --- a/command.go +++ b/command.go @@ -252,6 +252,8 @@ type Command struct { // SuggestionsMinimumDistance defines minimum levenshtein distance to display suggestions. // Must be > 0. SuggestionsMinimumDistance int + + I18n *i18nCommandGlossary } // Context returns underlying command context. If command was executed @@ -542,6 +544,7 @@ func (c *Command) NamePadding() int { // UsageTemplate returns usage template for the command. func (c *Command) UsageTemplate() string { + c.I18n = getCommandGlossary() if c.usageTemplate != "" { return c.usageTemplate } @@ -549,35 +552,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}} ` } diff --git a/localizer.go b/localizer.go index 3097eeb57..1a13d546a 100644 --- a/localizer.go +++ b/localizer.go @@ -27,6 +27,119 @@ var localeFS embed.FS // Localizer can be used to fetch localized messages var localizer *i18n.Localizer +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: i18nSectionUsage(), + SectionAliases: i18nSectionAliases(), + SectionExamples: i18nSectionExamples(), + SectionAvailableCommands: i18nSectionAvailableCommands(), + SectionAdditionalCommands: i18nSectionAdditionalCommands(), + SectionFlags: i18nSectionFlags(), + SectionGlobalFlags: i18nSectionGlobalFlags(), + SectionAdditionalHelpTopics: i18nSectionAdditionalHelpTopics(), + Use: i18nUse(), + ForInfoAboutCommand: i18nForInfoAboutCommand(), + } + } + return commonCommandGlossary +} + +func i18nSectionUsage() string { + return localizeMessage(&i18n.Message{ + ID: "SectionUsage", + Description: "title of the section in the usage template", + Other: "Usage", + }) +} + +func i18nSectionAliases() string { + return localizeMessage(&i18n.Message{ + ID: "SectionAliases", + Description: "title of the section in the usage template", + Other: "Aliases", + }) +} + +func i18nSectionExamples() string { + return localizeMessage(&i18n.Message{ + ID: "SectionExamples", + Description: "title of the section in the usage template", + Other: "Examples", + }) +} + +func i18nSectionAvailableCommands() string { + return localizeMessage(&i18n.Message{ + ID: "SectionAvailableCommands", + Description: "title of the section in the usage template", + Other: "Available Commands", + }) +} + +func i18nSectionAdditionalCommands() string { + return localizeMessage(&i18n.Message{ + ID: "SectionAdditionalCommands", + Description: "title of the section in the usage template", + Other: "Additional Commands", + }) +} + +func i18nSectionFlags() string { + return localizeMessage(&i18n.Message{ + ID: "SectionFlags", + Description: "title of the section in the usage template", + Other: "Flags", + }) +} + +func i18nSectionGlobalFlags() string { + return localizeMessage(&i18n.Message{ + ID: "SectionGlobalFlags", + Description: "title of the section in the usage template", + Other: "Global Flags", + }) +} + +func i18nSectionAdditionalHelpTopics() string { + return localizeMessage(&i18n.Message{ + ID: "SectionAdditionalHelpTopics", + Description: "title of the section in the usage template", + Other: "Additional Help Topics", + }) +} + +func i18nUse() string { + return localizeMessage(&i18n.Message{ + ID: "Use", + Description: "beginning of a sentence like 'Use to do '", + Other: "Use", + }) +} + +func i18nForInfoAboutCommand() string { + return localizeMessage(&i18n.Message{ + ID: "ForInfoAboutCommand", + Description: "end of a sentence", + Other: "for more information about a command", + }) +} + func i18nError() string { return localizeMessage(&i18n.Message{ ID: "Error", diff --git a/translations/active.en.toml b/translations/active.en.toml index 9c7405de9..605656a3f 100644 --- a/translations/active.en.toml +++ b/translations/active.en.toml @@ -11,6 +11,10 @@ other = "accepts %d args, received %d" description = "error shown when multiple exclusive flags are provided (group flags, offending flags)" other = "if any flags in the group [%v] are set none of the others can be; %v were all set" +[ForInfoAboutCommand] +description = "end of a sentence" +other = "for more information about a command" + [LegacyArgsValidationError] description = "error shown when args are not understood (subcmd, cmd, suggestion)" other = "unknown command %q for %q%s" @@ -41,3 +45,39 @@ other = "accepts between %d and %d args, received %d" [RunHelpTip] description = "tip shown when a command fails (command path)" other = "Run '%v --help' for usage." + +[SectionAdditionalCommands] +description = "title of the section in the usage template" +other = "Additional Commands" + +[SectionAdditionalHelpTopics] +description = "title of the section in the usage template" +other = "Additional Help Topics" + +[SectionAliases] +description = "title of the section in the usage template" +other = "Aliases" + +[SectionAvailableCommands] +description = "title of the section in the usage template" +other = "Available Commands" + +[SectionExamples] +description = "title of the section in the usage template" +other = "Examples" + +[SectionFlags] +description = "title of the section in the usage template" +other = "Flags" + +[SectionGlobalFlags] +description = "title of the section in the usage template" +other = "Global Flags" + +[SectionUsage] +description = "title of the section in the usage template" +other = "Usage" + +[Use] +description = "beginning of a sentence like 'Use to do '" +other = "Use" diff --git a/translations/active.fr.toml b/translations/active.fr.toml index e6b2005f8..701dba147 100644 --- a/translations/active.fr.toml +++ b/translations/active.fr.toml @@ -14,6 +14,11 @@ description = "error shown when multiple exclusive flags are provided (group fla hash = "sha1-221b98bada52cfc2932f9aa5142b653b46baded6" other = "les options [%v] sont exclusives, mais les options %v ont été fournies" +[ForInfoAboutCommand] +description = "end of a sentence" +hash = "sha1-923f6d48908b3a6981fb5504b0e5078700a312c0" +other = "pour plus d'information au sujet d'une commande" + [LegacyArgsValidationError] description = "error shown when args are not understood (subcmd, cmd, suggestion)" hash = "sha1-c601c68bdcb9687109e793112b789b1858953b15" @@ -51,3 +56,48 @@ other = "accepte entre %d et %d args, mais en a reçu %d" description = "tip shown when a command fails (command path)" hash = "sha1-e1d2c4cccd484df365c3249347d5172981929b88" other = "Essayez '%v --help' pour obtenir de l'aide." + +[SectionAdditionalCommands] +description = "title of the section in the usage template" +hash = "sha1-730484f22c7ce0885ca13a3b8222b231dc29a4a9" +other = "Commandes Connexes" + +[SectionAdditionalHelpTopics] +description = "title of the section in the usage template" +hash = "sha1-615c9551f4dcd30836fbe04506e0b807c59dd901" +other = "Autres Sujets" + +[SectionAliases] +description = "title of the section in the usage template" +hash = "sha1-3c66c036f454cb3d23f757c430a5c05b148dc01f" +other = "Alias" + +[SectionAvailableCommands] +description = "title of the section in the usage template" +hash = "sha1-fc33c38777373e51c9c84c8594efb5de4df0f23e" +other = "Commandes Disponibles" + +[SectionExamples] +description = "title of the section in the usage template" +hash = "sha1-6e2f0c7dddc3ce6aacb1184b3b21694492d6595a" +other = "Exemples" + +[SectionFlags] +description = "title of the section in the usage template" +hash = "sha1-5513ed25ed4e1ad2e569ff4c963bba347320e9f7" +other = "Options" + +[SectionGlobalFlags] +description = "title of the section in the usage template" +hash = "sha1-705f1ebcdce1374e23f41bc8e0e14e5197ac1742" +other = "Options Globales" + +[SectionUsage] +description = "title of the section in the usage template" +hash = "sha1-5fdae7bd171315a453063384887ee4300363cf20" +other = "Usage" + +[Use] +description = "beginning of a sentence like 'Use to do '" +hash = "sha1-f42c92acc9934eec952161dfa338a95ec0f02b75" +other = "Utilisez" From ca6ece94dff0c821ebe36dbe46b43b0dd0a8b53b Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 10:33:43 +0200 Subject: [PATCH 07/12] feat(i18n): translate more user-facing messages --- command.go | 23 +++++----- localizer.go | 90 +++++++++++++++++++++++++++++++++++++ translations/active.en.toml | 45 +++++++++++++++++++ translations/active.fr.toml | 56 +++++++++++++++++++++++ 4 files changed, 202 insertions(+), 12 deletions(-) diff --git a/command.go b/command.go index 92b659b40..595b5b5f2 100644 --- a/command.go +++ b/command.go @@ -759,7 +759,7 @@ func (c *Command) findSuggestions(arg string) string { } suggestionsString := "" if suggestions := c.SuggestionsFor(arg); len(suggestions) > 0 { - suggestionsString += "\n\nDid you mean this?\n" + suggestionsString += "\n\n" + i18nDidYouMeanThis() + "\n" for _, s := range suggestions { suggestionsString += fmt.Sprintf("\t%v\n", s) } @@ -880,7 +880,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(i18nCommandDeprecatedWarning()+"\n", c.Name(), c.Deprecated) } // initialize help and version flag at the last point possible to allow for user @@ -1165,7 +1165,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(i18nFlagNotSetError(len(missingFlagNames)), strings.Join(missingFlagNames, `", "`)) } return nil } @@ -1189,9 +1189,9 @@ func (c *Command) checkCommandGroups() { func (c *Command) InitDefaultHelpFlag() { c.mergePersistentFlags() if c.Flags().Lookup("help") == nil { - usage := "help for " + usage := i18nHelpFor() + " " if c.Name() == "" { - usage += "this command" + usage += i18nThisCommand() } else { usage += c.Name() } @@ -1211,9 +1211,9 @@ func (c *Command) InitDefaultVersionFlag() { c.mergePersistentFlags() if c.Flags().Lookup("version") == nil { - usage := "version for " + usage := i18nVersionFor() + " " if c.Name() == "" { - usage += "this command" + usage += i18nThisCommand() } else { usage += c.Name() } @@ -1236,10 +1236,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]", i18nCommand()), + Short: i18nCommandHelpShort(), + Long: fmt.Sprintf(i18nCommandHelpLong(), c.Name()+fmt.Sprintf(" help [%s]", i18nPathToCommand())), ValidArgsFunction: func(c *Command, args []string, toComplete string) ([]string, ShellCompDirective) { var completions []string cmd, _, e := c.Root().Find(args) @@ -1262,7 +1261,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(i18nCommandHelpUnknownTopicError()+"\n", args) CheckErr(c.Root().Usage()) } else { cmd.InitDefaultHelpFlag() // make possible 'help' flag to be shown diff --git a/localizer.go b/localizer.go index 1a13d546a..c1971869f 100644 --- a/localizer.go +++ b/localizer.go @@ -140,6 +140,87 @@ func i18nForInfoAboutCommand() string { }) } +func i18nCommand() string { + return localizeMessage(&i18n.Message{ + ID: "Command", + Description: "lowercase", + Other: "command", + }) +} + +func i18nPathToCommand() string { + return localizeMessage(&i18n.Message{ + ID: "PathToCommand", + Description: "lowercase", + Other: "path to command", + }) +} + +func i18nCommandHelpShort() string { + return localizeMessage(&i18n.Message{ + ID: "CommandHelpShort", + Description: "short help for command help", + Other: "Help about any command", + }) +} + +func i18nCommandHelpLong() string { + return localizeMessage(&i18n.Message{ + ID: "CommandHelpLong", + Description: "long help for command help (cmd example)", + Other: `Help provides help for any command in the application. +Simply type %s for full details.`, + }) +} + +func i18nCommandHelpUnknownTopicError() string { + return localizeMessage(&i18n.Message{ + ID: "CommandHelpUnknownTopicError", + Description: "shown when help topic is unknown (args)", + Other: "Unknown help topic %#q", + }) +} + +func i18nHelpFor() string { + return localizeMessage(&i18n.Message{ + ID: "HelpFor", + Description: "lowercase, beginning of sentence", + Other: "help for", + }) +} + +func i18nVersionFor() string { + return localizeMessage(&i18n.Message{ + ID: "VersionFor", + Description: "lowercase, beginning of sentence", + Other: "version for", + }) +} + +func i18nThisCommand() string { + return localizeMessage(&i18n.Message{ + ID: "ThisCommand", + Description: "lowercase, end of sentence, used when command name is undefined", + Other: "this command", + }) +} + +func i18nDidYouMeanThis() string { + return localizeMessage(&i18n.Message{ + ID: "DidYouMeanThis", + Description: "shown as suggestion", + Other: "Did you mean this?", + }) +} + +func i18nCommandDeprecatedWarning() string { + return localizeMessage(&i18n.Message{ + ID: "CommandDeprecatedWarning", + Description: "printed when a deprecated command is executed (cmd, deprecation message)", + Other: "Command %q is deprecated, %s", + }) +} + func i18nError() string { return localizeMessage(&i18n.Message{ ID: "Error", @@ -224,6 +305,15 @@ func i18nExclusiveFlagsValidationError() string { }) } +func i18nFlagNotSetError(amountFlags int) string { + return localizeMessageWithPlural(&i18n.Message{ + ID: "FlagNotSetError", + Description: "error shown when required flags are not set (flags)", + Other: "required flags \"%s\" are not set", + One: "required flag \"%s\" is not set", + }, amountFlags) +} + // … lots more translations here func localizeMessage(message *i18n.Message) string { diff --git a/translations/active.en.toml b/translations/active.en.toml index 605656a3f..b1dfac281 100644 --- a/translations/active.en.toml +++ b/translations/active.en.toml @@ -1,3 +1,27 @@ +[Command] +description = "lowercase" +other = "command" + +[CommandDeprecatedWarning] +description = "printed when a deprecated command is executed (cmd, deprecation message)" +other = "Command %q is deprecated, %s" + +[CommandHelpLong] +description = "long help for command help (cmd example)" +other = "Help provides help for any command in the application.\nSimply type %s for full details." + +[CommandHelpShort] +description = "short help for command help" +other = "Help about any command" + +[CommandHelpUnknownTopicError] +description = "shown when help topic is unknown (args)" +other = "Unknown help topic %#q" + +[DidYouMeanThis] +description = "shown as suggestion" +other = "Did you mean this?" + [Error] description = "prefix of error messages" other = "Error" @@ -11,10 +35,19 @@ other = "accepts %d args, received %d" description = "error shown when multiple exclusive flags are provided (group flags, offending flags)" other = "if any flags in the group [%v] are set none of the others can be; %v were all set" +[FlagNotSetError] +description = "error shown when required flags are not set (flags)" +one = "required flag \"%s\" is not set" +other = "required flags \"%s\" are not set" + [ForInfoAboutCommand] description = "end of a sentence" other = "for more information about a command" +[HelpFor] +description = "lowercase, beginning of sentence" +other = "help for" + [LegacyArgsValidationError] description = "error shown when args are not understood (subcmd, cmd, suggestion)" other = "unknown command %q for %q%s" @@ -37,6 +70,10 @@ other = "unknown command %q for %q" description = "error shown when arg is invalid (arg, cmd, suggestion)" other = "invalid argument %q for %q%s" +[PathToCommand] +description = "lowercase" +other = "path to command" + [RangeArgsValidationError] description = "error shown when arg count is not in range (expected min, expected max, actual amount)" one = "accepts between %d and %d arg, received %d" @@ -78,6 +115,14 @@ other = "Global Flags" description = "title of the section in the usage template" other = "Usage" +[ThisCommand] +description = "lowercase, end of sentence, used when command name is undefined" +other = "this command" + [Use] description = "beginning of a sentence like 'Use to do '" other = "Use" + +[VersionFor] +description = "lowercase, beginning of sentence" +other = "version for" diff --git a/translations/active.fr.toml b/translations/active.fr.toml index 701dba147..1dd8539fc 100644 --- a/translations/active.fr.toml +++ b/translations/active.fr.toml @@ -1,3 +1,33 @@ +[Command] +description = "lowercase" +hash = "sha1-c90bde9bb93ee6452afbe579d55cd32ba0159c8d" +other = "commande" + +[CommandDeprecatedWarning] +description = "printed when a deprecated command is executed (cmd, deprecation message)" +hash = "sha1-e8b974a255eafe86d4a4fe15a0e79aaab345354c" +other = "La commande %q est dépréciée, %s" + +[CommandHelpLong] +description = "long help for command help (cmd example)" +hash = "sha1-dc993d9f12d54910147c477318fa82c8210b6669" +other = "Help fournit de l'aide pour n'importe quelle commande de l'application.\nTapez '%s' pour obtenir une aide détaillée." + +[CommandHelpShort] +description = "short help for command help" +hash = "sha1-5bab4a463f36ea8e33681f523445bbd614e9891c" +other = "Obtenir de l'aide au sujet d'une commande" + +[CommandHelpUnknownTopicError] +description = "shown when help topic is unknown (args)" +hash = "sha1-d3b6cb615eee5770e0e8e62fbf7a3293502dd7a8" +other = "Sujet d'aide %#q inconnu" + +[DidYouMeanThis] +description = "shown as suggestion" +hash = "sha1-eb9504fe384499caafe690853204c7fb7ac8b531" +other = "Vouliez-vous dire ceci ?" + [Error] description = "prefix of error messages" hash = "sha1-7dcb56355a3ddc7ff7e5ccd6522507999ca7f238" @@ -14,11 +44,22 @@ description = "error shown when multiple exclusive flags are provided (group fla hash = "sha1-221b98bada52cfc2932f9aa5142b653b46baded6" other = "les options [%v] sont exclusives, mais les options %v ont été fournies" +[FlagNotSetError] +description = "error shown when required flags are not set (flags)" +hash = "sha1-f5340c4bfceb0066ec70944b57e6458b6f8cc97b" +one = "l'option requise \"%s\" n'est pas présente" +other = "les options requises \"%s\" ne sont pas présentes" + [ForInfoAboutCommand] description = "end of a sentence" hash = "sha1-923f6d48908b3a6981fb5504b0e5078700a312c0" other = "pour plus d'information au sujet d'une commande" +[HelpFor] +description = "lowercase, beginning of sentence" +hash = "sha1-18fbe8d2780935dbe6a71f998821c22f283cbd25" +other = "aide pour" + [LegacyArgsValidationError] description = "error shown when args are not understood (subcmd, cmd, suggestion)" hash = "sha1-c601c68bdcb9687109e793112b789b1858953b15" @@ -46,6 +87,11 @@ description = "error shown when arg is invalid (arg, cmd, suggestion)" hash = "sha1-60b40e5782dd252c78ef3d585065cb99197ec22e" other = "argument %q invalide pour %q%s" +[PathToCommand] +description = "lowercase" +hash = "sha1-7dcca04d5dcb7ff289a5483bea3e4f1e4119c1f4" +other = "commande" + [RangeArgsValidationError] description = "error shown when arg count is not in range (expected min, expected max, actual amount)" hash = "sha1-aa81e0aee17a3439b479cdf47169eb194706cd14" @@ -97,7 +143,17 @@ description = "title of the section in the usage template" hash = "sha1-5fdae7bd171315a453063384887ee4300363cf20" other = "Usage" +[ThisCommand] +description = "lowercase, end of sentence, used when command name is undefined" +hash = "sha1-5cfa4629fdf18b89471b3d07a0d051a8a10afe70" +other = "cette commande" + [Use] description = "beginning of a sentence like 'Use to do '" hash = "sha1-f42c92acc9934eec952161dfa338a95ec0f02b75" other = "Utilisez" + +[VersionFor] +description = "lowercase, beginning of sentence" +hash = "sha1-e7f03028ab18076d111cd3343a24bbd8113d5431" +other = "version de" From 64f5c4f4052364e91e658d87758d07d46c27d541 Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 10:37:26 +0200 Subject: [PATCH 08/12] fix(i18n): test assertions and plurality --- command_test.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/command_test.go b/command_test.go index 9f686d65e..98c7ec230 100644 --- a/command_test.go +++ b/command_test.go @@ -823,7 +823,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 +850,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 { From 102ab5b544df4c6f520248e4a7c471bee0c39340 Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 10:40:59 +0200 Subject: [PATCH 09/12] fix(i18n): add a test for plurality of required flags --- command_test.go | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/command_test.go b/command_test.go index 98c7ec230..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", "", "") From 775014c459a724f92161edea6eee663a3438a6bd Mon Sep 17 00:00:00 2001 From: Goutte Date: Tue, 4 Apr 2023 11:39:07 +0200 Subject: [PATCH 10/12] fix(i18n): remove an ugly hack --- command.go | 9 +++++---- localizer.go | 5 +++++ 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/command.go b/command.go index 595b5b5f2..406106a85 100644 --- a/command.go +++ b/command.go @@ -252,8 +252,6 @@ type Command struct { // SuggestionsMinimumDistance defines minimum levenshtein distance to display suggestions. // Must be > 0. SuggestionsMinimumDistance int - - I18n *i18nCommandGlossary } // Context returns underlying command context. If command was executed @@ -434,7 +432,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) } @@ -544,7 +546,6 @@ func (c *Command) NamePadding() int { // UsageTemplate returns usage template for the command. func (c *Command) UsageTemplate() string { - c.I18n = getCommandGlossary() if c.usageTemplate != "" { return c.usageTemplate } diff --git a/localizer.go b/localizer.go index c1971869f..3d2085830 100644 --- a/localizer.go +++ b/localizer.go @@ -40,6 +40,11 @@ type i18nCommandGlossary struct { ForInfoAboutCommand string } +type CommandUsageTemplateData struct { + *Command + I18n *i18nCommandGlossary +} + var commonCommandGlossary *i18nCommandGlossary func getCommandGlossary() *i18nCommandGlossary { From cdb680ac1ebb361171d1684b7bffb41df749c3e5 Mon Sep 17 00:00:00 2001 From: Goutte Date: Wed, 5 Apr 2023 05:55:48 +0200 Subject: [PATCH 11/12] fix(i18n): lint, review and clean up a little --- Makefile | 2 ++ localizer.go | 12 ++++++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index 7098c2d8f..c44bcb2e4 100644 --- a/Makefile +++ b/Makefile @@ -35,8 +35,10 @@ clean: rm -rf $(BIN) i18n_extract: + $(info ******************** extracting translation files ********************) goi18n extract -outdir translations goi18n merge -outdir translations translations/* i18n_merge: + $(info ******************** merging translation files ********************) goi18n merge -outdir translations translations/* diff --git a/localizer.go b/localizer.go index 3d2085830..8f2319a62 100644 --- a/localizer.go +++ b/localizer.go @@ -3,10 +3,11 @@ package cobra import ( "embed" "fmt" + "os" + "github.com/BurntSushi/toml" "github.com/nicksnyder/go-i18n/v2/i18n" "golang.org/x/text/language" - "os" ) var defaultLanguage = language.English @@ -366,10 +367,13 @@ func detectLangs() []string { func appendLang(langs *[]string, lang language.Tag) { langString := lang.String() - langBase, _ := lang.Base() *langs = append(*langs, langString) - *langs = append(*langs, langBase.ISO3()) - *langs = append(*langs, langBase.String()) + + langBase, confidentInBase := lang.Base() + if confidentInBase != language.No { + *langs = append(*langs, langBase.String()) + *langs = append(*langs, langBase.ISO3()) + } } func setupLocalizer() { From 9d4b134da10774d1ade19b746b7c18cce59406ac Mon Sep 17 00:00:00 2001 From: Goutte Date: Sun, 5 Nov 2023 00:34:23 +0100 Subject: [PATCH 12/12] chore: update go.sum with so much bloat --- go.sum | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/go.sum b/go.sum index 871c3a8af..03640cdbb 100644 --- a/go.sum +++ b/go.sum @@ -1,12 +1,43 @@ +github.com/BurntSushi/toml v1.0.0 h1:dtDWrepsVPfW9H/4y7dDgFc2MBUSeJhlaDtK13CxFlU= +github.com/BurntSushi/toml v1.0.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= 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/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA= +github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0= 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.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=