diff --git a/.github/workflows/i18n-nightly-push.yaml b/.github/workflows/i18n-nightly-push.yaml new file mode 100644 index 00000000000..85e60fefadc --- /dev/null +++ b/.github/workflows/i18n-nightly-push.yaml @@ -0,0 +1,30 @@ +name: i18n-nightly-push + +on: + schedule: + # run every day at 1AM + - cron: '0 1 * * *' + +jobs: + push-to-transifex: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: '1.14' + + - name: Install Taskfile + uses: Arduino/actions/setup-taskfile@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run task i18n:push + run: task i18n:push + env: + TRANSIFEX_PROJECT: ${{ secrets.TRANSIFEX_PROJECT }} + TRANSIFEX_RESOURCE: ${{ secrets.TRANSIFEX_RESOURCE }} + TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }} \ No newline at end of file diff --git a/.github/workflows/i18n-weekly-pull.yaml b/.github/workflows/i18n-weekly-pull.yaml new file mode 100644 index 00000000000..06fd5c84e36 --- /dev/null +++ b/.github/workflows/i18n-weekly-pull.yaml @@ -0,0 +1,41 @@ +name: i18n-weekly-pull + +on: + schedule: + # run every monday at 2AM + - cron: '0 2 * * 1' + +jobs: + pull-from-transifex: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@master + + - name: Install Go + uses: actions/setup-go@v2 + with: + go-version: '1.14' + + - name: Install Go deps + run: | + go get github.com/GeertJohan/go.rice/rice + + - name: Install Taskfile + uses: Arduino/actions/setup-taskfile@master + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Run task i18n:pull + run: task i18n:pull + env: + TRANSIFEX_PROJECT: ${{ secrets.TRANSIFEX_PROJECT }} + TRANSIFEX_RESOURCE: ${{ secrets.TRANSIFEX_RESOURCE }} + TRANSIFEX_API_KEY: ${{ secrets.TRANSIFEX_API_KEY }} + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v2 + with: + commit-message: Updated translation files + title: Updated translation files + branch: i18n/translations-update \ No newline at end of file diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index dc6df0487cd..94c7fe1ca04 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@master - name: Install Go - uses: actions/setup-go@v1 + uses: actions/setup-go@v2 with: go-version: '1.14' @@ -35,6 +35,7 @@ jobs: go get github.com/golangci/govet go get golang.org/x/lint/golint go get github.com/golang/protobuf/protoc-gen-go + go get github.com/GeertJohan/go.rice/rice shell: bash - name: Install Taskfile diff --git a/Taskfile.yml b/Taskfile.yml index 64282302b55..3f717d5fe96 100755 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -102,6 +102,7 @@ tasks: - test -z $(go fmt {{ default .DEFAULT_TARGETS .TARGETS }}) - go vet {{ default .DEFAULT_TARGETS .TARGETS }} - "'{{.GOLINTBIN}}' {{.GOLINTFLAGS}} {{ default .DEFAULT_TARGETS .TARGETS }}" + - task: i18n:check check-legacy: desc: Check fmt and lint for the `legacy` package @@ -114,6 +115,36 @@ tasks: cmds: - go test -run TestWithClientE2E ./commands/daemon + i18n:update: + desc: Updates i18n files + cmds: + - go run ./i18n/cmd/main.go catalog generate . > ./i18n/data/en.po + - task: i18n:generate + + i18n:pull: + desc: Pull i18n files from transifex + cmds: + - go run ./i18n/cmd/main.go transifex pull -l {{.I18N_LANGS}} ./i18n/data + - task: i18n:generate + + i18n:push: + desc: Push i18n files to transifex + cmds: + - go run ./i18n/cmd/main.go transifex push ./i18n/data + + i18n:check: + desc: Check if the i18n message catalog was updated + cmds: + - task: i18n:update + - git add -N ./i18n/data + - git diff --exit-code ./i18n/data + + i18n:generate: + desc: Generate embedded i18n catalog files + cmds: + - git add -N ./i18n/data + - git diff --exit-code ./i18n/data &> /dev/null || (cd ./i18n && rice embed-go) + vars: # all modules of this project except for "legacy/..." module DEFAULT_TARGETS: @@ -138,3 +169,4 @@ vars: DOCS_VERSION: dev DOCS_ALIAS: "" DOCS_REMOTE: "origin" + I18N_LANGS: "pt_BR" diff --git a/cli/board/board.go b/cli/board/board.go index 613130a28b7..29ab85b4243 100644 --- a/cli/board/board.go +++ b/cli/board/board.go @@ -36,7 +36,7 @@ func NewCommand() *cobra.Command { boardCommand.AddCommand(initAttachCommand()) boardCommand.AddCommand(initDetailsCommand()) boardCommand.AddCommand(initListCommand()) - boardCommand.AddCommand(listAllCommand) + boardCommand.AddCommand(initListAllCommand()) return boardCommand } diff --git a/cli/board/details.go b/cli/board/details.go index 1b3d4ebe00a..56616f457ff 100644 --- a/cli/board/details.go +++ b/cli/board/details.go @@ -18,6 +18,8 @@ package board import ( "context" "fmt" + "os" + "github.com/arduino/arduino-cli/cli/errorcodes" "github.com/arduino/arduino-cli/cli/feedback" "github.com/arduino/arduino-cli/cli/instance" @@ -26,7 +28,6 @@ import ( "github.com/arduino/arduino-cli/table" "github.com/fatih/color" "github.com/spf13/cobra" - "os" ) var showFullDetails bool diff --git a/cli/board/listall.go b/cli/board/listall.go index d2e2c03eef0..0598b5dc5fd 100644 --- a/cli/board/listall.go +++ b/cli/board/listall.go @@ -29,17 +29,20 @@ import ( "github.com/spf13/cobra" ) -var listAllCommand = &cobra.Command{ - Use: "listall [boardname]", - Short: "List all known boards and their corresponding FQBN.", - Long: "" + - "List all boards that have the support platform installed. You can search\n" + - "for a specific board if you specify the board name", - Example: "" + - " " + os.Args[0] + " board listall\n" + - " " + os.Args[0] + " board listall zero", - Args: cobra.ArbitraryArgs, - Run: runListAllCommand, +func initListAllCommand() *cobra.Command { + var listAllCommand = &cobra.Command{ + Use: "listall [boardname]", + Short: "List all known boards and their corresponding FQBN.", + Long: "" + + "List all boards that have the support platform installed. You can search\n" + + "for a specific board if you specify the board name", + Example: "" + + " " + os.Args[0] + " board listall\n" + + " " + os.Args[0] + " board listall zero", + Args: cobra.ArbitraryArgs, + Run: runListAllCommand, + } + return listAllCommand } // runListAllCommand list all installed boards diff --git a/cli/cli.go b/cli/cli.go index 653aaff6d04..44504de44ed 100644 --- a/cli/cli.go +++ b/cli/cli.go @@ -19,7 +19,6 @@ import ( "fmt" "io/ioutil" "os" - "path/filepath" "strings" "github.com/arduino/arduino-cli/cli/board" @@ -38,7 +37,7 @@ import ( "github.com/arduino/arduino-cli/cli/sketch" "github.com/arduino/arduino-cli/cli/upload" "github.com/arduino/arduino-cli/cli/version" - "github.com/arduino/arduino-cli/configuration" + "github.com/arduino/arduino-cli/i18n" "github.com/arduino/arduino-cli/inventory" "github.com/mattn/go-colorable" "github.com/rifflock/lfshook" @@ -48,8 +47,17 @@ import ( ) var ( + verbose bool + outputFormat string + configFile string +) + +// NewCommand creates a new ArduinoCli command root +func NewCommand() *cobra.Command { + cobra.AddTemplateFunc("tr", i18n.Tr) + // ArduinoCli is the root command - ArduinoCli = &cobra.Command{ + arduinoCli := &cobra.Command{ Use: "arduino-cli", Short: "Arduino CLI.", Long: "Arduino Command Line Interface (arduino-cli).", @@ -57,14 +65,11 @@ var ( PersistentPreRun: preRun, } - verbose bool - outputFormat string - configFile string -) + arduinoCli.SetUsageTemplate(usageTemplate) -// Init the cobra root command -func init() { - createCliCommandTree(ArduinoCli) + createCliCommandTree(arduinoCli) + + return arduinoCli } // this is here only for testing @@ -120,52 +125,7 @@ func parseFormatString(arg string) (feedback.OutputFormat, bool) { return f, found } -// This function is here to replicate the old logic looking for a config -// file in the parent tree of the CWD, aka "project config". -// Please -func searchConfigTree(cwd string) string { - // go back up to root and search for the config file - for { - if _, err := os.Stat(filepath.Join(cwd, "arduino-cli.yaml")); err == nil { - // config file found - return cwd - } else if os.IsNotExist(err) { - // no config file found - next := filepath.Dir(cwd) - if next == cwd { - return "" - } - cwd = next - } else { - // some error we can't handle happened - return "" - } - } -} - func preRun(cmd *cobra.Command, args []string) { - // - // Prepare the configuration system - // - configPath := "" - - // get cwd, if something is wrong don't do anything and let - // configuration init proceed - if cwd, err := os.Getwd(); err == nil { - configPath = searchConfigTree(cwd) - } - - // override the config path if --config-file was passed - if fi, err := os.Stat(configFile); err == nil { - if fi.IsDir() { - configPath = configFile - } else { - configPath = filepath.Dir(configFile) - } - } - - // initialize the config system - configuration.Init(configPath) configFile := viper.ConfigFileUsed() // initialize inventory diff --git a/cli/config/config.go b/cli/config/config.go index a7463494664..c382532df8f 100644 --- a/cli/config/config.go +++ b/cli/config/config.go @@ -29,7 +29,7 @@ func NewCommand() *cobra.Command { Example: " " + os.Args[0] + " config init", } - configCommand.AddCommand(dumpCmd) + configCommand.AddCommand(initDumpCmd()) configCommand.AddCommand(initInitCommand()) return configCommand diff --git a/cli/config/dump.go b/cli/config/dump.go index d449790c5cb..c384a8d48a7 100644 --- a/cli/config/dump.go +++ b/cli/config/dump.go @@ -25,13 +25,16 @@ import ( "gopkg.in/yaml.v2" ) -var dumpCmd = &cobra.Command{ - Use: "dump", - Short: "Prints the current configuration", - Long: "Prints the current configuration.", - Example: " " + os.Args[0] + " config dump", - Args: cobra.NoArgs, - Run: runDumpCommand, +func initDumpCmd() *cobra.Command { + var dumpCmd = &cobra.Command{ + Use: "dump", + Short: "Prints the current configuration", + Long: "Prints the current configuration.", + Example: " " + os.Args[0] + " config dump", + Args: cobra.NoArgs, + Run: runDumpCommand, + } + return dumpCmd } // output from this command requires special formatting, let's create a dedicated diff --git a/cli/usage.go b/cli/usage.go new file mode 100644 index 00000000000..6750e546742 --- /dev/null +++ b/cli/usage.go @@ -0,0 +1,58 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package cli + +import ( + "github.com/arduino/arduino-cli/i18n" +) + +// Declare ids used in usage +var ( + tr = i18n.Tr + _ = tr("Usage:") + _ = tr("Aliases:") + _ = tr("Examples:") + _ = tr("Available Commands:") + _ = tr("Flags:") + _ = tr("Global Flags:") + _ = tr("Additional help topics:") + _ = tr("Use %s for more information about a command.") +) + +const usageTemplate = `{{tr "Usage:"}}{{if .Runnable}} + {{.UseLine}}{{end}}{{if .HasAvailableSubCommands}} + {{.CommandPath}} [command]{{end}}{{if gt (len .Aliases) 0}} + +{{tr "Aliases:"}} + {{.NameAndAliases}}{{end}}{{if .HasExample}} + +{{tr "Examples:"}} +{{.Example}}{{end}}{{if .HasAvailableSubCommands}} + +{{tr "Available Commands:"}}{{range .Commands}}{{if (or .IsAvailableCommand (eq .Name "help"))}} + {{rpad .Name .NamePadding }} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableLocalFlags}} + +{{tr "Flags:"}} +{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasAvailableInheritedFlags}} + +{{tr "Global Flags:"}} +{{.InheritedFlags.FlagUsages | trimTrailingWhitespaces}}{{end}}{{if .HasHelpSubCommands}} + +{{tr "Additional help topics:"}}{{range .Commands}}{{if .IsAdditionalHelpTopicCommand}} + {{rpad .CommandPath .CommandPathPadding}} {{.Short}}{{end}}{{end}}{{end}}{{if .HasAvailableSubCommands}} + +{{tr "Use %s for more information about a command." (printf "%s %s" .CommandPath "[command] --help" | printf "%q")}}{{end}} +` diff --git a/configuration/configuration.go b/configuration/configuration.go index 5dd4218baea..66c089bd162 100644 --- a/configuration/configuration.go +++ b/configuration/configuration.go @@ -180,3 +180,60 @@ func IsBundledInDesktopIDE() bool { return true } + +// FindConfigFile returns the config file path using the argument '--config-file' if specified or via the current working dir +func FindConfigFile() string { + + configFile := "" + for i, arg := range os.Args { + // 0 --config-file ss + if arg == "--config-file" { + if len(os.Args) > i+1 { + configFile = os.Args[i+1] + } + } + } + + if configFile != "" { + if fi, err := os.Stat(configFile); err == nil { + if fi.IsDir() { + return configFile + } + return filepath.Dir(configFile) + } + } + + return searchCwdForConfig() +} + +func searchConfigTree(cwd string) string { + + // go back up to root and search for the config file + for { + if _, err := os.Stat(filepath.Join(cwd, "arduino-cli.yaml")); err == nil { + // config file found + return cwd + } else if os.IsNotExist(err) { + // no config file found + next := filepath.Dir(cwd) + if next == cwd { + return "" + } + cwd = next + } else { + // some error we can't handle happened + return "" + } + } + +} + +func searchCwdForConfig() string { + cwd, err := os.Getwd() + + if err != nil { + return "" + } + + return searchConfigTree(cwd) +} diff --git a/cli/cli_test.go b/configuration/configuration_test.go similarity index 91% rename from cli/cli_test.go rename to configuration/configuration_test.go index 72ee80a1eac..820d19c2f50 100644 --- a/cli/cli_test.go +++ b/configuration/configuration_test.go @@ -13,10 +13,7 @@ // Arduino software without disclosing the source code of your own applications. // To purchase a commercial license, send an email to license@arduino.cc. -// These tests are mocked and won't work on OSX -// +build !darwin - -package cli +package configuration import ( "fmt" @@ -46,7 +43,7 @@ func TestSearchConfigTreeSameFolder(t *testing.T) { defer os.RemoveAll(tmp) _, err := os.Create(filepath.Join(tmp, "arduino-cli.yaml")) require.Nil(t, err) - require.Equal(t, searchConfigTree(tmp), tmp) + require.Equal(t, tmp, searchConfigTree(tmp)) } func TestSearchConfigTreeInParent(t *testing.T) { @@ -57,7 +54,7 @@ func TestSearchConfigTreeInParent(t *testing.T) { require.Nil(t, err) _, err = os.Create(filepath.Join(tmp, "arduino-cli.yaml")) require.Nil(t, err) - require.Equal(t, searchConfigTree(target), tmp) + require.Equal(t, tmp, searchConfigTree(target)) } var result string diff --git a/docsgen/main.go b/docsgen/main.go index d3286a117e8..9e8cf837a61 100644 --- a/docsgen/main.go +++ b/docsgen/main.go @@ -28,7 +28,7 @@ func main() { log.Fatal("Please provide output folder") } - cli := cli.ArduinoCli + cli := cli.NewCommand() err := doc.GenMarkdownTree(cli, os.Args[1]) if err != nil { log.Fatal(err) diff --git a/go.mod b/go.mod index 758841d14bd..fa398bf170e 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.14 require ( bou.ke/monkey v1.0.1 + github.com/GeertJohan/go.rice v1.0.0 github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c github.com/arduino/go-paths-helper v1.0.1 github.com/arduino/go-properties-orderedmap v1.0.0 @@ -23,6 +24,7 @@ require ( github.com/juju/errors v0.0.0-20181118221551-089d3ea4e4d5 // indirect github.com/juju/loggo v0.0.0-20190526231331-6e530bcce5d8 // indirect github.com/juju/testing v0.0.0-20190429233213-dfc56b8c09fc // indirect + github.com/leonelquinteros/gotext v1.4.0 github.com/mattn/go-colorable v0.1.2 github.com/mattn/go-runewidth v0.0.2 // indirect github.com/miekg/dns v1.0.5 // indirect diff --git a/go.sum b/go.sum index d32bee8d0ec..52b9bdc331c 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,13 @@ bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/GeertJohan/go.incremental v1.0.0 h1:7AH+pY1XUgQE4Y1HcXYaMqAI0m9yrFqo/jt0CW30vsg= +github.com/GeertJohan/go.incremental v1.0.0/go.mod h1:6fAjUhbVuX1KcMD3c8TEgVUqmo4seqhv0i0kdATSkM0= +github.com/GeertJohan/go.rice v1.0.0 h1:KkI6O9uMaQU3VEKaj01ulavtF7o1fWT7+pk/4voiMLQ= +github.com/GeertJohan/go.rice v1.0.0/go.mod h1:eH6gbSOAUv07dQuZVnBmoDP8mgsM1rtixis4Tib9if0= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/akavel/rsrc v0.8.0 h1:zjWn7ukO9Kc5Q62DOJCcxGpXC18RawVtYAGdz2aLlfw= +github.com/akavel/rsrc v0.8.0/go.mod h1:uLoCtb9J+EyAqh+26kdrTgmzRBFPGOolLWKpdxkKq+c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/arduino/board-discovery v0.0.0-20180823133458-1ba29327fb0c h1:agh2JT96G8egU7FEb13L4dq3fnCN7lxXhJ86t69+W7s= @@ -38,6 +44,8 @@ github.com/cpuguy83/go-md2man v1.0.10 h1:BSKMNlYxDvnunlTymqtgONjNnaRV1sTpcovwwjF github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE= github.com/creack/goselect v0.1.1 h1:tiSSgKE1eJtxs1h/VgGQWuXUP0YS4CDIFMp6vaI1ls0= github.com/creack/goselect v0.1.1/go.mod h1:a/NhLweNvqIYMuxcMOuWY516Cimucms3DglDzQP3hKY= +github.com/daaku/go.zipexe v1.0.0 h1:VSOgZtH418pH9L16hC/JrgSNJbbAL26pj7lmD1+CGdY= +github.com/daaku/go.zipexe v1.0.0/go.mod h1:z8IiR6TsVLEYKwXAoE/I+8ys/sDkgTzSL0CLnGVd57E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -89,6 +97,8 @@ github.com/imjasonmiller/godice v0.1.2 h1:T1/sW/HoDzFeuwzOOuQjmeMELz9CzZ53I2CnD+ github.com/imjasonmiller/godice v0.1.2/go.mod h1:8cTkdnVI+NglU2d6sv+ilYcNaJ5VSTBwvMbFULJd/QQ= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= +github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= @@ -109,6 +119,8 @@ github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORN github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/leonelquinteros/gotext v1.4.0 h1:2NHPCto5IoMXbrT0bldPrxj0qM5asOCwtb1aUQZ1tys= +github.com/leonelquinteros/gotext v1.4.0/go.mod h1:yZGXREmoGTtBvZHNcc+Yfug49G/2spuF/i/Qlsvz1Us= github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY= github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ= github.com/magiconair/properties v1.8.1 h1:ZC2Vc7/ZFkGmsVC9KvOjumD+G5lXy2RtTKyzRKO2BQ4= @@ -130,6 +142,8 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229 h1:E2B8qYyeSgv5MXpmzZXRNp8IAQ4vjxIjhpAf5hv/tAg= +github.com/nkovacs/streamquote v0.0.0-20170412213628-49af9bddb229/go.mod h1:0aYXnNPJ8l7uZxf45rWW1a/uME32OF0rhiYGNQ2oF2E= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/oleksandr/bonjour v0.0.0-20160508152359-5dcf00d8b228 h1:Cvfd2dOlXIPTeEkOT/h8PyK4phBngOM4at9/jlgy7d4= github.com/oleksandr/bonjour v0.0.0-20160508152359-5dcf00d8b228/go.mod h1:MGuVJ1+5TX1SCoO2Sx0eAnjpdRytYla2uC1YIZfkC9c= @@ -201,6 +215,10 @@ github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69 github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0= +github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= +github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= +github.com/valyala/fasttemplate v1.0.1 h1:tY9CJiPnMXf1ERmG2EyK7gNUd+c6RKGD0IfU8WdUSz8= +github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= go.bug.st/cleanup v1.0.0 h1:XVj1HZxkBXeq3gMT7ijWUpHyIC1j8XAoNSyQ06CskgA= diff --git a/i18n/README.md b/i18n/README.md new file mode 100644 index 00000000000..417fc3cbdd8 --- /dev/null +++ b/i18n/README.md @@ -0,0 +1,53 @@ +# I18N + +## Usage + +In the source code, use the function `i18n.Tr("message", ...args)` to get a localized string. This tool parses the source using the `go/ast` package to generate the `en` locale using these messages. + +## Updating messages to reflect code changes + +Install [go-rice](https://github.com/GeertJohan/go.rice) + +```sh +go get github.com/GeertJohan/go.rice +go get github.com/GeertJohan/go.rice/rice +``` + +The following command updates the locales present in the source code to reflect addition/removal of messages. + +```sh +task i18n:update +``` + +## Syncing the catalog with transifex + +### Environment variables + +Set the following environment variables according to the project + +|Variable|Description| +|--------|-----------| +|TRANSIFEX_PROJECT|Name of the transifex project| +|TRANSIFEX_RESOURCE|Name of the transifex translation resource| +|TRANSIFEX_API_KEY|API Key to access the transifex project| + +### Push + +```sh +task i18n:push +``` + +### Pull + +```sh +task i18n:pull +``` + +## Adding a new language + +To add a new supported language add the locale string to the project's Taskfile.yml (comma separated list) + +e.g +``` +I18N_LANGS: "pt_BR,es,jp" +``` \ No newline at end of file diff --git a/i18n/cmd/ast/parser.go b/i18n/cmd/ast/parser.go new file mode 100644 index 00000000000..9126a4ada8e --- /dev/null +++ b/i18n/cmd/ast/parser.go @@ -0,0 +1,97 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package ast + +import ( + "fmt" + "go/ast" + "go/parser" + "go/token" + "os" + "path/filepath" + "strconv" + + "github.com/arduino/arduino-cli/i18n/cmd/po" +) + +// GenerateCatalog generates the i18n message catalog for the go source files +func GenerateCatalog(files []string) po.MessageCatalog { + fset := token.NewFileSet() + catalog := po.MessageCatalog{} + + for _, file := range files { + node, err := parser.ParseFile(fset, file, nil, parser.AllErrors) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + doFile(fset, node, &catalog) + } + + catalog.Add("", "", nil) + + return catalog +} + +func doFile(fset *token.FileSet, file *ast.File, catalog *po.MessageCatalog) { + ast.Inspect(file, func(node ast.Node) bool { + funcCall, ok := node.(*ast.CallExpr) + + if !ok { + return true + } + + if functionName(funcCall) != "i18n.Tr" && functionName(funcCall) != "tr" { + return true + } + + pos := fset.Position(funcCall.Pos()) + firstArg, ok := funcCall.Args[0].(*ast.BasicLit) + if !ok { + fmt.Fprintf(os.Stderr, "%s:%d\n", pos.Filename, pos.Line) + fmt.Fprintln(os.Stderr, "argument to i18n.Tr must be a literal string") + return true + } + + msg, err := strconv.Unquote(firstArg.Value) + + if err != nil { + fmt.Fprintf(os.Stderr, "%s:%d\n", pos.Filename, pos.Line) + fmt.Fprintln(os.Stderr, err.Error()) + return true + } + + catalog.Add(msg, msg, []string{fmt.Sprintf("#: %s:%d", filepath.ToSlash(pos.Filename), pos.Line)}) + + return true + }) +} + +func functionName(callExpr *ast.CallExpr) string { + + if iden, ok := callExpr.Fun.(*ast.Ident); ok { + return iden.Name + } + + if sel, ok := callExpr.Fun.(*ast.SelectorExpr); ok { + if iden, ok := sel.X.(*ast.Ident); ok { + return iden.Name + "." + sel.Sel.Name + } + } + + return "" +} diff --git a/i18n/cmd/commands/catalog/catalog.go b/i18n/cmd/commands/catalog/catalog.go new file mode 100644 index 00000000000..25b69cfe4cc --- /dev/null +++ b/i18n/cmd/commands/catalog/catalog.go @@ -0,0 +1,28 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package catalog + +import "github.com/spf13/cobra" + +// Command is the catalog command +var Command = &cobra.Command{ + Use: "catalog", + Short: "catalog", +} + +func init() { + Command.AddCommand(generateCatalogCommand) +} diff --git a/i18n/cmd/commands/catalog/generate_catalog.go b/i18n/cmd/commands/catalog/generate_catalog.go new file mode 100644 index 00000000000..c895660867a --- /dev/null +++ b/i18n/cmd/commands/catalog/generate_catalog.go @@ -0,0 +1,48 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package catalog + +import ( + "os" + "path/filepath" + + "github.com/arduino/arduino-cli/i18n/cmd/ast" + "github.com/spf13/cobra" +) + +var generateCatalogCommand = &cobra.Command{ + Use: "generate [input folder]", + Short: "generates the en catalog from source files", + Args: cobra.MinimumNArgs(1), + Run: generateCatalog, +} + +func generateCatalog(cmd *cobra.Command, args []string) { + + folder := args[0] + files := []string{} + filepath.Walk(folder, func(name string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || filepath.Ext(name) != ".go" { + return nil + } + + files = append(files, name) + return nil + }) + + catalog := ast.GenerateCatalog(files) + catalog.Write(os.Stdout) +} diff --git a/i18n/cmd/commands/root.go b/i18n/cmd/commands/root.go new file mode 100644 index 00000000000..e05403669c0 --- /dev/null +++ b/i18n/cmd/commands/root.go @@ -0,0 +1,37 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package commands + +import ( + "github.com/arduino/arduino-cli/i18n/cmd/commands/catalog" + "github.com/arduino/arduino-cli/i18n/cmd/commands/transifex" + "github.com/spf13/cobra" +) + +var i18nCommand = &cobra.Command{ + Use: "i18n", + Short: "i18n", +} + +func init() { + i18nCommand.AddCommand(catalog.Command) + i18nCommand.AddCommand(transifex.Command) +} + +// Execute executes the i18n command +func Execute() error { + return i18nCommand.Execute() +} diff --git a/i18n/cmd/commands/transifex/pull_transifex.go b/i18n/cmd/commands/transifex/pull_transifex.go new file mode 100644 index 00000000000..e94755789cd --- /dev/null +++ b/i18n/cmd/commands/transifex/pull_transifex.go @@ -0,0 +1,91 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package transifex + +import ( + "fmt" + "io/ioutil" + "net/http" + "os" + "path" + + "github.com/spf13/cobra" +) + +var pullTransifexCommand = &cobra.Command{ + Use: "pull -l pt_BR [catalog folder]", + Short: "pulls the translation files from transifex", + Args: cobra.ExactArgs(1), + Run: pullCatalog, +} + +var languages = []string{} + +func init() { + pullTransifexCommand.Flags().StringSliceVarP(&languages, "languages", "l", nil, "languages") + pullTransifexCommand.MarkFlagRequired("languages") +} + +func pullCatalog(cmd *cobra.Command, args []string) { + folder := args[0] + + for _, lang := range languages { + + req, err := http.NewRequest( + "GET", + fmt.Sprintf( + "https://www.transifex.com/api/2/project/%s/resource/%s/translation/%s/?mode=reviewed&file=po", + project, resource, lang, + ), nil) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + req.SetBasicAuth("api", apiKey) + + resp, err := http.DefaultClient.Do(req) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + defer resp.Body.Close() + + b, err := ioutil.ReadAll(resp.Body) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + os.Remove(path.Join(folder, lang+".po")) + file, err := os.OpenFile(path.Join(folder, lang+".po"), os.O_CREATE|os.O_RDWR, 0644) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + _, err = file.Write(b) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + } +} diff --git a/i18n/cmd/commands/transifex/push_transifex.go b/i18n/cmd/commands/transifex/push_transifex.go new file mode 100644 index 00000000000..40a28a2bbb6 --- /dev/null +++ b/i18n/cmd/commands/transifex/push_transifex.go @@ -0,0 +1,90 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package transifex + +import ( + "bytes" + "fmt" + "io" + "mime/multipart" + "net/http" + "os" + "path" + "path/filepath" + + "github.com/spf13/cobra" +) + +var pushTransifexCommand = &cobra.Command{ + Use: "push [catalog folder]", + Short: "pushes the translation files to transifex", + Args: cobra.ExactArgs(1), + Run: pushCatalog, +} + +func pushFile(folder, lang, url string) { + filename := path.Join(folder, lang+".po") + file, err := os.Open(filename) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + defer file.Close() + + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile(lang, filepath.Base(filename)) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + _, err = io.Copy(part, file) + writer.WriteField("file_type", "po") + + req, err := http.NewRequest("PUT", url, body) + req.Header.Set("Content-Type", writer.FormDataContentType()) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + req.SetBasicAuth("api", apiKey) + + resp, err := http.DefaultClient.Do(req) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + resp.Body.Close() +} + +func pushCatalog(cmd *cobra.Command, args []string) { + folder := args[0] + + pushFile( + folder, + "en", + fmt.Sprintf( + "https://www.transifex.com/api/2/project/%s/resource/%s/content/", + project, + resource, + ), + ) +} diff --git a/i18n/cmd/commands/transifex/transifex.go b/i18n/cmd/commands/transifex/transifex.go new file mode 100644 index 00000000000..63fb3750ba2 --- /dev/null +++ b/i18n/cmd/commands/transifex/transifex.go @@ -0,0 +1,62 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package transifex + +import ( + "fmt" + "os" + + "github.com/spf13/cobra" +) + +// Command is the transifex command +var Command = &cobra.Command{ + Use: "transifex", + Short: "transifex", + PersistentPreRun: preRun, +} + +var project string +var resource string +var apiKey string + +func init() { + Command.AddCommand(pullTransifexCommand) + Command.AddCommand(pushTransifexCommand) +} + +func preRun(cmd *cobra.Command, args []string) { + project = os.Getenv("TRANSIFEX_PROJECT") + resource = os.Getenv("TRANSIFEX_RESOURCE") + apiKey = os.Getenv("TRANSIFEX_RESOURCE") + + if project = os.Getenv("TRANSIFEX_PROJECT"); project == "" { + fmt.Println("missing TRANSIFEX_PROJECT environment variable") + os.Exit(1) + } + + if resource = os.Getenv("TRANSIFEX_RESOURCE"); resource == "" { + fmt.Println("missing TRANSIFEX_RESOURCE environment variable") + os.Exit(1) + } + + if apiKey = os.Getenv("TRANSIFEX_API_KEY"); apiKey == "" { + fmt.Println("missing TRANSIFEX_API_KEY environment variable") + os.Exit(1) + } + + return +} diff --git a/i18n/cmd/main.go b/i18n/cmd/main.go new file mode 100644 index 00000000000..69b1373251e --- /dev/null +++ b/i18n/cmd/main.go @@ -0,0 +1,30 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package main + +import ( + "fmt" + "os" + + "github.com/arduino/arduino-cli/i18n/cmd/commands" +) + +func main() { + if err := commands.Execute(); err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } +} diff --git a/i18n/cmd/po/catalog.go b/i18n/cmd/po/catalog.go new file mode 100644 index 00000000000..d90bcc27e5a --- /dev/null +++ b/i18n/cmd/po/catalog.go @@ -0,0 +1,109 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package po + +import ( + "fmt" + "io" + "sort" + "strings" +) + +type ( + // MessageCatalog is the catalog of i18n messages for a given locale + MessageCatalog struct { + Messages map[string]*Message + } + + // Message represents a i18n message + Message struct { + Comments []string + Value string + } +) + +// Add adds a new message in the i18n catalog +func (catalog *MessageCatalog) Add(id, value string, comment []string) { + if catalog.Messages == nil { + catalog.Messages = map[string]*Message{} + } + + if catalog.Messages[id] == nil { + catalog.Messages[id] = &Message{Value: value} + } + + if len(comment) != 0 { + catalog.Messages[id].Comments = append(catalog.Messages[id].Comments, comment...) + } +} + +// AddMessage adds a new message in the i18n catalog +func (catalog *MessageCatalog) AddMessage(id string, message Message) { + catalog.Add(id, message.Value, message.Comments) +} + +// SortedKeys returns the sorted keys in the catalog +func (catalog *MessageCatalog) SortedKeys() []string { + keys := []string{} + for k := range catalog.Messages { + keys = append(keys, k) + } + + sort.Strings(keys) + return keys +} + +// Write writes the catalog in PO file format into w +func (catalog *MessageCatalog) Write(w io.Writer) { + keys := []string{} + for k := range catalog.Messages { + keys = append(keys, k) + } + + sort.Strings(keys) + + for _, k := range keys { + msg := catalog.Messages[k] + + for _, comment := range msg.Comments { + fmt.Fprintln(w, comment) + } + + printValue(w, "msgid", k) + printValue(w, "msgstr", msg.Value) + fmt.Fprintln(w) + } +} + +func printValue(w io.Writer, field, value string) { + if strings.Contains(value, "\n") { + fmt.Fprintf(w, "%s ", field) + lines := strings.Split(value, "\n") + for i, line := range lines { + if i == len(lines)-1 { + fmt.Fprintf(w, "\"%s\"\n", escape(line)) + } else { + fmt.Fprintf(w, "\"%s\\n\"\n", escape(line)) + } + } + } else { + fmt.Fprintf(w, "%s \"%s\"\n", field, escape(value)) + } +} + +func escape(value string) string { + return strings.ReplaceAll(value, `"`, `\"`) +} diff --git a/i18n/cmd/po/catalog_test.go b/i18n/cmd/po/catalog_test.go new file mode 100644 index 00000000000..85b61f1a9db --- /dev/null +++ b/i18n/cmd/po/catalog_test.go @@ -0,0 +1,61 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package po + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestCatalogWriter(t *testing.T) { + var catalog MessageCatalog + catalog.Add("\"test-id\"", "test-value", []string{"# comment A", "# comment B"}) + catalog.Add("test-id\nmultiline", "test-value\nmultiline", []string{}) + catalog.Add(` + test-id + backquoted + `, ` + test-value + backquoted + `, []string{}) + + var buf bytes.Buffer + catalog.Write(&buf) + + require.Equal(t, `msgid "\n" +" test-id\n" +" backquoted\n" +" " +msgstr "\n" +" test-value\n" +" backquoted\n" +" " + +# comment A +# comment B +msgid "\"test-id\"" +msgstr "test-value" + +msgid "test-id\n" +"multiline" +msgstr "test-value\n" +"multiline" + +`, buf.String()) + +} diff --git a/i18n/cmd/po/merge.go b/i18n/cmd/po/merge.go new file mode 100644 index 00000000000..c4a74b0d16f --- /dev/null +++ b/i18n/cmd/po/merge.go @@ -0,0 +1,42 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package po + +// Merge merges two message catalogs, preserving only keys present in source +func Merge(source MessageCatalog, destination MessageCatalog) MessageCatalog { + catalog := MessageCatalog{} + for _, k := range source.SortedKeys() { + if k == "" { + sourceMessage := source.Messages[k] + destinationMessage := destination.Messages[k] + + if destinationMessage != nil { + catalog.AddMessage(k, *destinationMessage) + } else { + catalog.AddMessage(k, *sourceMessage) + } + + continue + } + + if destination.Messages[k] != nil { + catalog.Add(k, destination.Messages[k].Value, source.Messages[k].Comments) + } else { + catalog.Add(k, "", source.Messages[k].Comments) + } + } + return catalog +} diff --git a/i18n/cmd/po/merge_test.go b/i18n/cmd/po/merge_test.go new file mode 100644 index 00000000000..2540a4c7182 --- /dev/null +++ b/i18n/cmd/po/merge_test.go @@ -0,0 +1,47 @@ +package po + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestMerge(t *testing.T) { + r := strings.NewReader(` +msgid "a" +msgstr "" + +msgid "b" +msgstr "value-b" + `) + catalogA := ParseReader(r) + + r = strings.NewReader(` +msgid "a" +msgstr "value-a" + +msgid "b" +msgstr "value-b" + +msgid "c" +msgstr "value-c" + `) + + catalogB := ParseReader(r) + + mergedCatalog := Merge(catalogA, catalogB) + + var buf bytes.Buffer + mergedCatalog.Write(&buf) + + require.Equal(t, `msgid "a" +msgstr "value-a" + +msgid "b" +msgstr "value-b" + +`, buf.String()) + +} diff --git a/i18n/cmd/po/parser.go b/i18n/cmd/po/parser.go new file mode 100644 index 00000000000..0d5ebf85e63 --- /dev/null +++ b/i18n/cmd/po/parser.go @@ -0,0 +1,132 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package po + +import ( + "bufio" + "fmt" + "io" + "os" + "strconv" + "strings" +) + +// Parse parses the PO file into a MessageCatalog +func Parse(filename string) MessageCatalog { + if !fileExists(filename) { + return MessageCatalog{} + } + + file, err := os.Open(filename) + + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + + return ParseReader(file) +} + +// ParseReader parses the PO file into a MessageCatalog +func ParseReader(r io.Reader) MessageCatalog { + scanner := bufio.NewScanner(r) + return parseCatalog(scanner) +} + +func parseCatalog(scanner *bufio.Scanner) MessageCatalog { + const ( + StateWhitespace = 0 + StateComment = 1 + StateMessageID = 2 + StateMessageValue = 3 + ) + + state := StateWhitespace + catalog := MessageCatalog{} + comments := []string{} + id := "" + value := "" + for { + more := scanner.Scan() + + if !more { + if state != StateWhitespace { + catalog.Add(id, value, comments) + } + break + } + + line := scanner.Text() + + if state == StateWhitespace && strings.TrimSpace(line) == "" { + continue + } else if state != StateWhitespace && strings.TrimSpace(line) == "" { + catalog.Add(id, value, comments) + state = StateWhitespace + id = "" + value = "" + comments = []string{} + continue + } + + if strings.HasPrefix(line, "#") { + state = StateComment + comments = append(comments, line) + continue + } + + if strings.HasPrefix(line, "msgid") { + state = StateMessageID + id += mustUnquote(strings.TrimLeft(line, "msgid ")) + continue + } + + if state == StateMessageID && strings.HasPrefix(line, "\"") { + id += mustUnquote(line) + continue + } + + if strings.HasPrefix(line, "msgstr") { + state = StateMessageValue + value += mustUnquote(strings.TrimLeft(line, "msgstr ")) + continue + } + + if state == StateMessageValue && strings.HasPrefix(line, "\"") { + value += mustUnquote(line) + continue + } + } + + return catalog +} + +func mustUnquote(line string) string { + v, err := strconv.Unquote(line) + if err != nil { + fmt.Println(err.Error()) + os.Exit(1) + } + return v +} + +func fileExists(filename string) bool { + info, err := os.Stat(filename) + if os.IsNotExist(err) { + return false + } + return !info.IsDir() +} diff --git a/i18n/cmd/po/parser_test.go b/i18n/cmd/po/parser_test.go new file mode 100644 index 00000000000..010cb574d71 --- /dev/null +++ b/i18n/cmd/po/parser_test.go @@ -0,0 +1,42 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package po + +import ( + "bytes" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParser(t *testing.T) { + r := strings.NewReader(` +msgid "\"a\"\nline" +msgstr "value\nline" + `) + catalog := ParseReader(r) + + var buf bytes.Buffer + catalog.Write(&buf) + + require.Equal(t, `msgid "\"a\"\n" +"line" +msgstr "value\n" +"line" + +`, buf.String()) +} diff --git a/i18n/data/.gitkeep b/i18n/data/.gitkeep new file mode 100644 index 00000000000..e69de29bb2d diff --git a/i18n/data/en.po b/i18n/data/en.po new file mode 100644 index 00000000000..7129df7e31e --- /dev/null +++ b/i18n/data/en.po @@ -0,0 +1,35 @@ +msgid "" +msgstr "" + +#: cli/usage.go:31 +msgid "Additional help topics:" +msgstr "Additional help topics:" + +#: cli/usage.go:26 +msgid "Aliases:" +msgstr "Aliases:" + +#: cli/usage.go:28 +msgid "Available Commands:" +msgstr "Available Commands:" + +#: cli/usage.go:27 +msgid "Examples:" +msgstr "Examples:" + +#: cli/usage.go:29 +msgid "Flags:" +msgstr "Flags:" + +#: cli/usage.go:30 +msgid "Global Flags:" +msgstr "Global Flags:" + +#: cli/usage.go:25 +msgid "Usage:" +msgstr "Usage:" + +#: cli/usage.go:32 +msgid "Use %s for more information about a command." +msgstr "Use %s for more information about a command." + diff --git a/i18n/data/pt_BR.po b/i18n/data/pt_BR.po new file mode 100644 index 00000000000..90a16da7c57 --- /dev/null +++ b/i18n/data/pt_BR.po @@ -0,0 +1,35 @@ +msgid "" +msgstr "" + +#: cli/usage.go:31 +msgid "Additional help topics:" +msgstr "" + +#: cli/usage.go:26 +msgid "Aliases:" +msgstr "" + +#: cli/usage.go:28 +msgid "Available Commands:" +msgstr "" + +#: cli/usage.go:27 +msgid "Examples:" +msgstr "" + +#: cli/usage.go:29 +msgid "Flags:" +msgstr "" + +#: cli/usage.go:30 +msgid "Global Flags:" +msgstr "" + +#: cli/usage.go:25 +msgid "Usage:" +msgstr "" + +#: cli/usage.go:32 +msgid "Use %s for more information about a command." +msgstr "" + diff --git a/i18n/detect.go b/i18n/detect.go new file mode 100644 index 00000000000..f35d4f1b94e --- /dev/null +++ b/i18n/detect.go @@ -0,0 +1,35 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package i18n + +import ( + "os" + "strings" +) + +func getLocaleIdentifierFromOS() string { + return getLocaleIdentifier() +} + +func getLocaleIdentifierFromEnv() string { + locale := os.Getenv("LC_ALL") + + if locale == "" { + locale = os.Getenv("LANG") + } + + return strings.Split(locale, ".")[0] +} diff --git a/i18n/detect_darwin.go b/i18n/detect_darwin.go new file mode 100644 index 00000000000..00503ba0ae8 --- /dev/null +++ b/i18n/detect_darwin.go @@ -0,0 +1,37 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package i18n + +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Foundation +#import + +const char* getLocaleIdentifier() { + NSString *cs = [[NSLocale currentLocale] localeIdentifier]; + const char *cstr = [cs UTF8String]; + return cstr; +} + +*/ +import "C" + +func getLocaleIdentifier() string { + if envLocale := getLocaleIdentifierFromEnv(); envLocale != "" { + return envLocale + } + return C.GoString(C.getLocaleIdentifier()) +} diff --git a/i18n/detect_linux.go b/i18n/detect_linux.go new file mode 100644 index 00000000000..759509c5abe --- /dev/null +++ b/i18n/detect_linux.go @@ -0,0 +1,20 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package i18n + +func getLocaleIdentifier() string { + return getLocaleIdentifierFromEnv() +} diff --git a/i18n/detect_windows.go b/i18n/detect_windows.go new file mode 100644 index 00000000000..2210bfa2553 --- /dev/null +++ b/i18n/detect_windows.go @@ -0,0 +1,47 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package i18n + +import ( + "fmt" + "strings" + "syscall" + "unsafe" +) + +func getLocaleIdentifier() string { + defer func() { + if r := recover(); r != nil { + fmt.Println("failed to get windows user locale", r) + } + }() + + dll := syscall.MustLoadDLL("kernel32") + defer dll.Release() + proc := dll.MustFindProc("GetUserDefaultLocaleName") + + localeNameMaxLen := 85 + buffer := make([]uint16, localeNameMaxLen) + len, _, err := proc.Call(uintptr(unsafe.Pointer(&buffer[0])), uintptr(localeNameMaxLen)) + + if len == 0 { + panic(err) + } + + locale := syscall.UTF16ToString(buffer) + + return strings.ReplaceAll(locale, "-", "_") +} diff --git a/i18n/i18n.go b/i18n/i18n.go new file mode 100644 index 00000000000..7e60ffd17bc --- /dev/null +++ b/i18n/i18n.go @@ -0,0 +1,51 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package i18n + +import ( + "github.com/spf13/viper" +) + +// Init initializes the i18n module, setting the locale according to this order of preference: +// 1. Configuration set in arduino-cli.yaml +// 2. OS Locale +// 3. en (default) +func Init() { + initRiceBox() + locales := supportedLocales() + + if configLocale := viper.GetString("locale"); configLocale != "" { + if locale := findMatchingLocale(configLocale, locales); locale != "" { + setLocale(locale) + return + } + } + + if osLocale := getLocaleIdentifierFromOS(); osLocale != "" { + if locale := findMatchingLocale(osLocale, locales); locale != "" { + setLocale(locale) + return + } + } + + setLocale("en") +} + +// Tr returns msg translated to the selected locale +// the msg argument must be a literal string +func Tr(msg string, args ...interface{}) string { + return po.Get(msg, args...) +} diff --git a/i18n/i18n_test.go b/i18n/i18n_test.go new file mode 100644 index 00000000000..207673fb85b --- /dev/null +++ b/i18n/i18n_test.go @@ -0,0 +1,97 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package i18n + +import ( + "bytes" + "testing" + "text/template" + + "github.com/leonelquinteros/gotext" + "github.com/stretchr/testify/require" +) + +func setPo(poFile string) { + po = new(gotext.Po) + po.Parse([]byte(poFile)) +} + +func TestPoTranslation(t *testing.T) { + setPo(` + msgid "test-key-ok" + msgstr "test-key-translated" + `) + require.Equal(t, "test-key", Tr("test-key")) + require.Equal(t, "test-key-translated", Tr("test-key-ok")) +} + +func TestNoLocaleSet(t *testing.T) { + po = new(gotext.Po) + require.Equal(t, "test-key", Tr("test-key")) +} + +func TestTranslationWithVariables(t *testing.T) { + setPo(` + msgid "test-key-ok %s" + msgstr "test-key-translated %s" + `) + require.Equal(t, "test-key", Tr("test-key")) + require.Equal(t, "test-key-translated message", Tr("test-key-ok %s", "message")) +} + +func TestTranslationInTemplate(t *testing.T) { + setPo(` + msgid "test-key" + msgstr "test-key-translated %s" + `) + + tpl, err := template.New("test-template").Funcs(template.FuncMap{ + "tr": Tr, + }).Parse(`{{ tr "test-key" .Value }}`) + require.NoError(t, err) + + data := struct { + Value string + }{ + "value", + } + var buf bytes.Buffer + require.NoError(t, tpl.Execute(&buf, data)) + + require.Equal(t, "test-key-translated value", buf.String()) +} + +func TestTranslationWithQuotedStrings(t *testing.T) { + setPo(` + msgid "test-key \"quoted\"" + msgstr "test-key-translated" + `) + + require.Equal(t, "test-key-translated", Tr("test-key \"quoted\"")) + require.Equal(t, "test-key-translated", Tr(`test-key "quoted"`)) +} + +func TestTranslationWithLineBreaks(t *testing.T) { + setPo(` + msgid "test-key \"quoted\"\n" + "new line" + msgstr "test-key-translated" + `) + + require.Equal(t, "test-key-translated", Tr("test-key \"quoted\"\nnew line")) + require.Equal(t, "test-key-translated", Tr(`test-key "quoted" +new line`)) +} diff --git a/i18n/locale.go b/i18n/locale.go new file mode 100644 index 00000000000..9e9b899bd2a --- /dev/null +++ b/i18n/locale.go @@ -0,0 +1,84 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package i18n + +import ( + "os" + "path/filepath" + "strings" + "sync" + + rice "github.com/GeertJohan/go.rice" + "github.com/leonelquinteros/gotext" +) + +var ( + loadOnce sync.Once + po *gotext.Po + box *rice.Box +) + +func init() { + po = new(gotext.Po) +} + +func initRiceBox() { + box = rice.MustFindBox("./data") +} + +func supportedLocales() []string { + var locales []string + box.Walk("", func(path string, info os.FileInfo, err error) error { + if filepath.Ext(path) == ".po" { + locales = append(locales, strings.TrimSuffix(path, ".po")) + } + return nil + }) + return locales +} + +func findMatchingLanguage(language string, supportedLocales []string) string { + var matchingLocales []string + for _, supportedLocale := range supportedLocales { + if strings.HasPrefix(supportedLocale, language) { + matchingLocales = append(matchingLocales, supportedLocale) + } + } + + if len(matchingLocales) == 1 { + return matchingLocales[0] + } + + return "" +} + +func findMatchingLocale(locale string, supportedLocales []string) string { + for _, suportedLocale := range supportedLocales { + if locale == suportedLocale { + return suportedLocale + } + } + + parts := strings.Split(locale, "_") + + return findMatchingLanguage(parts[0], supportedLocales) +} + +func setLocale(locale string) { + poFile := box.MustBytes(locale + ".po") + po = new(gotext.Po) + po.Parse(poFile) +} diff --git a/i18n/locale_test.go b/i18n/locale_test.go new file mode 100644 index 00000000000..6212258e8f8 --- /dev/null +++ b/i18n/locale_test.go @@ -0,0 +1,38 @@ +// This file is part of arduino-cli. +// +// Copyright 2020 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package i18n + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestLocaleMatch(t *testing.T) { + supportedLocales := []string{ + "en", + "pt_BR", + "it_IT", + "es_CO", + "es_ES", + } + + require.Equal(t, "pt_BR", findMatchingLocale("pt", supportedLocales), "Language match") + require.Equal(t, "pt_BR", findMatchingLocale("pt_BR", supportedLocales), "Exact match") + require.Equal(t, "pt_BR", findMatchingLocale("pt_PT", supportedLocales), "Language match with country") + require.Equal(t, "", findMatchingLocale("es", supportedLocales), "Multiple languages match") + require.Equal(t, "", findMatchingLocale("zn_CH", supportedLocales), "Not supported") +} diff --git a/i18n/rice-box.go b/i18n/rice-box.go new file mode 100644 index 00000000000..910d9d250d1 --- /dev/null +++ b/i18n/rice-box.go @@ -0,0 +1,59 @@ +package i18n + +import ( + "time" + + "github.com/GeertJohan/go.rice/embedded" +) + +func init() { + + // define files + file2 := &embedded.EmbeddedFile{ + Filename: ".gitkeep", + FileModTime: time.Unix(1589851571, 0), + + Content: string(""), + } + file3 := &embedded.EmbeddedFile{ + Filename: "en.po", + FileModTime: time.Unix(1590158852, 0), + + Content: string("msgid \"\"\nmsgstr \"\"\n\n#: cli/usage.go:31\nmsgid \"Additional help topics:\"\nmsgstr \"Additional help topics:\"\n\n#: cli/usage.go:26\nmsgid \"Aliases:\"\nmsgstr \"Aliases:\"\n\n#: cli/usage.go:28\nmsgid \"Available Commands:\"\nmsgstr \"Available Commands:\"\n\n#: cli/usage.go:27\nmsgid \"Examples:\"\nmsgstr \"Examples:\"\n\n#: cli/usage.go:29\nmsgid \"Flags:\"\nmsgstr \"Flags:\"\n\n#: cli/usage.go:30\nmsgid \"Global Flags:\"\nmsgstr \"Global Flags:\"\n\n#: cli/usage.go:25\nmsgid \"Usage:\"\nmsgstr \"Usage:\"\n\n#: cli/usage.go:32\nmsgid \"Use %s for more information about a command.\"\nmsgstr \"Use %s for more information about a command.\"\n\n"), + } + file4 := &embedded.EmbeddedFile{ + Filename: "pt_BR.po", + FileModTime: time.Unix(1590158853, 0), + + Content: string("msgid \"\"\nmsgstr \"\"\n\n#: cli/usage.go:31\nmsgid \"Additional help topics:\"\nmsgstr \"\"\n\n#: cli/usage.go:26\nmsgid \"Aliases:\"\nmsgstr \"\"\n\n#: cli/usage.go:28\nmsgid \"Available Commands:\"\nmsgstr \"\"\n\n#: cli/usage.go:27\nmsgid \"Examples:\"\nmsgstr \"\"\n\n#: cli/usage.go:29\nmsgid \"Flags:\"\nmsgstr \"\"\n\n#: cli/usage.go:30\nmsgid \"Global Flags:\"\nmsgstr \"\"\n\n#: cli/usage.go:25\nmsgid \"Usage:\"\nmsgstr \"\"\n\n#: cli/usage.go:32\nmsgid \"Use %s for more information about a command.\"\nmsgstr \"\"\n\n"), + } + + // define dirs + dir1 := &embedded.EmbeddedDir{ + Filename: "", + DirModTime: time.Unix(1590158853, 0), + ChildFiles: []*embedded.EmbeddedFile{ + file2, // ".gitkeep" + file3, // "en.po" + file4, // "pt_BR.po" + + }, + } + + // link ChildDirs + dir1.ChildDirs = []*embedded.EmbeddedDir{} + + // register embeddedBox + embedded.RegisterEmbeddedBox(`./data`, &embedded.EmbeddedBox{ + Name: `./data`, + Time: time.Unix(1590158853, 0), + Dirs: map[string]*embedded.EmbeddedDir{ + "": dir1, + }, + Files: map[string]*embedded.EmbeddedFile{ + ".gitkeep": file2, + "en.po": file3, + "pt_BR.po": file4, + }, + }) +} diff --git a/main.go b/main.go index 4ab55ba9d8e..d4a43cf81c8 100644 --- a/main.go +++ b/main.go @@ -20,10 +20,15 @@ import ( "github.com/arduino/arduino-cli/cli" "github.com/arduino/arduino-cli/cli/errorcodes" + "github.com/arduino/arduino-cli/configuration" + "github.com/arduino/arduino-cli/i18n" ) func main() { - if err := cli.ArduinoCli.Execute(); err != nil { + configuration.Init(configuration.FindConfigFile()) + i18n.Init() + arduinoCmd := cli.NewCommand() + if err := arduinoCmd.Execute(); err != nil { os.Exit(errorcodes.ErrGeneric) } }