diff --git a/.github/workflows/cli.yml b/.github/workflows/cli.yml index a2a7df6a5d..c97e884603 100644 --- a/.github/workflows/cli.yml +++ b/.github/workflows/cli.yml @@ -25,6 +25,8 @@ jobs: - name: Set PATH run: echo "${GITHUB_WORKSPACE}/.local/bin" >>"${GITHUB_PATH}" - uses: actions/checkout@v3 + - run: make ensure-gotext + - run: make go-generate - if: matrix.go == '1.20.x' && matrix.os == 'ubuntu-latest' run: make ensure-goimports - if: matrix.go == '1.20.x' && matrix.os == 'ubuntu-latest' diff --git a/Makefile b/Makefile index e9e13ff545..dbb4848daa 100644 --- a/Makefile +++ b/Makefile @@ -7,7 +7,7 @@ GO_RUN_BUILD := go run internal/build/build.go .PHONY: all -all: generate vet test check-binary-size gfmrun +all: generate go-generate vet test check-binary-size gfmrun # NOTE: this is a special catch-all rule to run any of the commands # defined in internal/build/build.go with optional arguments passed diff --git a/command.go b/command.go index b032239153..84466e80eb 100644 --- a/command.go +++ b/command.go @@ -384,7 +384,7 @@ func (cmd *Command) Run(ctx context.Context, osArgs []string) (deferErr error) { err = cmd.handleExitCoder(ctx, err) return err } - fmt.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error()) + mprinter.Fprintf(cmd.Root().ErrWriter, "Incorrect Usage: %s\n\n", err.Error()) if cmd.Suggest { if suggestion, err := cmd.suggestFlagFromError(err, ""); err == nil { fmt.Fprintf(cmd.Root().ErrWriter, "%s", suggestion) diff --git a/completion.go b/completion.go index 2319a815c9..d7a4ba21e0 100644 --- a/completion.go +++ b/completion.go @@ -3,7 +3,6 @@ package cli import ( "context" "embed" - "fmt" "sort" ) @@ -51,12 +50,12 @@ func completionCommandAction(ctx context.Context, cmd *Command) error { sort.Strings(shells) if cmd.Args().Len() == 0 { - return Exit(fmt.Sprintf("no shell provided for completion command. available shells are %+v", shells), 1) + return Exit(mprinter.Sprintf("no shell provided for completion command. available shells are %+v", shells), 1) } s := cmd.Args().First() if rc, ok := shellCompletions[s]; !ok { - return Exit(fmt.Sprintf("unknown shell %s, available shells are %+v", s, shells), 1) + return Exit(mprinter.Sprintf("unknown shell %s, available shells are %+v", s, shells), 1) } else if c, err := rc(cmd); err != nil { return Exit(err, 1) } else { diff --git a/go.mod b/go.mod index 34b9ceda5b..4db9e78b75 100644 --- a/go.mod +++ b/go.mod @@ -10,5 +10,6 @@ require ( require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/text v0.10.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index b806593b77..3c5f092f3e 100644 --- a/go.sum +++ b/go.sum @@ -6,6 +6,8 @@ github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcU github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 h1:bAn7/zixMGCfxrRTfdpNzjtPYqr8smhKouy9mxVdGPU= github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673/go.mod h1:N3UwUGtsrSj3ccvlPHLoLsHnpR27oXr4ZE984MbSER8= +golang.org/x/text v0.10.0 h1:UpjohKhiEgNc0CSauXmwYftY1+LlaC75SJwh0SgCX58= +golang.org/x/text v0.10.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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/help.go b/help.go index 2ee9f7658c..e5a6cc1dbd 100644 --- a/help.go +++ b/help.go @@ -318,7 +318,7 @@ func ShowVersion(cmd *Command) { } func printVersion(cmd *Command) { - _, _ = fmt.Fprintf(cmd.Root().Writer, "%v version %v\n", cmd.Name, cmd.Version) + _, _ = mprinter.Fprintf(cmd.Root().Writer, "%v version %v\n", cmd.Name, cmd.Version) } func handleTemplateError(err error) { diff --git a/i18n.go b/i18n.go new file mode 100644 index 0000000000..be280bd34b --- /dev/null +++ b/i18n.go @@ -0,0 +1,13 @@ +package cli + +import ( + ts "github.com/urfave/cli/v3/internal/translations" + "golang.org/x/text/message" +) + +var mprinter *message.Printer + +func init() { + // TBD add language detection logic and fallback to en-US + mprinter = message.NewPrinter(message.MatchLanguage("en-US"), message.Catalog(ts.Catalog)) +} diff --git a/internal/build/build.go b/internal/build/build.go index 48b809be00..3e6bd841ed 100644 --- a/internal/build/build.go +++ b/internal/build/build.go @@ -64,6 +64,10 @@ func main() { Name: "vet", Action: topRunAction("go", "vet", "./..."), }, + { + Name: "go-generate", + Action: topRunAction("go", "generate", "./..."), + }, { Name: "test", Action: TestActionFunc, @@ -95,6 +99,10 @@ func main() { Name: "ensure-goimports", Action: EnsureGoimportsActionFunc, }, + { + Name: "ensure-gotext", + Action: EnsureGoTextActionFunc, + }, { Name: "ensure-gfmrun", Action: EnsureGfmrunActionFunc, @@ -535,6 +543,17 @@ func EnsureGoimportsActionFunc(ctx context.Context, cmd *cli.Command) error { return runCmd(ctx, "go", "install", "golang.org/x/tools/cmd/goimports@latest") } +func EnsureGoTextActionFunc(ctx context.Context, cmd *cli.Command) error { + top := cmd.String("top") + if err := os.Chdir(top); err != nil { + return err + } + + os.Setenv("GOBIN", filepath.Join(top, ".local/bin")) + + return runCmd(ctx, "go", "install", "golang.org/x/text/cmd/gotext@latest") +} + func EnsureGfmrunActionFunc(ctx context.Context, cmd *cli.Command) error { top := cmd.String("top") gfmrunExe := filepath.Join(top, ".local/bin/gfmrun") diff --git a/internal/translations/bcatalog.go b/internal/translations/bcatalog.go new file mode 100644 index 0000000000..d13e3b2768 --- /dev/null +++ b/internal/translations/bcatalog.go @@ -0,0 +1,12 @@ +package translations + +import ( + "golang.org/x/text/message" + "golang.org/x/text/message/catalog" +) + +var oldCatalog catalog.Catalog + +func init() { + oldCatalog = message.DefaultCatalog +} diff --git a/internal/translations/catalog.go b/internal/translations/catalog.go new file mode 100644 index 0000000000..9990768356 --- /dev/null +++ b/internal/translations/catalog.go @@ -0,0 +1,70 @@ +// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. + +package translations + +import ( + "golang.org/x/text/language" + "golang.org/x/text/message" + "golang.org/x/text/message/catalog" +) + +type dictionary struct { + index []uint32 + data string +} + +func (d *dictionary) Lookup(key string) (data string, ok bool) { + p, ok := messageKeyToIndex[key] + if !ok { + return "", false + } + start, end := d.index[p], d.index[p+1] + if start == end { + return "", false + } + return d.data[start:end], true +} + +func init() { + dict := map[string]catalog.Dictionary{ + "en_GB": &dictionary{index: en_GBIndex, data: en_GBData}, + "en_US": &dictionary{index: en_USIndex, data: en_USData}, + } + fallback := language.MustParse("en-US") + cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback)) + if err != nil { + panic(err) + } + message.DefaultCatalog = cat +} + +var messageKeyToIndex = map[string]int{ + "%v version %v\n": 3, + "Incorrect Usage: %s\n\n": 0, + "no shell provided for completion command. available shells are %+v": 1, + "unknown shell %s, available shells are %+v": 2, +} + +var en_GBIndex = []uint32{ // 5 elements + 0x00000000, 0x0000001d, 0x00000063, 0x00000094, + 0x000000ad, +} // Size: 44 bytes + +const en_GBData string = "" + // Size: 173 bytes + "\x04\x00\x02\x0a\x0a\x17\x02Incorrect Usage: %[1]s\x02no shell provided " + + "for completion command. available shells are %+[1]v\x02unknown shell %[1" + + "]s, available shells are %+[2]v\x04\x00\x01\x0a\x14\x02%[1]v version %[2" + + "]v" + +var en_USIndex = []uint32{ // 5 elements + 0x00000000, 0x0000001d, 0x00000063, 0x00000094, + 0x000000ad, +} // Size: 44 bytes + +const en_USData string = "" + // Size: 173 bytes + "\x04\x00\x02\x0a\x0a\x17\x02Incorrect Usage: %[1]s\x02no shell provided " + + "for completion command. available shells are %+[1]v\x02unknown shell %[1" + + "]s, available shells are %+[2]v\x04\x00\x01\x0a\x14\x02%[1]v version %[2" + + "]v" + + // Total table size 434 bytes (0KiB); checksum: F631BB3 diff --git a/internal/translations/dcatalog.go b/internal/translations/dcatalog.go new file mode 100644 index 0000000000..ac53e3609f --- /dev/null +++ b/internal/translations/dcatalog.go @@ -0,0 +1,13 @@ +package translations + +import ( + "golang.org/x/text/message" + "golang.org/x/text/message/catalog" +) + +var Catalog catalog.Catalog + +func init() { + Catalog = message.DefaultCatalog + message.DefaultCatalog = oldCatalog +} diff --git a/internal/translations/locales/en-CA/out.gotext.json b/internal/translations/locales/en-CA/out.gotext.json new file mode 100644 index 0000000000..3e1acfbb42 --- /dev/null +++ b/internal/translations/locales/en-CA/out.gotext.json @@ -0,0 +1,89 @@ +{ + "language": "en-CA", + "messages": [ + { + "id": "{Incorrect_Usage} {Error}", + "message": "{Incorrect_Usage} {Error}", + "translation": "", + "placeholders": [ + { + "id": "Incorrect_Usage", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "\"Incorrect Usage:\"" + }, + { + "id": "Error", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "err.Error()" + } + ] + }, + { + "id": "no shell provided for completion command. available shells are {Shells}", + "message": "no shell provided for completion command. available shells are {Shells}", + "translation": "", + "placeholders": [ + { + "id": "Shells", + "string": "%+[1]v", + "type": "[]string", + "underlyingType": "[]string", + "argNum": 1, + "expr": "shells" + } + ] + }, + { + "id": "unknown shell {S}, available shells are {Shells}", + "message": "unknown shell {S}, available shells are {Shells}", + "translation": "", + "placeholders": [ + { + "id": "S", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "s" + }, + { + "id": "Shells", + "string": "%+[2]v", + "type": "[]string", + "underlyingType": "[]string", + "argNum": 2, + "expr": "shells" + } + ] + }, + { + "id": "{Name} version {Version}", + "message": "{Name} version {Version}", + "translation": "", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "cCtx.Command.Name" + }, + { + "id": "Version", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "cCtx.Command.Version" + } + ] + } + ] +} \ No newline at end of file diff --git a/internal/translations/locales/en-GB/messages.gotext.json b/internal/translations/locales/en-GB/messages.gotext.json new file mode 100644 index 0000000000..56e8998d76 --- /dev/null +++ b/internal/translations/locales/en-GB/messages.gotext.json @@ -0,0 +1,89 @@ +{ + "language": "en-GB", + "messages": [ + { + "id": "Incorrect Usage: {Error}", + "message": "Incorrect Usage: {Error}", + "translation": "Incorrect Usage: {Error}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Error", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "err.Error()" + } + ], + "fuzzy": true + }, + { + "id": "no shell provided for completion command. available shells are {Shells}", + "message": "no shell provided for completion command. available shells are {Shells}", + "translation": "no shell provided for completion command. available shells are {Shells}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Shells", + "string": "%+[1]v", + "type": "[]string", + "underlyingType": "[]string", + "argNum": 1, + "expr": "shells" + } + ], + "fuzzy": true + }, + { + "id": "unknown shell {S}, available shells are {Shells}", + "message": "unknown shell {S}, available shells are {Shells}", + "translation": "unknown shell {S}, available shells are {Shells}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "S", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "s" + }, + { + "id": "Shells", + "string": "%+[2]v", + "type": "[]string", + "underlyingType": "[]string", + "argNum": 2, + "expr": "shells" + } + ], + "fuzzy": true + }, + { + "id": "{Name} version {Version}", + "message": "{Name} version {Version}", + "translation": "{Name} version {Version}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "cmd.Name" + }, + { + "id": "Version", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "cmd.Version" + } + ], + "fuzzy": true + } + ] +} \ No newline at end of file diff --git a/internal/translations/locales/en-GB/out.gotext.json b/internal/translations/locales/en-GB/out.gotext.json new file mode 100644 index 0000000000..56e8998d76 --- /dev/null +++ b/internal/translations/locales/en-GB/out.gotext.json @@ -0,0 +1,89 @@ +{ + "language": "en-GB", + "messages": [ + { + "id": "Incorrect Usage: {Error}", + "message": "Incorrect Usage: {Error}", + "translation": "Incorrect Usage: {Error}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Error", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "err.Error()" + } + ], + "fuzzy": true + }, + { + "id": "no shell provided for completion command. available shells are {Shells}", + "message": "no shell provided for completion command. available shells are {Shells}", + "translation": "no shell provided for completion command. available shells are {Shells}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Shells", + "string": "%+[1]v", + "type": "[]string", + "underlyingType": "[]string", + "argNum": 1, + "expr": "shells" + } + ], + "fuzzy": true + }, + { + "id": "unknown shell {S}, available shells are {Shells}", + "message": "unknown shell {S}, available shells are {Shells}", + "translation": "unknown shell {S}, available shells are {Shells}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "S", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "s" + }, + { + "id": "Shells", + "string": "%+[2]v", + "type": "[]string", + "underlyingType": "[]string", + "argNum": 2, + "expr": "shells" + } + ], + "fuzzy": true + }, + { + "id": "{Name} version {Version}", + "message": "{Name} version {Version}", + "translation": "{Name} version {Version}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "cmd.Name" + }, + { + "id": "Version", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "cmd.Version" + } + ], + "fuzzy": true + } + ] +} \ No newline at end of file diff --git a/internal/translations/locales/en-US/out.gotext.json b/internal/translations/locales/en-US/out.gotext.json new file mode 100644 index 0000000000..ef299631f4 --- /dev/null +++ b/internal/translations/locales/en-US/out.gotext.json @@ -0,0 +1,89 @@ +{ + "language": "en-US", + "messages": [ + { + "id": "Incorrect Usage: {Error}", + "message": "Incorrect Usage: {Error}", + "translation": "Incorrect Usage: {Error}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Error", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "err.Error()" + } + ], + "fuzzy": true + }, + { + "id": "no shell provided for completion command. available shells are {Shells}", + "message": "no shell provided for completion command. available shells are {Shells}", + "translation": "no shell provided for completion command. available shells are {Shells}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Shells", + "string": "%+[1]v", + "type": "[]string", + "underlyingType": "[]string", + "argNum": 1, + "expr": "shells" + } + ], + "fuzzy": true + }, + { + "id": "unknown shell {S}, available shells are {Shells}", + "message": "unknown shell {S}, available shells are {Shells}", + "translation": "unknown shell {S}, available shells are {Shells}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "S", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "s" + }, + { + "id": "Shells", + "string": "%+[2]v", + "type": "[]string", + "underlyingType": "[]string", + "argNum": 2, + "expr": "shells" + } + ], + "fuzzy": true + }, + { + "id": "{Name} version {Version}", + "message": "{Name} version {Version}", + "translation": "{Name} version {Version}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]v", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "cmd.Name" + }, + { + "id": "Version", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "cmd.Version" + } + ], + "fuzzy": true + } + ] +} \ No newline at end of file diff --git a/internal/translations/translations.go b/internal/translations/translations.go new file mode 100644 index 0000000000..318a4e3600 --- /dev/null +++ b/internal/translations/translations.go @@ -0,0 +1,7 @@ +package translations + +//go:generate gotext -srclang=en-US update -out=catalog.go -lang=en-US,en-GB github.com/urfave/cli/v3 + +// the languages to be supported need to be provided with the -lang flag above +// -lang=en-US,en-GB +// https://www.fincher.org/Utilities/CountryLanguageList.shtml