diff options
author | Simon Rozman <simon@rozman.si> | 2019-11-14 09:27:05 +0100 |
---|---|---|
committer | Jason A. Donenfeld <Jason@zx2c4.com> | 2020-03-15 14:49:27 -0600 |
commit | 8a476b326136f5c03790fa168686848884c8cd5a (patch) | |
tree | 3fac4f2940c3012734a4147ef627e2b52a27a859 | |
parent | manager: chdir into unelevated profile before execing (diff) | |
download | wireguard-windows-8a476b326136f5c03790fa168686848884c8cd5a.tar.xz wireguard-windows-8a476b326136f5c03790fa168686848884c8cd5a.zip |
l18n: add localization support
Revise the messages to make them localizable.
Note: The log messages are not marked for localization. Probably, we
want to keep log files in English for easier global troubleshooting.
Having a user run `go generate` requires a valid and up-to-date Go
environment. Rather than instructing users how to setup the environment
correctly, the `go generate` was integrated into build.bat. This reuses
the Go building environment downloaded and prepared by build.bat to
provide controllable and consistent result.
Use `make generate` on Linux.
As the zgotext.go output varies for GOARCH=386 and amd64, one had to be
chosen to provide stable output. The former is the first one to build in
build.bat.
Signed-off-by: Simon Rozman <simon@rozman.si>
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | Makefile | 5 | ||||
-rw-r--r-- | README.md | 22 | ||||
-rw-r--r-- | build.bat | 4 | ||||
-rw-r--r-- | conf/config.go | 44 | ||||
-rw-r--r-- | conf/parser.go | 63 | ||||
-rw-r--r-- | go.sum | 1 | ||||
-rw-r--r-- | l18n/l18n.go | 65 | ||||
-rw-r--r-- | locales/en/messages.gotext.json | 1949 | ||||
-rw-r--r-- | main.go | 38 | ||||
-rw-r--r-- | resources.rc | 64 | ||||
-rw-r--r-- | ui/aboutdialog.go | 12 | ||||
-rw-r--r-- | ui/confview.go | 54 | ||||
-rw-r--r-- | ui/editdialog.go | 34 | ||||
-rw-r--r-- | ui/filesave.go | 7 | ||||
-rw-r--r-- | ui/iconprovider.go | 11 | ||||
-rw-r--r-- | ui/logpage.go | 19 | ||||
-rw-r--r-- | ui/managewindow.go | 7 | ||||
-rw-r--r-- | ui/raise.go | 5 | ||||
-rw-r--r-- | ui/tray.go | 41 | ||||
-rw-r--r-- | ui/tunnelspage.go | 83 | ||||
-rw-r--r-- | ui/ui.go | 7 | ||||
-rw-r--r-- | ui/updatepage.go | 23 | ||||
-rw-r--r-- | zgotext.go | 357 |
24 files changed, 2669 insertions, 247 deletions
@@ -6,6 +6,7 @@ /amd64 # Misc +/locales/*/out.gotext.json /sign.bat *.swp *.bak @@ -46,6 +46,11 @@ fmt: export GOARCH := amd64 fmt: go fmt ./... +generate: export CC := i686-w64-mingw32-gcc +generate: export GOARCH := 386 +generate: + go generate + deploy: amd64/wireguard.exe -ssh $(DEPLOYMENT_HOST) -- 'taskkill /im wireguard.exe /f' scp $< $(DEPLOYMENT_HOST):$(DEPLOYMENT_PATH) @@ -24,6 +24,28 @@ C:\Projects\wireguard-windows> amd64\wireguard.exe Since WireGuard requires the Wintun driver to be installed, and this generally requires a valid Microsoft signature, you may benefit from first installing a release of WireGuard for Windows from the official [wireguard.com](https://www.wireguard.com/install/) builds, which bundles a Microsoft-signed Wintun, and then subsequently run your own wireguard.exe. Alternatively, you can craft your own installer using the `quickinstall.bat` script. +### Optional: Localizing + +To translate WireGuard UI to your language: + +1. Upgrade `resources.rc` accordingly. Follow the pattern. + +2. Add your language ID to the `//go:generate go run golang.org/x/text/cmd/gotext ... -lang=en,<langID>...` line in `main.go`. + +3. Configure and run `build` to prepare initial `locales\<langID>\messages.gotext.json` file: + + ``` + C:\Projects\wireguard-windows> set GenerateLocalizations=yes + C:\Projects\wireguard-windows> build + C:\Projects\wireguard-windows> copy locales\<langID>\out.gotext.json locales\<langID>\messages.gotext.json + ``` + +4. Translate `locales\<langID>\messages.gotext.json`. See other language message files how to translate messages and how to tackle plural. + +5. Run `build` from the step 3 again, and test. + +6. Repeat from step 4. + ### Optional: Creating the Installer The installer build script will take care of downloading, verifying, and extracting the right versions of the various dependencies: @@ -70,6 +70,10 @@ if exist .deps\prepared goto :render mkdir %1 >NUL 2>&1 echo [+] Assembling resources %1 windres -i resources.rc -o resources.syso -O coff || exit /b %errorlevel% + if "%GenerateLocalizations%|%1"=="yes|x86" ( + echo [+] Generating localizations %1 + go generate || exit /b 1 + ) echo [+] Building program %1 go build -ldflags="-H windowsgui -s -w" -tags walk_use_cgo -trimpath -v -o "%~1\wireguard.exe" || exit /b 1 if not exist "%~1\wg.exe" ( diff --git a/conf/config.go b/conf/config.go index a84dc418..f4d8478a 100644 --- a/conf/config.go +++ b/conf/config.go @@ -16,6 +16,8 @@ import ( "time" "golang.org/x/crypto/curve25519" + + "golang.zx2c4.com/wireguard/windows/l18n" ) const KeyLength = 32 @@ -131,18 +133,6 @@ func NewPrivateKeyFromString(b64 string) (*Key, error) { return parseKeyBase64(b64) } -func formatInterval(i int64, n string, l int) string { - r := "" - if l > 0 { - r += ", " - } - r += fmt.Sprintf("%d %s", i, n) - if i != 1 { - r += "s" - } - return r -} - func (t HandshakeTime) IsEmpty() bool { return t == HandshakeTime(0) } @@ -151,9 +141,9 @@ func (t HandshakeTime) String() string { u := time.Unix(0, 0).Add(time.Duration(t)).Unix() n := time.Now().Unix() if u == n { - return "Now" + return l18n.Sprintf("Now") } else if u > n { - return "System clock wound backward!" + return l18n.Sprintf("System clock wound backward!") } left := n - u years := left / (365 * 24 * 60 * 60) @@ -164,37 +154,37 @@ func (t HandshakeTime) String() string { left = left % (60 * 60) minutes := left / 60 seconds := left % 60 - s := "" + s := make([]string, 0, 5) if years > 0 { - s += formatInterval(years, "year", len(s)) + s = append(s, l18n.Sprintf("%d year(s)", years)) } if days > 0 { - s += formatInterval(days, "day", len(s)) + s = append(s, l18n.Sprintf("%d day(s)", days)) } if hours > 0 { - s += formatInterval(hours, "hour", len(s)) + s = append(s, l18n.Sprintf("%d hour(s)", hours)) } if minutes > 0 { - s += formatInterval(minutes, "minute", len(s)) + s = append(s, l18n.Sprintf("%d minute(s)", minutes)) } if seconds > 0 { - s += formatInterval(seconds, "second", len(s)) + s = append(s, l18n.Sprintf("%d second(s)", seconds)) } - s += " ago" - return s + timestamp := strings.Join(s, l18n.EnumerationSeparator()) + return l18n.Sprintf("%s ago", timestamp) } func (b Bytes) String() string { if b < 1024 { - return fmt.Sprintf("%d B", b) + return l18n.Sprintf("%d\u00a0B", b) } else if b < 1024*1024 { - return fmt.Sprintf("%.2f KiB", float64(b)/1024) + return l18n.Sprintf("%.2f\u00a0KiB", float64(b)/1024) } else if b < 1024*1024*1024 { - return fmt.Sprintf("%.2f MiB", float64(b)/(1024*1024)) + return l18n.Sprintf("%.2f\u00a0MiB", float64(b)/(1024*1024)) } else if b < 1024*1024*1024*1024 { - return fmt.Sprintf("%.2f GiB", float64(b)/(1024*1024*1024)) + return l18n.Sprintf("%.2f\u00a0GiB", float64(b)/(1024*1024*1024)) } - return fmt.Sprintf("%.2f TiB", float64(b)/(1024*1024*1024)/1024) + return l18n.Sprintf("%.2f\u00a0TiB", float64(b)/(1024*1024*1024)/1024) } func (conf *Config) DeduplicateNetworkEntries() { diff --git a/conf/parser.go b/conf/parser.go index 3f64677b..5f44edb2 100644 --- a/conf/parser.go +++ b/conf/parser.go @@ -8,13 +8,14 @@ package conf import ( "encoding/base64" "encoding/hex" - "fmt" "net" "strconv" "strings" "time" "golang.org/x/text/encoding/unicode" + + "golang.zx2c4.com/wireguard/windows/l18n" ) type ParseError struct { @@ -23,7 +24,7 @@ type ParseError struct { } func (e *ParseError) Error() string { - return fmt.Sprintf("%s: %q", e.why, e.offender) + return l18n.Sprintf("%s: %q", e.why, e.offender) } func parseIPCidr(s string) (ipcidr *IPCidr, err error) { @@ -37,7 +38,7 @@ func parseIPCidr(s string) (ipcidr *IPCidr, err error) { addrStr, cidrStr = s[:i], s[i+1:] } - err = &ParseError{"Invalid IP address", s} + err = &ParseError{l18n.Sprintf("Invalid IP address"), s} addr := net.ParseIP(addrStr) if addr == nil { return @@ -47,7 +48,7 @@ func parseIPCidr(s string) (ipcidr *IPCidr, err error) { addr = maybeV4 } if len(cidrStr) > 0 { - err = &ParseError{"Invalid network prefix length", s} + err = &ParseError{l18n.Sprintf("Invalid network prefix length"), s} cidr, err = strconv.Atoi(cidrStr) if err != nil || cidr < 0 || cidr > 128 { return @@ -68,11 +69,11 @@ func parseIPCidr(s string) (ipcidr *IPCidr, err error) { func parseEndpoint(s string) (*Endpoint, error) { i := strings.LastIndexByte(s, ':') if i < 0 { - return nil, &ParseError{"Missing port from endpoint", s} + return nil, &ParseError{l18n.Sprintf("Missing port from endpoint"), s} } host, portStr := s[:i], s[i+1:] if len(host) < 1 { - return nil, &ParseError{"Invalid endpoint host", host} + return nil, &ParseError{l18n.Sprintf("Invalid endpoint host"), host} } port, err := parsePort(portStr) if err != nil { @@ -80,7 +81,7 @@ func parseEndpoint(s string) (*Endpoint, error) { } hostColon := strings.IndexByte(host, ':') if host[0] == '[' || host[len(host)-1] == ']' || hostColon > 0 { - err := &ParseError{"Brackets must contain an IPv6 address", host} + err := &ParseError{l18n.Sprintf("Brackets must contain an IPv6 address"), host} if len(host) > 3 && host[0] == '[' && host[len(host)-1] == ']' && hostColon > 0 { end := len(host) - 1 if i := strings.LastIndexByte(host, '%'); i > 1 { @@ -104,7 +105,7 @@ func parseMTU(s string) (uint16, error) { return 0, err } if m < 576 || m > 65535 { - return 0, &ParseError{"Invalid MTU", s} + return 0, &ParseError{l18n.Sprintf("Invalid MTU"), s} } return uint16(m), nil } @@ -115,7 +116,7 @@ func parsePort(s string) (uint16, error) { return 0, err } if m < 0 || m > 65535 { - return 0, &ParseError{"Invalid port", s} + return 0, &ParseError{l18n.Sprintf("Invalid port"), s} } return uint16(m), nil } @@ -129,7 +130,7 @@ func parsePersistentKeepalive(s string) (uint16, error) { return 0, err } if m < 0 || m > 65535 { - return 0, &ParseError{"Invalid persistent keepalive", s} + return 0, &ParseError{l18n.Sprintf("Invalid persistent keepalive"), s} } return uint16(m), nil } @@ -137,10 +138,10 @@ func parsePersistentKeepalive(s string) (uint16, error) { func parseKeyBase64(s string) (*Key, error) { k, err := base64.StdEncoding.DecodeString(s) if err != nil { - return nil, &ParseError{"Invalid key: " + err.Error(), s} + return nil, &ParseError{l18n.Sprintf("Invalid key: %v", err), s} } if len(k) != KeyLength { - return nil, &ParseError{"Keys must decode to exactly 32 bytes", s} + return nil, &ParseError{l18n.Sprintf("Keys must decode to exactly 32 bytes"), s} } var key Key copy(key[:], k) @@ -150,10 +151,10 @@ func parseKeyBase64(s string) (*Key, error) { func parseKeyHex(s string) (*Key, error) { k, err := hex.DecodeString(s) if err != nil { - return nil, &ParseError{"Invalid key: " + err.Error(), s} + return nil, &ParseError{l18n.Sprintf("Invalid key: %v", err), s} } if len(k) != KeyLength { - return nil, &ParseError{"Keys must decode to exactly 32 bytes", s} + return nil, &ParseError{l18n.Sprintf("Keys must decode to exactly 32 bytes"), s} } var key Key copy(key[:], k) @@ -163,7 +164,7 @@ func parseKeyHex(s string) (*Key, error) { func parseBytesOrStamp(s string) (uint64, error) { b, err := strconv.ParseUint(s, 10, 64) if err != nil { - return 0, &ParseError{"Number must be a number between 0 and 2^64-1: " + err.Error(), s} + return 0, &ParseError{l18n.Sprintf("Number must be a number between 0 and 2^64-1: %v", err), s} } return b, nil } @@ -173,7 +174,7 @@ func splitList(s string) ([]string, error) { for _, split := range strings.Split(s, ",") { trim := strings.TrimSpace(split) if len(trim) == 0 { - return nil, &ParseError{"Two commas in a row", s} + return nil, &ParseError{l18n.Sprintf("Two commas in a row"), s} } out = append(out, trim) } @@ -196,7 +197,7 @@ func (c *Config) maybeAddPeer(p *Peer) { func FromWgQuick(s string, name string) (*Config, error) { if !TunnelNameIsValid(name) { - return nil, &ParseError{"Tunnel name is not valid", name} + return nil, &ParseError{l18n.Sprintf("Tunnel name is not valid"), name} } lines := strings.Split(s, "\n") parserState := notInASection @@ -225,15 +226,15 @@ func FromWgQuick(s string, name string) (*Config, error) { continue } if parserState == notInASection { - return nil, &ParseError{"Line must occur in a section", line} + return nil, &ParseError{l18n.Sprintf("Line must occur in a section"), line} } equals := strings.IndexByte(line, '=') if equals < 0 { - return nil, &ParseError{"Invalid config key is missing an equals separator", line} + return nil, &ParseError{l18n.Sprintf("Invalid config key is missing an equals separator"), line} } key, val := strings.TrimSpace(lineLower[:equals]), strings.TrimSpace(line[equals+1:]) if len(val) == 0 { - return nil, &ParseError{"Key must have a value", line} + return nil, &ParseError{l18n.Sprintf("Key must have a value"), line} } if parserState == inInterfaceSection { switch key { @@ -276,12 +277,12 @@ func FromWgQuick(s string, name string) (*Config, error) { for _, address := range addresses { a := net.ParseIP(address) if a == nil { - return nil, &ParseError{"Invalid IP address", address} + return nil, &ParseError{l18n.Sprintf("Invalid IP address"), address} } conf.Interface.DNS = append(conf.Interface.DNS, a) } default: - return nil, &ParseError{"Invalid key for [Interface] section", key} + return nil, &ParseError{l18n.Sprintf("Invalid key for [Interface] section"), key} } } else if parserState == inPeerSection { switch key { @@ -322,18 +323,18 @@ func FromWgQuick(s string, name string) (*Config, error) { } peer.Endpoint = *e default: - return nil, &ParseError{"Invalid key for [Peer] section", key} + return nil, &ParseError{l18n.Sprintf("Invalid key for [Peer] section"), key} } } } conf.maybeAddPeer(peer) if !sawPrivateKey { - return nil, &ParseError{"An interface must have a private key", "[none specified]"} + return nil, &ParseError{l18n.Sprintf("An interface must have a private key"), l18n.Sprintf("[none specified]")} } for _, p := range conf.Peers { if p.PublicKey.IsZero() { - return nil, &ParseError{"All peers must have public keys", "[none specified]"} + return nil, &ParseError{l18n.Sprintf("All peers must have public keys"), l18n.Sprintf("[none specified]")} } } @@ -375,11 +376,11 @@ func FromUAPI(s string, existingConfig *Config) (*Config, error) { } equals := strings.IndexByte(line, '=') if equals < 0 { - return nil, &ParseError{"Invalid config key is missing an equals separator", line} + return nil, &ParseError{l18n.Sprintf("Invalid config key is missing an equals separator"), line} } key, val := line[:equals], line[equals+1:] if len(val) == 0 { - return nil, &ParseError{"Key must have a value", line} + return nil, &ParseError{l18n.Sprintf("Key must have a value"), line} } switch key { case "public_key": @@ -390,7 +391,7 @@ func FromUAPI(s string, existingConfig *Config) (*Config, error) { if val == "0" { continue } else { - return nil, &ParseError{"Error in getting configuration", val} + return nil, &ParseError{l18n.Sprintf("Error in getting configuration"), val} } } if parserState == inInterfaceSection { @@ -411,7 +412,7 @@ func FromUAPI(s string, existingConfig *Config) (*Config, error) { // Ignored for now. default: - return nil, &ParseError{"Invalid key for interface section", key} + return nil, &ParseError{l18n.Sprintf("Invalid key for interface section"), key} } } else if parserState == inPeerSection { switch key { @@ -429,7 +430,7 @@ func FromUAPI(s string, existingConfig *Config) (*Config, error) { peer.PresharedKey = *k case "protocol_version": if val != "1" { - return nil, &ParseError{"Protocol version must be 1", val} + return nil, &ParseError{l18n.Sprintf("Protocol version must be 1"), val} } case "allowed_ip": a, err := parseIPCidr(val) @@ -474,7 +475,7 @@ func FromUAPI(s string, existingConfig *Config) (*Config, error) { } peer.LastHandshakeTime += HandshakeTime(time.Duration(t) * time.Nanosecond) default: - return nil, &ParseError{"Invalid key for peer section", key} + return nil, &ParseError{l18n.Sprintf("Invalid key for peer section"), key} } } } @@ -16,6 +16,7 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3-0.20191230102452-929e72ca90de h1:aYKJLPSrddB2N7/6OKyFqJ337SXpo61bBuvO5p1+7iY= golang.org/x/text v0.3.3-0.20191230102452-929e72ca90de/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e h1:FDhOuMEY4JVRztM/gsbk+IKUQ8kj74bxZrgw87eMMVc= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.zx2c4.com/wireguard v0.0.20200122-0.20200214175355-9cbcff10dd3e h1:cEeaW64u6+FcYcO4/M+rpqJhb9pFl/9CGs5oqlN6ps8= golang.zx2c4.com/wireguard v0.0.20200122-0.20200214175355-9cbcff10dd3e/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4= diff --git a/l18n/l18n.go b/l18n/l18n.go new file mode 100644 index 00000000..717e4861 --- /dev/null +++ b/l18n/l18n.go @@ -0,0 +1,65 @@ +/* SPDX-License-Identifier: MIT + * + * Copyright (C) 2019 WireGuard LLC. All Rights Reserved. + */ + +package l18n + +import ( + "sync" + + "golang.org/x/sys/windows" + "golang.org/x/text/language" + "golang.org/x/text/message" +) + +var printer *message.Printer +var printerLock sync.Mutex + +// prn returns the printer for user preferred UI language. +func prn() *message.Printer { + if printer != nil { + return printer + } + printerLock.Lock() + if printer != nil { + printerLock.Unlock() + return printer + } + printer = message.NewPrinter(lang()) + printerLock.Unlock() + return printer +} + +// lang returns the user preferred UI language we have most confident translation in the default catalog available. +func lang() (tag language.Tag) { + tag = language.English + confidence := language.No + languages, err := windows.GetUserPreferredUILanguages(windows.MUI_LANGUAGE_NAME) + if err != nil { + return + } + for i := range languages { + t, _, c := message.DefaultCatalog.Matcher().Match(message.MatchLanguage(languages[i])) + if c > confidence { + tag = t + confidence = c + } + } + return +} + +// Sprintf is like fmt.Sprintf, but using language-specific formatting. +func Sprintf(key message.Reference, a ...interface{}) string { + return prn().Sprintf(key, a...) +} + +// EnumerationSeparator returns enumeration separator. For English and western languages, +// enumeration separator is a comma followed by a space (i.e. ", "). For Chinese, it returns +// "\u3001". +func EnumerationSeparator() string { + // BUG: We could just use `Sprintf(", " /* ...translator instructions... */)` and let the + // individual locale catalog handle its translation. Unfortunately, the gotext utility tries to + // be nice to translators and skips all strings without letters when updating catalogs. + return Sprintf("[EnumerationSeparator]" /* Text to insert between items when listing - most western languages will translate ‘[EnumerationSeparator]’ into ‘, ’ to produce lists like ‘apple, orange, strawberry’. Eastern languages might translate into ‘、’ to produce lists like ‘リンゴ、オレンジ、イチゴ’. */) +} diff --git a/locales/en/messages.gotext.json b/locales/en/messages.gotext.json new file mode 100644 index 00000000..cda1f59d --- /dev/null +++ b/locales/en/messages.gotext.json @@ -0,0 +1,1949 @@ +{ + "language": "en", + "messages": [ + { + "id": "Error", + "message": "Error", + "translation": "Error", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "(no argument): elevate and install manager service", + "message": "(no argument): elevate and install manager service", + "translation": "(no argument): elevate and install manager service", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Usage: {Args0} [\n{String}]", + "message": "Usage: {Args0} [\n{String}]", + "translation": "Usage: {Args0} [\n{String}]", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Args0", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "os.Args[0]" + }, + { + "id": "String", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "builder.String()" + } + ], + "fuzzy": true + }, + { + "id": "Command Line Options", + "message": "Command Line Options", + "translation": "Command Line Options", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Unable to determine whether the process is running under WOW64: {Err}", + "message": "Unable to determine whether the process is running under WOW64: {Err}", + "translation": "Unable to determine whether the process is running under WOW64: {Err}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "You must use the 64-bit version of WireGuard on this computer.", + "message": "You must use the 64-bit version of WireGuard on this computer.", + "translation": "You must use the 64-bit version of WireGuard on this computer.", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Unable to open current process token: {Err}", + "message": "Unable to open current process token: {Err}", + "translation": "Unable to open current process token: {Err}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "WireGuard may only be used by users who are a member of the Builtin {AdminGroupName} group.", + "message": "WireGuard may only be used by users who are a member of the Builtin {AdminGroupName} group.", + "translation": "WireGuard may only be used by users who are a member of the Builtin {AdminGroupName} group.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "AdminGroupName", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "elevate.AdminGroupName()" + } + ], + "fuzzy": true + }, + { + "id": "WireGuard is running, but the UI is only accessible from desktops of the Builtin {AdminGroupName} group.", + "message": "WireGuard is running, but the UI is only accessible from desktops of the Builtin {AdminGroupName} group.", + "translation": "WireGuard is running, but the UI is only accessible from desktops of the Builtin {AdminGroupName} group.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "AdminGroupName", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "elevate.AdminGroupName()" + } + ], + "fuzzy": true + }, + { + "id": "WireGuard system tray icon did not appear after 30 seconds.", + "message": "WireGuard system tray icon did not appear after 30 seconds.", + "translation": "WireGuard system tray icon did not appear after 30 seconds.", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Now", + "message": "Now", + "translation": "Now", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "System clock wound backward!", + "message": "System clock wound backward!", + "translation": "System clock wound backward!", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "{Years} year(s)", + "message": "{Years} year(s)", + "translation": { + "select": { + "feature": "plural", + "arg": "Years", + "cases": { + "one": { + "msg": "{Years} year" + }, + "other": { + "msg": "{Years} years" + } + } + } + }, + "placeholders": [ + { + "id": "Years", + "string": "%[1]d", + "type": "int64", + "underlyingType": "int64", + "argNum": 1, + "expr": "years" + } + ] + }, + { + "id": "{Days} day(s)", + "message": "{Days} day(s)", + "translation": { + "select": { + "feature": "plural", + "arg": "Days", + "cases": { + "one": { + "msg": "{Days} day" + }, + "other": { + "msg": "{Days} days" + } + } + } + }, + "placeholders": [ + { + "id": "Days", + "string": "%[1]d", + "type": "int64", + "underlyingType": "int64", + "argNum": 1, + "expr": "days" + } + ] + }, + { + "id": "{Hours} hour(s)", + "message": "{Hours} hour(s)", + "translation": { + "select": { + "feature": "plural", + "arg": "Hours", + "cases": { + "one": { + "msg": "{Hours} hour" + }, + "other": { + "msg": "{Hours} hours" + } + } + } + }, + "placeholders": [ + { + "id": "Hours", + "string": "%[1]d", + "type": "int64", + "underlyingType": "int64", + "argNum": 1, + "expr": "hours" + } + ] + }, + { + "id": "{Minutes} minute(s)", + "message": "{Minutes} minute(s)", + "translation": { + "select": { + "feature": "plural", + "arg": "Minutes", + "cases": { + "one": { + "msg": "{Minutes} minute" + }, + "other": { + "msg": "{Minutes} minutes" + } + } + } + }, + "placeholders": [ + { + "id": "Minutes", + "string": "%[1]d", + "type": "int64", + "underlyingType": "int64", + "argNum": 1, + "expr": "minutes" + } + ] + }, + { + "id": "{Seconds} second(s)", + "message": "{Seconds} second(s)", + "translation": { + "select": { + "feature": "plural", + "arg": "Seconds", + "cases": { + "one": { + "msg": "{Seconds} second" + }, + "other": { + "msg": "{Seconds} seconds" + } + } + } + }, + "placeholders": [ + { + "id": "Seconds", + "string": "%[1]d", + "type": "int64", + "underlyingType": "int64", + "argNum": 1, + "expr": "seconds" + } + ] + }, + { + "id": "{Timestamp} ago", + "message": "{Timestamp} ago", + "translation": "{Timestamp} ago", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Timestamp", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "timestamp" + } + ], + "fuzzy": true + }, + { + "id": "{Bytes} B", + "message": "{Bytes} B", + "translation": "{Bytes} B", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Bytes", + "string": "%[1]d", + "type": "golang.zx2c4.com/wireguard/windows/conf.Bytes", + "underlyingType": "uint64", + "argNum": 1, + "expr": "b" + } + ], + "fuzzy": true + }, + { + "id": "{Float64b__1024} KiB", + "message": "{Float64b__1024} KiB", + "translation": "{Float64b__1024} KiB", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Float64b__1024", + "string": "%.2[1]f", + "type": "float64", + "underlyingType": "float64", + "argNum": 1, + "expr": "float64(b) / 1024" + } + ], + "fuzzy": true + }, + { + "id": "{Float64b__1024__1024} MiB", + "message": "{Float64b__1024__1024} MiB", + "translation": "{Float64b__1024__1024} MiB", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Float64b__1024__1024", + "string": "%.2[1]f", + "type": "float64", + "underlyingType": "float64", + "argNum": 1, + "expr": "float64(b) / (1024 * 1024)" + } + ], + "fuzzy": true + }, + { + "id": "{Float64b__1024__1024__1024} GiB", + "message": "{Float64b__1024__1024__1024} GiB", + "translation": "{Float64b__1024__1024__1024} GiB", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Float64b__1024__1024__1024", + "string": "%.2[1]f", + "type": "float64", + "underlyingType": "float64", + "argNum": 1, + "expr": "float64(b) / (1024 * 1024 * 1024)" + } + ], + "fuzzy": true + }, + { + "id": "{Float64b__1024__1024__1024__1024} TiB", + "message": "{Float64b__1024__1024__1024__1024} TiB", + "translation": "{Float64b__1024__1024__1024__1024} TiB", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Float64b__1024__1024__1024__1024", + "string": "%.2[1]f", + "type": "float64", + "underlyingType": "float64", + "argNum": 1, + "expr": "float64(b) / (1024 * 1024 * 1024) / 1024" + } + ], + "fuzzy": true + }, + { + "id": "{Why}: {Offender}", + "message": "{Why}: {Offender}", + "translation": "{Why}: {Offender}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Why", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "e.why" + }, + { + "id": "Offender", + "string": "%[2]q", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "e.offender" + } + ], + "fuzzy": true + }, + { + "id": "Invalid IP address", + "message": "Invalid IP address", + "translation": "Invalid IP address", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid network prefix length", + "message": "Invalid network prefix length", + "translation": "Invalid network prefix length", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Missing port from endpoint", + "message": "Missing port from endpoint", + "translation": "Missing port from endpoint", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid endpoint host", + "message": "Invalid endpoint host", + "translation": "Invalid endpoint host", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Brackets must contain an IPv6 address", + "message": "Brackets must contain an IPv6 address", + "translation": "Brackets must contain an IPv6 address", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid MTU", + "message": "Invalid MTU", + "translation": "Invalid MTU", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid port", + "message": "Invalid port", + "translation": "Invalid port", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid persistent keepalive", + "message": "Invalid persistent keepalive", + "translation": "Invalid persistent keepalive", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid key: {Err}", + "message": "Invalid key: {Err}", + "translation": "Invalid key: {Err}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "Keys must decode to exactly 32 bytes", + "message": "Keys must decode to exactly 32 bytes", + "translation": "Keys must decode to exactly 32 bytes", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Number must be a number between 0 and 2^64-1: {Err}", + "message": "Number must be a number between 0 and 2^64-1: {Err}", + "translation": "Number must be a number between 0 and 2^64-1: {Err}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "Two commas in a row", + "message": "Two commas in a row", + "translation": "Two commas in a row", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Tunnel name is not valid", + "message": "Tunnel name is not valid", + "translation": "Tunnel name is not valid", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Line must occur in a section", + "message": "Line must occur in a section", + "translation": "Line must occur in a section", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid config key is missing an equals separator", + "message": "Invalid config key is missing an equals separator", + "translation": "Invalid config key is missing an equals separator", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Key must have a value", + "message": "Key must have a value", + "translation": "Key must have a value", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid key for [Interface] section", + "message": "Invalid key for [Interface] section", + "translation": "Invalid key for [Interface] section", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid key for [Peer] section", + "message": "Invalid key for [Peer] section", + "translation": "Invalid key for [Peer] section", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "An interface must have a private key", + "message": "An interface must have a private key", + "translation": "An interface must have a private key", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "[none specified]", + "message": "[none specified]", + "translation": "[none specified]", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "All peers must have public keys", + "message": "All peers must have public keys", + "translation": "All peers must have public keys", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Error in getting configuration", + "message": "Error in getting configuration", + "translation": "Error in getting configuration", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid key for interface section", + "message": "Invalid key for interface section", + "translation": "Invalid key for interface section", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Protocol version must be 1", + "message": "Protocol version must be 1", + "translation": "Protocol version must be 1", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid key for peer section", + "message": "Invalid key for peer section", + "translation": "Invalid key for peer section", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "[EnumerationSeparator]", + "message": "[EnumerationSeparator]", + "translation": ", ", + "comment": "Text to insert between items when listing - most western languages will translate ‘[EnumerationSeparator]’ into ‘, ’ to produce lists like ‘apple, orange, strawberry’. Eastern languages might translate into ‘、’ to produce lists like ‘リンゴ、オレンジ、イチゴ’." + }, + { + "id": "About WireGuard", + "message": "About WireGuard", + "translation": "About WireGuard", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "WireGuard logo image", + "message": "WireGuard logo image", + "translation": "WireGuard logo image", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "App version: {Number}\nGo backend version: {WireGuardGoVersion}\nGo version: {Version_go}\nOperating system: {OsName}\nArchitecture: {GOARCH}", + "message": "App version: {Number}\nGo backend version: {WireGuardGoVersion}\nGo version: {Version_go}\nOperating system: {OsName}\nArchitecture: {GOARCH}", + "translation": "App version: {Number}\nGo backend version: {WireGuardGoVersion}\nGo version: {Version_go}\nOperating system: {OsName}\nArchitecture: {GOARCH}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Number", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "version.Number" + }, + { + "id": "WireGuardGoVersion", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "device.WireGuardGoVersion" + }, + { + "id": "Version_go", + "string": "%[3]s", + "type": "string", + "underlyingType": "string", + "argNum": 3, + "expr": "strings.TrimPrefix(runtime.Version(), \"go\")" + }, + { + "id": "OsName", + "string": "%[4]s", + "type": "string", + "underlyingType": "string", + "argNum": 4, + "expr": "version.OsName()" + }, + { + "id": "GOARCH", + "string": "%[5]s", + "type": "string", + "underlyingType": "string", + "argNum": 5, + "expr": "runtime.GOARCH" + } + ], + "fuzzy": true + }, + { + "id": "Close", + "message": "Close", + "translation": "Close", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "♥ \u0026Donate!", + "message": "♥ \u0026Donate!", + "translation": "♥ \u0026Donate!", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Status:", + "message": "Status:", + "translation": "Status:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Deactivate", + "message": "\u0026Deactivate", + "translation": "\u0026Deactivate", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Activate", + "message": "\u0026Activate", + "translation": "\u0026Activate", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Public key:", + "message": "Public key:", + "translation": "Public key:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Listen port:", + "message": "Listen port:", + "translation": "Listen port:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "MTU:", + "message": "MTU:", + "translation": "MTU:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Addresses:", + "message": "Addresses:", + "translation": "Addresses:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "DNS servers:", + "message": "DNS servers:", + "translation": "DNS servers:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Preshared key:", + "message": "Preshared key:", + "translation": "Preshared key:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Allowed IPs:", + "message": "Allowed IPs:", + "translation": "Allowed IPs:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Endpoint:", + "message": "Endpoint:", + "translation": "Endpoint:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Persistent keepalive:", + "message": "Persistent keepalive:", + "translation": "Persistent keepalive:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Latest handshake:", + "message": "Latest handshake:", + "translation": "Latest handshake:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Transfer:", + "message": "Transfer:", + "translation": "Transfer:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "enabled", + "message": "enabled", + "translation": "enabled", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "{String} received, {String_1} sent", + "message": "{String} received, {String_1} sent", + "translation": "{String} received, {String_1} sent", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "String", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "c.RxBytes.String()" + }, + { + "id": "String_1", + "string": "%[2]s", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "c.TxBytes.String()" + } + ], + "fuzzy": true + }, + { + "id": "Failed to determine tunnel state", + "message": "Failed to determine tunnel state", + "translation": "Failed to determine tunnel state", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Failed to activate tunnel", + "message": "Failed to activate tunnel", + "translation": "Failed to activate tunnel", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Failed to deactivate tunnel", + "message": "Failed to deactivate tunnel", + "translation": "Failed to deactivate tunnel", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Interface: {Name}", + "message": "Interface: {Name}", + "translation": "Interface: {Name}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "config.Name" + } + ], + "fuzzy": true + }, + { + "id": "Peer", + "message": "Peer", + "translation": "Peer", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Create new tunnel", + "message": "Create new tunnel", + "translation": "Create new tunnel", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Edit tunnel", + "message": "Edit tunnel", + "translation": "Edit tunnel", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Name:", + "message": "\u0026Name:", + "translation": "\u0026Name:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Public key:", + "message": "\u0026Public key:", + "translation": "\u0026Public key:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "(unknown)", + "message": "(unknown)", + "translation": "(unknown)", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Block untunneled traffic (kill-switch)", + "message": "\u0026Block untunneled traffic (kill-switch)", + "translation": "\u0026Block untunneled traffic (kill-switch)", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "When a configuration has exactly one peer, and that peer has an allowed IPs containing at least one of 0.0.0.0/0 or ::/0, then the tunnel service engages a firewall ruleset to block all traffic that is neither to nor from the tunnel interface, with special exceptions for DHCP and NDP.", + "message": "When a configuration has exactly one peer, and that peer has an allowed IPs containing at least one of 0.0.0.0/0 or ::/0, then the tunnel service engages a firewall ruleset to block all traffic that is neither to nor from the tunnel interface, with special exceptions for DHCP and NDP.", + "translation": "When a configuration has exactly one peer, and that peer has an allowed IPs containing at least one of 0.0.0.0/0 or ::/0, then the tunnel service engages a firewall ruleset to block all traffic that is neither to nor from the tunnel interface, with special exceptions for DHCP and NDP.", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Save", + "message": "\u0026Save", + "translation": "\u0026Save", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Cancel", + "message": "Cancel", + "translation": "Cancel", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Configuration:", + "message": "\u0026Configuration:", + "translation": "\u0026Configuration:", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Invalid name", + "message": "Invalid name", + "translation": "Invalid name", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "A name is required.", + "message": "A name is required.", + "translation": "A name is required.", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Tunnel name ‘{NewName}’ is invalid.", + "message": "Tunnel name ‘{NewName}’ is invalid.", + "translation": "Tunnel name ‘{NewName}’ is invalid.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "NewName", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "newName" + } + ], + "fuzzy": true + }, + { + "id": "Unable to list existing tunnels", + "message": "Unable to list existing tunnels", + "translation": "Unable to list existing tunnels", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Tunnel already exists", + "message": "Tunnel already exists", + "translation": "Tunnel already exists", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Another tunnel already exists with the name ‘{NewName}’.", + "message": "Another tunnel already exists with the name ‘{NewName}’.", + "translation": "Another tunnel already exists with the name ‘{NewName}’.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "NewName", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "newName" + } + ], + "fuzzy": true + }, + { + "id": "Unable to create new configuration", + "message": "Unable to create new configuration", + "translation": "Unable to create new configuration", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Writing file failed", + "message": "Writing file failed", + "translation": "Writing file failed", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "File ‘{FilePath}’ already exists.\n\nDo you want to overwrite it?", + "message": "File ‘{FilePath}’ already exists.\n\nDo you want to overwrite it?", + "translation": "File ‘{FilePath}’ already exists.\n\nDo you want to overwrite it?", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "FilePath", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "filePath" + } + ], + "fuzzy": true + }, + { + "id": "Active", + "message": "Active", + "translation": "Active", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Activating", + "message": "Activating", + "translation": "Activating", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Inactive", + "message": "Inactive", + "translation": "Inactive", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Deactivating", + "message": "Deactivating", + "translation": "Deactivating", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Unknown state", + "message": "Unknown state", + "translation": "Unknown state", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Log", + "message": "Log", + "translation": "Log", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Copy", + "message": "\u0026Copy", + "translation": "\u0026Copy", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Select \u0026all", + "message": "Select \u0026all", + "translation": "Select \u0026all", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Save to file…", + "message": "\u0026Save to file…", + "translation": "\u0026Save to file…", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Time", + "message": "Time", + "translation": "Time", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Log message", + "message": "Log message", + "translation": "Log message", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Text Files (*.txt)|*.txt|All Files (*.*)|*.*", + "message": "Text Files (*.txt)|*.txt|All Files (*.*)|*.*", + "translation": "Text Files (*.txt)|*.txt|All Files (*.*)|*.*", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Export log to file", + "message": "Export log to file", + "translation": "Export log to file", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026About WireGuard…", + "message": "\u0026About WireGuard…", + "translation": "\u0026About WireGuard…", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Tunnel Error", + "message": "Tunnel Error", + "translation": "Tunnel Error", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "{ErrMsg}\n\nPlease consult the log for more information.", + "message": "{ErrMsg}\n\nPlease consult the log for more information.", + "translation": "{ErrMsg}\n\nPlease consult the log for more information.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "ErrMsg", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "errMsg" + } + ], + "fuzzy": true + }, + { + "id": "{Title} (out of date)", + "message": "{Title} (out of date)", + "translation": "{Title} (out of date)", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Title", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "mtw.Title()" + } + ], + "fuzzy": true + }, + { + "id": "WireGuard Detection Error", + "message": "WireGuard Detection Error", + "translation": "WireGuard Detection Error", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Unable to wait for WireGuard window to appear: {Err}", + "message": "Unable to wait for WireGuard window to appear: {Err}", + "translation": "Unable to wait for WireGuard window to appear: {Err}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "WireGuard: Deactivated", + "message": "WireGuard: Deactivated", + "translation": "WireGuard: Deactivated", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Status: Unknown", + "message": "Status: Unknown", + "translation": "Status: Unknown", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Addresses: None", + "message": "Addresses: None", + "translation": "Addresses: None", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Manage tunnels…", + "message": "\u0026Manage tunnels…", + "translation": "\u0026Manage tunnels…", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Import tunnel(s) from file…", + "message": "\u0026Import tunnel(s) from file…", + "translation": "\u0026Import tunnel(s) from file…", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "E\u0026xit", + "message": "E\u0026xit", + "translation": "E\u0026xit", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "WireGuard Tunnel Error", + "message": "WireGuard Tunnel Error", + "translation": "WireGuard Tunnel Error", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "WireGuard: {TextForStateglobalState_true}", + "message": "WireGuard: {TextForStateglobalState_true}", + "translation": "WireGuard: {TextForStateglobalState_true}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "TextForStateglobalState_true", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "textForState(globalState, true)" + } + ], + "fuzzy": true + }, + { + "id": "Status: {StateText}", + "message": "Status: {StateText}", + "translation": "Status: {StateText}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "StateText", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "stateText" + } + ], + "fuzzy": true + }, + { + "id": "Addresses: {String}", + "message": "Addresses: {String}", + "translation": "Addresses: {String}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "String", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "sb.String()" + } + ], + "fuzzy": true + }, + { + "id": "WireGuard Activated", + "message": "WireGuard Activated", + "translation": "WireGuard Activated", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "The {Name} tunnel has been activated.", + "message": "The {Name} tunnel has been activated.", + "translation": "The {Name} tunnel has been activated.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "tunnel.Name" + } + ], + "fuzzy": true + }, + { + "id": "WireGuard Deactivated", + "message": "WireGuard Deactivated", + "translation": "WireGuard Deactivated", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "The {Name} tunnel has been deactivated.", + "message": "The {Name} tunnel has been deactivated.", + "translation": "The {Name} tunnel has been deactivated.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "tunnel.Name" + } + ], + "fuzzy": true + }, + { + "id": "An Update is Available!", + "message": "An Update is Available!", + "translation": "An Update is Available!", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "WireGuard Update Available", + "message": "WireGuard Update Available", + "translation": "WireGuard Update Available", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "An update to WireGuard is now available. You are advised to update as soon as possible.", + "message": "An update to WireGuard is now available. You are advised to update as soon as possible.", + "translation": "An update to WireGuard is now available. You are advised to update as soon as possible.", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Tunnels", + "message": "Tunnels", + "translation": "Tunnels", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Edit", + "message": "\u0026Edit", + "translation": "\u0026Edit", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Add \u0026empty tunnel…", + "message": "Add \u0026empty tunnel…", + "translation": "Add \u0026empty tunnel…", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Add Tunnel", + "message": "Add Tunnel", + "translation": "Add Tunnel", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Remove selected tunnel(s)", + "message": "Remove selected tunnel(s)", + "translation": "Remove selected tunnel(s)", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Export all tunnels to zip", + "message": "Export all tunnels to zip", + "translation": "Export all tunnels to zip", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Toggle", + "message": "\u0026Toggle", + "translation": "\u0026Toggle", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Export all tunnels to \u0026zip…", + "message": "Export all tunnels to \u0026zip…", + "translation": "Export all tunnels to \u0026zip…", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Edit \u0026selected tunnel…", + "message": "Edit \u0026selected tunnel…", + "translation": "Edit \u0026selected tunnel…", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "\u0026Remove selected tunnel(s)", + "message": "\u0026Remove selected tunnel(s)", + "translation": "\u0026Remove selected tunnel(s)", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Could not import selected configuration: {LastErr}", + "message": "Could not import selected configuration: {LastErr}", + "translation": "Could not import selected configuration: {LastErr}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "LastErr", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "lastErr" + } + ], + "fuzzy": true + }, + { + "id": "Could not enumerate existing tunnels: {LastErr}", + "message": "Could not enumerate existing tunnels: {LastErr}", + "translation": "Could not enumerate existing tunnels: {LastErr}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "LastErr", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "lastErr" + } + ], + "fuzzy": true + }, + { + "id": "Another tunnel already exists with the name ‘{Name}’", + "message": "Another tunnel already exists with the name ‘{Name}’", + "translation": "Another tunnel already exists with the name ‘{Name}’", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Name", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "unparsedConfig.Name" + } + ], + "fuzzy": true + }, + { + "id": "Unable to import configuration: {LastErr}", + "message": "Unable to import configuration: {LastErr}", + "translation": "Unable to import configuration: {LastErr}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "LastErr", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "lastErr" + } + ], + "fuzzy": true + }, + { + "id": "Imported tunnels", + "message": "Imported tunnels", + "translation": "Imported tunnels", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Imported {M} tunnels", + "message": "Imported {M} tunnels", + "translation": { + "select": { + "feature": "plural", + "arg": "M", + "cases": { + "one": { + "msg": "Imported {M} tunnel" + }, + "other": { + "msg": "Imported {M} tunnels" + } + } + } + }, + "placeholders": [ + { + "id": "M", + "string": "%[1]d", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "m" + } + ] + }, + { + "id": "Imported {M} of {N} tunnels", + "message": "Imported {M} of {N} tunnels", + "translation": { + "select": { + "feature": "plural", + "arg": "N", + "cases": { + "one": { + "msg": "Imported {M} of {N} tunnel" + }, + "other": { + "msg": "Imported {M} of {N} tunnels" + } + } + } + }, + "placeholders": [ + { + "id": "M", + "string": "%[1]d", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "m" + }, + { + "id": "N", + "string": "%[2]d", + "type": "int", + "underlyingType": "int", + "argNum": 2, + "expr": "n" + } + ] + }, + { + "id": "Unable to create tunnel", + "message": "Unable to create tunnel", + "translation": "Unable to create tunnel", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Delete {TunnelCount} tunnels", + "message": "Delete {TunnelCount} tunnels", + "translation": { + "select": { + "feature": "plural", + "arg": "TunnelCount", + "cases": { + "one": { + "msg": "Delete {TunnelCount} tunnel" + }, + "other": { + "msg": "Delete {TunnelCount} tunnels" + } + } + } + }, + "placeholders": [ + { + "id": "TunnelCount", + "string": "%[1]d", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "tunnelCount" + } + ] + }, + { + "id": "Are you sure you would like to delete {TunnelCount} tunnels?", + "message": "Are you sure you would like to delete {TunnelCount} tunnels?", + "translation": { + "select": { + "feature": "plural", + "arg": "TunnelCount", + "cases": { + "one": { + "msg": "Are you sure you would like to delete {TunnelCount} tunnel?" + }, + "other": { + "msg": "Are you sure you would like to delete {TunnelCount} tunnels?" + } + } + } + }, + "placeholders": [ + { + "id": "TunnelCount", + "string": "%[1]d", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "tunnelCount" + } + ] + }, + { + "id": "Delete tunnel ‘{TunnelName}’", + "message": "Delete tunnel ‘{TunnelName}’", + "translation": "Delete tunnel ‘{TunnelName}’", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "TunnelName", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "tunnelName" + } + ], + "fuzzy": true + }, + { + "id": "Are you sure you would like to delete tunnel ‘{TunnelName}’?", + "message": "Are you sure you would like to delete tunnel ‘{TunnelName}’?", + "translation": "Are you sure you would like to delete tunnel ‘{TunnelName}’?", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "TunnelName", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "tunnelName" + } + ], + "fuzzy": true + }, + { + "id": "{Question} You cannot undo this action.", + "message": "{Question} You cannot undo this action.", + "translation": "{Question} You cannot undo this action.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Question", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "question" + } + ], + "fuzzy": true + }, + { + "id": "Unable to delete tunnel", + "message": "Unable to delete tunnel", + "translation": "Unable to delete tunnel", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "A tunnel was unable to be removed: {Error}", + "message": "A tunnel was unable to be removed: {Error}", + "translation": "A tunnel was unable to be removed: {Error}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Error", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "errors[0].Error()" + } + ], + "fuzzy": true + }, + { + "id": "Unable to delete tunnels", + "message": "Unable to delete tunnels", + "translation": "Unable to delete tunnels", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "{Lenerrors} tunnels were unable to be removed.", + "message": "{Lenerrors} tunnels were unable to be removed.", + "translation": { + "select": { + "feature": "plural", + "arg": "Lenerrors", + "cases": { + "one": { + "msg": "{Lenerrors} tunnel was unable to be removed." + }, + "other": { + "msg": "{Lenerrors} tunnels were unable to be removed." + } + } + } + }, + "placeholders": [ + { + "id": "Lenerrors", + "string": "%[1]d", + "type": "int", + "underlyingType": "int", + "argNum": 1, + "expr": "len(errors)" + } + ] + }, + { + "id": "Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*", + "message": "Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*", + "translation": "Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Import tunnel(s) from file", + "message": "Import tunnel(s) from file", + "translation": "Import tunnel(s) from file", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Configuration ZIP Files (*.zip)|*.zip", + "message": "Configuration ZIP Files (*.zip)|*.zip", + "translation": "Configuration ZIP Files (*.zip)|*.zip", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Export tunnels to zip", + "message": "Export tunnels to zip", + "translation": "Export tunnels to zip", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "{Title} (unsigned build, no updates)", + "message": "{Title} (unsigned build, no updates)", + "translation": "{Title} (unsigned build, no updates)", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Title", + "string": "%[1]s", + "type": "string", + "underlyingType": "string", + "argNum": 1, + "expr": "mtw.Title()" + } + ], + "fuzzy": true + }, + { + "id": "Error Exiting WireGuard", + "message": "Error Exiting WireGuard", + "translation": "Error Exiting WireGuard", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Unable to exit service due to: {Err}. You may want to stop WireGuard from the service manager.", + "message": "Unable to exit service due to: {Err}. You may want to stop WireGuard from the service manager.", + "translation": "Unable to exit service due to: {Err}. You may want to stop WireGuard from the service manager.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "An update to WireGuard is available. It is highly advisable to update without delay.", + "message": "An update to WireGuard is available. It is highly advisable to update without delay.", + "translation": "An update to WireGuard is available. It is highly advisable to update without delay.", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Status: Waiting for user", + "message": "Status: Waiting for user", + "translation": "Status: Waiting for user", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Update Now", + "message": "Update Now", + "translation": "Update Now", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Status: Waiting for updater service", + "message": "Status: Waiting for updater service", + "translation": "Status: Waiting for updater service", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "Error: {Err}. Please try again.", + "message": "Error: {Err}. Please try again.", + "translation": "Error: {Err}. Please try again.", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Err", + "string": "%[1]v", + "type": "error", + "underlyingType": "interface{Error() string}", + "argNum": 1, + "expr": "err" + } + ], + "fuzzy": true + }, + { + "id": "Status: Complete!", + "message": "Status: Complete!", + "translation": "Status: Complete!", + "translatorComment": "Copied from source.", + "fuzzy": true + }, + { + "id": "http2: Framer {F}: failed to decode just-written frame", + "message": "http2: Framer {F}: failed to decode just-written frame", + "translation": "http2: Framer {F}: failed to decode just-written frame", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "F", + "string": "%[1]p", + "type": "*net/http.http2Framer", + "underlyingType": "*net/http.http2Framer", + "argNum": 1, + "expr": "f" + } + ], + "fuzzy": true + }, + { + "id": "http2: Framer {F}: wrote {Http2summarizeFramefr}", + "message": "http2: Framer {F}: wrote {Http2summarizeFramefr}", + "translation": "http2: Framer {F}: wrote {Http2summarizeFramefr}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "F", + "string": "%[1]p", + "type": "*net/http.http2Framer", + "underlyingType": "*net/http.http2Framer", + "argNum": 1, + "expr": "f" + }, + { + "id": "Http2summarizeFramefr", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "http2summarizeFrame(fr)" + } + ], + "fuzzy": true + }, + { + "id": "http2: Framer {Fr}: read {Http2summarizeFramef}", + "message": "http2: Framer {Fr}: read {Http2summarizeFramef}", + "translation": "http2: Framer {Fr}: read {Http2summarizeFramef}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "Fr", + "string": "%[1]p", + "type": "*net/http.http2Framer", + "underlyingType": "*net/http.http2Framer", + "argNum": 1, + "expr": "fr" + }, + { + "id": "Http2summarizeFramef", + "string": "%[2]v", + "type": "string", + "underlyingType": "string", + "argNum": 2, + "expr": "http2summarizeFrame(f)" + } + ], + "fuzzy": true + }, + { + "id": "http2: decoded hpack field {HeaderField}", + "message": "http2: decoded hpack field {HeaderField}", + "translation": "http2: decoded hpack field {HeaderField}", + "translatorComment": "Copied from source.", + "placeholders": [ + { + "id": "HeaderField", + "string": "%+[1]v", + "type": "vendor/golang.org/x/net/http2/hpack.HeaderField", + "underlyingType": "struct{Name string; Value string; Sensitive bool}", + "argNum": 1, + "expr": "hf" + } + ], + "fuzzy": true + } + ] +}
\ No newline at end of file @@ -5,6 +5,8 @@ package main +//go:generate go run golang.org/x/text/cmd/gotext -srclang=en update -out=zgotext.go -lang=en + import ( "fmt" "os" @@ -15,43 +17,43 @@ import ( "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/elevate" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/manager" "golang.zx2c4.com/wireguard/windows/ringlogger" "golang.zx2c4.com/wireguard/windows/tunnel" "golang.zx2c4.com/wireguard/windows/ui" ) -var flags = [...]string{ - "(no argument): elevate and install manager service for current user", - "/installmanagerservice", - "/installtunnelservice CONFIG_PATH", - "/uninstallmanagerservice", - "/uninstalltunnelservice TUNNEL_NAME", - "/managerservice", - "/tunnelservice CONFIG_PATH", - "/ui CMD_READ_HANDLE CMD_WRITE_HANDLE CMD_EVENT_HANDLE LOG_MAPPING_HANDLE", - "/dumplog OUTPUT_PATH", -} - func fatal(v ...interface{}) { - windows.MessageBox(0, windows.StringToUTF16Ptr(fmt.Sprint(v...)), windows.StringToUTF16Ptr("Error"), windows.MB_ICONERROR) + windows.MessageBox(0, windows.StringToUTF16Ptr(fmt.Sprint(v...)), windows.StringToUTF16Ptr(l18n.Sprintf("Error")), windows.MB_ICONERROR) os.Exit(1) } func fatalf(format string, v ...interface{}) { - fatal(fmt.Sprintf(format, v...)) + fatal(l18n.Sprintf(format, v...)) } func info(title string, format string, v ...interface{}) { - windows.MessageBox(0, windows.StringToUTF16Ptr(fmt.Sprintf(format, v...)), windows.StringToUTF16Ptr(title), windows.MB_ICONINFORMATION) + windows.MessageBox(0, windows.StringToUTF16Ptr(l18n.Sprintf(format, v...)), windows.StringToUTF16Ptr(title), windows.MB_ICONINFORMATION) } func usage() { + var flags = [...]string{ + l18n.Sprintf("(no argument): elevate and install manager service"), + "/installmanagerservice", + "/installtunnelservice CONFIG_PATH", + "/uninstallmanagerservice", + "/uninstalltunnelservice TUNNEL_NAME", + "/managerservice", + "/tunnelservice CONFIG_PATH", + "/ui CMD_READ_HANDLE CMD_WRITE_HANDLE CMD_EVENT_HANDLE LOG_MAPPING_HANDLE", + "/dumplog OUTPUT_PATH", + } builder := strings.Builder{} for _, flag := range flags { builder.WriteString(fmt.Sprintf(" %s\n", flag)) } - info("Command Line Options", "Usage: %s [\n%s]", os.Args[0], builder.String()) + info(l18n.Sprintf("Command Line Options"), "Usage: %s [\n%s]", os.Args[0], builder.String()) os.Exit(1) } @@ -62,7 +64,7 @@ func checkForWow64() { fatalf("Unable to determine whether the process is running under WOW64: %v", err) } if b { - fatal("You must use the 64-bit version of WireGuard on this computer.") + fatalf("You must use the 64-bit version of WireGuard on this computer.") } } @@ -136,7 +138,7 @@ func main() { } checkForAdminDesktop() time.Sleep(30 * time.Second) - fatal("WireGuard system tray icon did not appear after 30 seconds.") + fatalf("WireGuard system tray icon did not appear after 30 seconds.") return case "/uninstallmanagerservice": if len(os.Args) != 2 { diff --git a/resources.rc b/resources.rc index 845e9684..9ea7ef58 100644 --- a/resources.rc +++ b/resources.rc @@ -6,35 +6,45 @@ #include <windows.h> #include "version/version.h" -CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml +#pragma code_page(65001) // UTF-8 +LANGUAGE LANG_NEUTRAL, SUBLANG_NEUTRAL +CREATEPROCESS_MANIFEST_RESOURCE_ID RT_MANIFEST manifest.xml $wireguard.ico ICON ui/icon/wireguard.ico dot-gray.ico ICON ui/icon/dot-gray.ico -VS_VERSION_INFO VERSIONINFO -FILEVERSION WIREGUARD_WINDOWS_VERSION_ARRAY -PRODUCTVERSION WIREGUARD_WINDOWS_VERSION_ARRAY -FILEOS VOS_NT_WINDOWS32 -FILETYPE VFT_APP -FILESUBTYPE VFT2_UNKNOWN -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904b0" - BEGIN - VALUE "CompanyName", "WireGuard LLC" - VALUE "FileDescription", "WireGuard: Fast, Modern, Secure VPN Tunnel" - VALUE "FileVersion", WIREGUARD_WINDOWS_VERSION_STRING - VALUE "InternalName", "wireguard" - VALUE "LegalCopyright", "Copyright \xa9 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved." - VALUE "OriginalFilename", "wireguard.exe" - VALUE "ProductName", "WireGuard" - VALUE "ProductVersion", WIREGUARD_WINDOWS_VERSION_STRING - VALUE "Comments", "https://www.wireguard.com/" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1200 - END +#define VERSIONINFO_TEMPLATE(block_id, lang_id, codepage_id, file_desc, comments) \ +VS_VERSION_INFO VERSIONINFO \ +FILEVERSION WIREGUARD_WINDOWS_VERSION_ARRAY \ +PRODUCTVERSION WIREGUARD_WINDOWS_VERSION_ARRAY \ +FILEOS VOS_NT_WINDOWS32 \ +FILETYPE VFT_APP \ +FILESUBTYPE VFT2_UNKNOWN \ +BEGIN \ + BLOCK "StringFileInfo" \ + BEGIN \ + BLOCK block_id \ + BEGIN \ + VALUE "CompanyName", "WireGuard LLC" \ + VALUE "FileDescription", file_desc \ + VALUE "FileVersion", WIREGUARD_WINDOWS_VERSION_STRING \ + VALUE "InternalName", "wireguard-windows" \ + VALUE "LegalCopyright", "Copyright © 2015-2019 Jason A. Donenfeld <Jason@zx2c4.com>. All Rights Reserved." \ + VALUE "OriginalFilename", "wireguard.exe" \ + VALUE "ProductName", "WireGuard" \ + VALUE "ProductVersion", WIREGUARD_WINDOWS_VERSION_STRING \ + VALUE "Comments", comments \ + END \ + END \ + BLOCK "VarFileInfo" \ + BEGIN \ + VALUE "Translation", lang_id, codepage_id \ + END \ END + +LANGUAGE LANG_ENGLISH, SUBLANG_DEFAULT +VERSIONINFO_TEMPLATE( + "040904b0", 0x409, 0x4b0, + "WireGuard: Fast, Modern, Secure VPN Tunnel", + "https://www.wireguard.com/" +) diff --git a/ui/aboutdialog.go b/ui/aboutdialog.go index d87727aa..238ba5e2 100644 --- a/ui/aboutdialog.go +++ b/ui/aboutdialog.go @@ -6,7 +6,6 @@ package ui import ( - "fmt" "runtime" "strings" @@ -14,6 +13,7 @@ import ( "github.com/lxn/win" "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/device" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/version" ) @@ -47,7 +47,7 @@ func runAboutDialog(owner walk.Form) error { showingAboutDialog = nil }() disposables.Add(showingAboutDialog) - showingAboutDialog.SetTitle("About WireGuard") + showingAboutDialog.SetTitle(l18n.Sprintf("About WireGuard")) showingAboutDialog.SetLayout(vbl) if icon, err := loadLogoIcon(32); err == nil { showingAboutDialog.SetIcon(icon) @@ -79,7 +79,7 @@ func runAboutDialog(owner walk.Form) error { if logo, err := loadLogoIcon(128); err == nil { iv.SetImage(logo) } - iv.Accessibility().SetName("WireGuard logo image") + iv.Accessibility().SetName(l18n.Sprintf("WireGuard logo image")) wgLbl, err := walk.NewTextLabel(showingAboutDialog) if err != nil { @@ -95,7 +95,7 @@ func runAboutDialog(owner walk.Form) error { return err } detailsLbl.SetTextAlignment(walk.AlignHCenterVNear) - detailsLbl.SetText(fmt.Sprintf("App version: %s\nGo backend version: %s\nGo version: %s\nOperating system: %s\nArchitecture: %s", version.Number, device.WireGuardGoVersion, strings.TrimPrefix(runtime.Version(), "go"), version.OsName(), runtime.GOARCH)) + detailsLbl.SetText(l18n.Sprintf("App version: %s\nGo backend version: %s\nGo version: %s\nOperating system: %s\nArchitecture: %s", version.Number, device.WireGuardGoVersion, strings.TrimPrefix(runtime.Version(), "go"), version.OsName(), runtime.GOARCH)) copyrightLbl, err := walk.NewTextLabel(showingAboutDialog) if err != nil { @@ -119,14 +119,14 @@ func runAboutDialog(owner walk.Form) error { return err } closePB.SetAlignment(walk.AlignHCenterVNear) - closePB.SetText("Close") + closePB.SetText(l18n.Sprintf("Close")) closePB.Clicked().Attach(showingAboutDialog.Accept) donatePB, err := walk.NewPushButton(buttonCP) if err != nil { return err } donatePB.SetAlignment(walk.AlignHCenterVNear) - donatePB.SetText("♥ &Donate!") + donatePB.SetText(l18n.Sprintf("♥ &Donate!")) donatePB.Clicked().Attach(func() { if easterEggIndex == -1 { easterEggIndex = 0 diff --git a/ui/confview.go b/ui/confview.go index 1c378aa1..d080db30 100644 --- a/ui/confview.go +++ b/ui/confview.go @@ -6,7 +6,6 @@ package ui import ( - "fmt" "strconv" "strings" "time" @@ -15,6 +14,7 @@ import ( "github.com/lxn/win" "golang.zx2c4.com/wireguard/windows/conf" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/manager" ) @@ -108,7 +108,7 @@ func newLabelStatusLine(parent walk.Container) (*labelStatusLine, error) { return nil, err } disposables.Add(lsl.label) - lsl.label.SetText("Status:") + lsl.label.SetText(l18n.Sprintf("Status:")) lsl.label.SetTextAlignment(walk.AlignHFarVNear) if lsl.statusComposite, err = walk.NewComposite(parent); err != nil { @@ -180,7 +180,7 @@ func newLabelTextLine(fieldName string, parent walk.Container) (*labelTextLine, return nil, err } disposables.Add(lt.label) - lt.label.SetText(fieldName + ":") + lt.label.SetText(fieldName) lt.label.SetTextAlignment(walk.AlignHFarVNear) lt.label.SetVisible(false) @@ -216,9 +216,9 @@ func (tal *toggleActiveLine) update(state manager.TunnelState) { switch state { case manager.TunnelStarted: - text = "&Deactivate" + text = l18n.Sprintf("&Deactivate") case manager.TunnelStopped: - text = "&Activate" + text = l18n.Sprintf("&Activate") case manager.TunnelStarting, manager.TunnelStopping: text = textForState(state, true) default: @@ -300,11 +300,11 @@ func newInterfaceView(parent walk.Container) (*interfaceView, error) { disposables.Add(iv.status) items := []labelTextLineItem{ - {"Public key", &iv.publicKey}, - {"Listen port", &iv.listenPort}, - {"MTU", &iv.mtu}, - {"Addresses", &iv.addresses}, - {"DNS servers", &iv.dns}, + {l18n.Sprintf("Public key:"), &iv.publicKey}, + {l18n.Sprintf("Listen port:"), &iv.listenPort}, + {l18n.Sprintf("MTU:"), &iv.mtu}, + {l18n.Sprintf("Addresses:"), &iv.addresses}, + {l18n.Sprintf("DNS servers:"), &iv.dns}, } if iv.lines, err = createLabelTextLines(items, parent, &disposables); err != nil { return nil, err @@ -328,13 +328,13 @@ func newPeerView(parent walk.Container) (*peerView, error) { pv := new(peerView) items := []labelTextLineItem{ - {"Public key", &pv.publicKey}, - {"Preshared key", &pv.presharedKey}, - {"Allowed IPs", &pv.allowedIPs}, - {"Endpoint", &pv.endpoint}, - {"Persistent keepalive", &pv.persistentKeepalive}, - {"Latest handshake", &pv.latestHandshake}, - {"Transfer", &pv.transfer}, + {l18n.Sprintf("Public key:"), &pv.publicKey}, + {l18n.Sprintf("Preshared key:"), &pv.presharedKey}, + {l18n.Sprintf("Allowed IPs:"), &pv.allowedIPs}, + {l18n.Sprintf("Endpoint:"), &pv.endpoint}, + {l18n.Sprintf("Persistent keepalive:"), &pv.persistentKeepalive}, + {l18n.Sprintf("Latest handshake:"), &pv.latestHandshake}, + {l18n.Sprintf("Transfer:"), &pv.transfer}, } var err error if pv.lines, err = createLabelTextLines(items, parent, nil); err != nil { @@ -383,7 +383,7 @@ func (iv *interfaceView) apply(c *conf.Interface) { for i, address := range c.Addresses { addrStrings[i] = address.String() } - iv.addresses.show(strings.Join(addrStrings[:], ", ")) + iv.addresses.show(strings.Join(addrStrings[:], l18n.EnumerationSeparator())) } else { iv.addresses.hide() } @@ -393,7 +393,7 @@ func (iv *interfaceView) apply(c *conf.Interface) { for i, address := range c.DNS { addrStrings[i] = address.String() } - iv.dns.show(strings.Join(addrStrings[:], ", ")) + iv.dns.show(strings.Join(addrStrings[:], l18n.EnumerationSeparator())) } else { iv.dns.hide() } @@ -407,7 +407,7 @@ func (pv *peerView) apply(c *conf.Peer) { pv.publicKey.show(c.PublicKey.String()) if !c.PresharedKey.IsZero() { - pv.presharedKey.show("enabled") + pv.presharedKey.show(l18n.Sprintf("enabled")) } else { pv.presharedKey.hide() } @@ -417,7 +417,7 @@ func (pv *peerView) apply(c *conf.Peer) { for i, address := range c.AllowedIPs { addrStrings[i] = address.String() } - pv.allowedIPs.show(strings.Join(addrStrings[:], ", ")) + pv.allowedIPs.show(strings.Join(addrStrings[:], l18n.EnumerationSeparator())) } else { pv.allowedIPs.hide() } @@ -441,7 +441,7 @@ func (pv *peerView) apply(c *conf.Peer) { } if c.RxBytes > 0 || c.TxBytes > 0 { - pv.transfer.show(fmt.Sprintf("%s received, %s sent", c.RxBytes.String(), c.TxBytes.String())) + pv.transfer.show(l18n.Sprintf("%s received, %s sent", c.RxBytes.String(), c.TxBytes.String())) } else { pv.transfer.hide() } @@ -552,11 +552,11 @@ func (cv *ConfView) onToggleActiveClicked() { if err != nil { cv.Synchronize(func() { if oldState == manager.TunnelUnknown { - showErrorCustom(cv.Form(), "Failed to determine tunnel state", err.Error()) + showErrorCustom(cv.Form(), l18n.Sprintf("Failed to determine tunnel state"), err.Error()) } else if oldState == manager.TunnelStopped { - showErrorCustom(cv.Form(), "Failed to activate tunnel", err.Error()) + showErrorCustom(cv.Form(), l18n.Sprintf("Failed to activate tunnel"), err.Error()) } else if oldState == manager.TunnelStarted { - showErrorCustom(cv.Form(), "Failed to deactivate tunnel", err.Error()) + showErrorCustom(cv.Form(), l18n.Sprintf("Failed to deactivate tunnel"), err.Error()) } }) } @@ -612,7 +612,7 @@ func (cv *ConfView) setTunnel(tunnel *manager.Tunnel, config *conf.Config, state return } - title := "Interface: " + config.Name + title := l18n.Sprintf("Interface: %s", config.Name) if cv.name.Title() != title { cv.SetSuspended(true) defer cv.SetSuspended(false) @@ -656,7 +656,7 @@ func (cv *ConfView) setTunnel(tunnel *manager.Tunnel, config *conf.Config, state if err != nil { continue } - group.SetTitle("Peer") + group.SetTitle(l18n.Sprintf("Peer")) pv, err := newPeerView(group) if err != nil { group.Dispose() diff --git a/ui/editdialog.go b/ui/editdialog.go index 4d25dd79..ade6c498 100644 --- a/ui/editdialog.go +++ b/ui/editdialog.go @@ -6,7 +6,6 @@ package ui import ( - "fmt" "strings" "github.com/lxn/walk" @@ -14,6 +13,7 @@ import ( "golang.org/x/sys/windows" "golang.zx2c4.com/wireguard/windows/conf" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/manager" "golang.zx2c4.com/wireguard/windows/ui/syntax" ) @@ -52,9 +52,9 @@ func newEditDialog(owner walk.Form, tunnel *manager.Tunnel) (*EditDialog, error) var title string if tunnel == nil { - title = "Create new tunnel" + title = l18n.Sprintf("Create new tunnel") } else { - title = "Edit tunnel" + title = l18n.Sprintf("Edit tunnel") } if tunnel == nil { @@ -88,7 +88,7 @@ func newEditDialog(owner walk.Form, tunnel *manager.Tunnel) (*EditDialog, error) } layout.SetRange(nameLabel, walk.Rectangle{0, 0, 1, 1}) nameLabel.SetTextAlignment(walk.AlignHFarVCenter) - nameLabel.SetText("&Name:") + nameLabel.SetText(l18n.Sprintf("&Name:")) if dlg.nameEdit, err = walk.NewLineEdit(dlg); err != nil { return nil, err @@ -102,14 +102,14 @@ func newEditDialog(owner walk.Form, tunnel *manager.Tunnel) (*EditDialog, error) } layout.SetRange(pubkeyLabel, walk.Rectangle{0, 1, 1, 1}) pubkeyLabel.SetTextAlignment(walk.AlignHFarVCenter) - pubkeyLabel.SetText("&Public key:") + pubkeyLabel.SetText(l18n.Sprintf("&Public key:")) if dlg.pubkeyEdit, err = walk.NewLineEdit(dlg); err != nil { return nil, err } layout.SetRange(dlg.pubkeyEdit, walk.Rectangle{1, 1, 1, 1}) dlg.pubkeyEdit.SetReadOnly(true) - dlg.pubkeyEdit.SetText("(unknown)") + dlg.pubkeyEdit.SetText(l18n.Sprintf("(unknown)")) dlg.pubkeyEdit.Accessibility().SetRole(walk.AccRoleStatictext) if dlg.syntaxEdit, err = syntax.NewSyntaxEdit(dlg); err != nil { @@ -128,8 +128,8 @@ func newEditDialog(owner walk.Form, tunnel *manager.Tunnel) (*EditDialog, error) if dlg.blockUntunneledTrafficCB, err = walk.NewCheckBox(buttonsContainer); err != nil { return nil, err } - dlg.blockUntunneledTrafficCB.SetText("&Block untunneled traffic (kill-switch)") - dlg.blockUntunneledTrafficCB.SetToolTipText("When a configuration has exactly one peer, and that peer has an allowed IPs containing at least one of 0.0.0.0/0 or ::/0, then the tunnel service engages a firewall ruleset to block all traffic that is neither to nor from the tunnel interface, with special exceptions for DHCP and NDP.") + dlg.blockUntunneledTrafficCB.SetText(l18n.Sprintf("&Block untunneled traffic (kill-switch)")) + dlg.blockUntunneledTrafficCB.SetToolTipText(l18n.Sprintf("When a configuration has exactly one peer, and that peer has an allowed IPs containing at least one of 0.0.0.0/0 or ::/0, then the tunnel service engages a firewall ruleset to block all traffic that is neither to nor from the tunnel interface, with special exceptions for DHCP and NDP.")) dlg.blockUntunneledTrafficCB.SetVisible(false) dlg.blockUntunneledTrafficCB.CheckedChanged().Attach(dlg.onBlockUntunneledTrafficCBCheckedChanged) @@ -138,14 +138,14 @@ func newEditDialog(owner walk.Form, tunnel *manager.Tunnel) (*EditDialog, error) if dlg.saveButton, err = walk.NewPushButton(buttonsContainer); err != nil { return nil, err } - dlg.saveButton.SetText("&Save") + dlg.saveButton.SetText(l18n.Sprintf("&Save")) dlg.saveButton.Clicked().Attach(dlg.onSaveButtonClicked) cancelButton, err := walk.NewPushButton(buttonsContainer) if err != nil { return nil, err } - cancelButton.SetText("Cancel") + cancelButton.SetText(l18n.Sprintf("Cancel")) cancelButton.Clicked().Attach(dlg.Cancel) dlg.SetCancelButton(cancelButton) @@ -160,7 +160,7 @@ func newEditDialog(owner walk.Form, tunnel *manager.Tunnel) (*EditDialog, error) syntaxEditWnd := dlg.syntaxEdit.Handle() parentWnd := win.GetParent(syntaxEditWnd) labelWnd := win.CreateWindowEx(0, - windows.StringToUTF16Ptr("STATIC"), windows.StringToUTF16Ptr("&Configuration:"), + windows.StringToUTF16Ptr("STATIC"), windows.StringToUTF16Ptr(l18n.Sprintf("&Configuration:")), win.WS_CHILD|win.WS_GROUP|win.SS_LEFT, 0, 0, 0, 0, parentWnd, win.HMENU(^uintptr(0)), win.HINSTANCE(win.GetWindowLongPtr(parentWnd, win.GWLP_HINSTANCE)), nil) prevWnd := win.GetWindow(syntaxEditWnd, win.GW_HWNDPREV) @@ -301,18 +301,18 @@ func (dlg *EditDialog) onSyntaxEditPrivateKeyChanged(privateKey string) { if key != nil { dlg.pubkeyEdit.SetText(key.Public().String()) } else { - dlg.pubkeyEdit.SetText("(unknown)") + dlg.pubkeyEdit.SetText(l18n.Sprintf("(unknown)")) } } func (dlg *EditDialog) onSaveButtonClicked() { newName := dlg.nameEdit.Text() if newName == "" { - showWarningCustom(dlg, "Invalid name", "A name is required.") + showWarningCustom(dlg, l18n.Sprintf("Invalid name"), l18n.Sprintf("A name is required.")) return } if !conf.TunnelNameIsValid(newName) { - showWarningCustom(dlg, "Invalid name", fmt.Sprintf("Tunnel name ‘%s’ is invalid.", newName)) + showWarningCustom(dlg, l18n.Sprintf("Invalid name"), l18n.Sprintf("Tunnel name ‘%s’ is invalid.", newName)) return } newNameLower := strings.ToLower(newName) @@ -320,12 +320,12 @@ func (dlg *EditDialog) onSaveButtonClicked() { if newNameLower != strings.ToLower(dlg.config.Name) { existingTunnelList, err := manager.IPCClientTunnels() if err != nil { - showWarningCustom(dlg, "Unable to list existing tunnels", err.Error()) + showWarningCustom(dlg, l18n.Sprintf("Unable to list existing tunnels"), err.Error()) return } for _, tunnel := range existingTunnelList { if strings.ToLower(tunnel.Name) == newNameLower { - showWarningCustom(dlg, "Tunnel already exists", fmt.Sprintf("Another tunnel already exists with the name ‘%s’.", newName)) + showWarningCustom(dlg, l18n.Sprintf("Tunnel already exists"), l18n.Sprintf("Another tunnel already exists with the name ‘%s’.", newName)) return } } @@ -333,7 +333,7 @@ func (dlg *EditDialog) onSaveButtonClicked() { cfg, err := conf.FromWgQuick(dlg.syntaxEdit.Text(), newName) if err != nil { - showErrorCustom(dlg, "Unable to create new configuration", err.Error()) + showErrorCustom(dlg, l18n.Sprintf("Unable to create new configuration"), err.Error()) return } diff --git a/ui/filesave.go b/ui/filesave.go index 4b5c2947..7ca8a2c2 100644 --- a/ui/filesave.go +++ b/ui/filesave.go @@ -6,10 +6,11 @@ package ui import ( - "fmt" "os" "github.com/lxn/walk" + + "golang.zx2c4.com/wireguard/windows/l18n" ) func writeFileWithOverwriteHandling(owner walk.Form, filePath string, write func(file *os.File) error) bool { @@ -18,7 +19,7 @@ func writeFileWithOverwriteHandling(owner walk.Form, filePath string, write func return false } - showErrorCustom(owner, "Writing file failed", err.Error()) + showErrorCustom(owner, l18n.Sprintf("Writing file failed"), err.Error()) return true } @@ -26,7 +27,7 @@ func writeFileWithOverwriteHandling(owner walk.Form, filePath string, write func file, err := os.OpenFile(filePath, os.O_CREATE|os.O_WRONLY|os.O_EXCL, 0600) if err != nil { if os.IsExist(err) { - if walk.DlgCmdNo == walk.MsgBox(owner, "Writing file failed", fmt.Sprintf(`File ‘%s’ already exists. + if walk.DlgCmdNo == walk.MsgBox(owner, l18n.Sprintf("Writing file failed"), l18n.Sprintf(`File ‘%s’ already exists. Do you want to overwrite it?`, filePath), walk.MsgBoxYesNo|walk.MsgBoxDefButton2|walk.MsgBoxIconWarning) { return false diff --git a/ui/iconprovider.go b/ui/iconprovider.go index e5177ac3..8db841c5 100644 --- a/ui/iconprovider.go +++ b/ui/iconprovider.go @@ -8,6 +8,7 @@ package ui import ( "github.com/lxn/walk" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/manager" ) @@ -87,15 +88,15 @@ func iconForState(state manager.TunnelState, size int) (icon *walk.Icon, err err func textForState(state manager.TunnelState, withEllipsis bool) (text string) { switch state { case manager.TunnelStarted: - text = "Active" + text = l18n.Sprintf("Active") case manager.TunnelStarting: - text = "Activating" + text = l18n.Sprintf("Activating") case manager.TunnelStopped: - text = "Inactive" + text = l18n.Sprintf("Inactive") case manager.TunnelStopping: - text = "Deactivating" + text = l18n.Sprintf("Deactivating") case manager.TunnelUnknown: - text = "Unknown state" + text = l18n.Sprintf("Unknown state") } if withEllipsis { switch state { diff --git a/ui/logpage.go b/ui/logpage.go index b8febcbb..1de5c920 100644 --- a/ui/logpage.go +++ b/ui/logpage.go @@ -12,6 +12,7 @@ import ( "time" "github.com/lxn/walk" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/ringlogger" ) @@ -41,7 +42,7 @@ func NewLogPage() (*LogPage, error) { lp.model.quit <- true }) - lp.SetTitle("Log") + lp.SetTitle(l18n.Sprintf("Log")) lp.SetLayout(walk.NewVBoxLayout()) if lp.logView, err = walk.NewTableView(lp); err != nil { @@ -57,19 +58,19 @@ func NewLogPage() (*LogPage, error) { } lp.logView.AddDisposable(contextMenu) copyAction := walk.NewAction() - copyAction.SetText("&Copy") + copyAction.SetText(l18n.Sprintf("&Copy")) copyAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyC}) copyAction.Triggered().Attach(lp.onCopy) contextMenu.Actions().Add(copyAction) lp.ShortcutActions().Add(copyAction) selectAllAction := walk.NewAction() - selectAllAction.SetText("Select &all") + selectAllAction.SetText(l18n.Sprintf("Select &all")) selectAllAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyA}) selectAllAction.Triggered().Attach(lp.onSelectAll) contextMenu.Actions().Add(selectAllAction) lp.ShortcutActions().Add(selectAllAction) saveAction := walk.NewAction() - saveAction.SetText("&Save to file…") + saveAction.SetText(l18n.Sprintf("&Save to file…")) saveAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyS}) saveAction.Triggered().Attach(lp.onSave) contextMenu.Actions().Add(saveAction) @@ -83,14 +84,14 @@ func NewLogPage() (*LogPage, error) { stampCol := walk.NewTableViewColumn() stampCol.SetName("Stamp") - stampCol.SetTitle("Time") + stampCol.SetTitle(l18n.Sprintf("Time")) stampCol.SetFormat("2006-01-02 15:04:05.000") stampCol.SetWidth(140) lp.logView.Columns().Add(stampCol) msgCol := walk.NewTableViewColumn() msgCol.SetName("Line") - msgCol.SetTitle("Log message") + msgCol.SetTitle(l18n.Sprintf("Log message")) lp.logView.Columns().Add(msgCol) lp.model = newLogModel(lp) @@ -111,7 +112,7 @@ func NewLogPage() (*LogPage, error) { if err != nil { return nil, err } - saveButton.SetText("&Save") + saveButton.SetText(l18n.Sprintf("&Save")) saveButton.Clicked().Attach(lp.onSave) disposables.Spare() @@ -146,9 +147,9 @@ func (lp *LogPage) onSelectAll() { func (lp *LogPage) onSave() { fd := walk.FileDialog{ - Filter: "Text Files (*.txt)|*.txt|All Files (*.*)|*.*", + Filter: l18n.Sprintf("Text Files (*.txt)|*.txt|All Files (*.*)|*.*"), FilePath: fmt.Sprintf("wireguard-log-%s.txt", time.Now().Format("2006-01-02T150405")), - Title: "Export log to file", + Title: l18n.Sprintf("Export log to file"), } form := lp.Form() diff --git a/ui/managewindow.go b/ui/managewindow.go index 542c91b9..e6855d5d 100644 --- a/ui/managewindow.go +++ b/ui/managewindow.go @@ -13,6 +13,7 @@ import ( "github.com/lxn/win" "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/manager" ) @@ -118,7 +119,7 @@ func NewManageTunnelsWindow() (*ManageTunnelsWindow, error) { CbSize: uint32(unsafe.Sizeof(win.MENUITEMINFO{})), FMask: win.MIIM_ID | win.MIIM_STRING | win.MIIM_FTYPE, FType: win.MIIM_STRING, - DwTypeData: windows.StringToUTF16Ptr("&About WireGuard…"), + DwTypeData: windows.StringToUTF16Ptr(l18n.Sprintf("&About WireGuard…")), WID: uint32(aboutWireGuardCmd), }) win.InsertMenuItem(systemMenu, 1, true, &win.MENUITEMINFO{ @@ -169,7 +170,7 @@ func (mtw *ManageTunnelsWindow) onTunnelChange(tunnel *manager.Tunnel, state man if len(errMsg) > 0 && errMsg[len(errMsg)-1] != '.' { errMsg += "." } - showWarningCustom(mtw, "Tunnel Error", errMsg+"\n\nPlease consult the log for more information.") + showWarningCustom(mtw, l18n.Sprintf("Tunnel Error"), l18n.Sprintf("%s\n\nPlease consult the log for more information.", errMsg)) } }) } @@ -178,7 +179,7 @@ func (mtw *ManageTunnelsWindow) UpdateFound() { if mtw.updatePage != nil { return } - mtw.SetTitle(mtw.Title() + " (out of date)") + mtw.SetTitle(l18n.Sprintf("%s (out of date)", mtw.Title())) updatePage, err := NewUpdatePage() if err == nil { mtw.updatePage = updatePage diff --git a/ui/raise.go b/ui/raise.go index 42509994..0eac828a 100644 --- a/ui/raise.go +++ b/ui/raise.go @@ -6,12 +6,13 @@ package ui import ( - "fmt" "os" "runtime" "github.com/lxn/win" "golang.org/x/sys/windows" + + "golang.zx2c4.com/wireguard/windows/l18n" ) func raise(hwnd win.HWND) { @@ -66,7 +67,7 @@ func WaitForRaiseUIThenQuit() { return 0 }, 0, 0, win.WINEVENT_SKIPOWNPROCESS|win.WINEVENT_OUTOFCONTEXT) if err != nil { - showErrorCustom(nil, "WireGuard Detection Error", fmt.Sprintf("Unable to wait for WireGuard window to appear: %v", err)) + showErrorCustom(nil, l18n.Sprintf("WireGuard Detection Error"), l18n.Sprintf("Unable to wait for WireGuard window to appear: %v", err)) } for { var msg win.MSG @@ -6,12 +6,12 @@ package ui import ( - "fmt" "sort" "strings" "time" "golang.zx2c4.com/wireguard/windows/conf" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/manager" "github.com/lxn/walk" @@ -53,7 +53,7 @@ func NewTray(mtw *ManageTunnelsWindow) (*Tray, error) { func (tray *Tray) setup() error { tray.clicked = tray.onManageTunnels - tray.SetToolTip("WireGuard: Deactivated") + tray.SetToolTip(l18n.Sprintf("WireGuard: Deactivated")) tray.SetVisible(true) if icon, err := loadLogoIcon(16); err == nil { tray.SetIcon(icon) @@ -76,15 +76,15 @@ func (tray *Tray) setup() error { separator bool defawlt bool }{ - {label: "Status: Unknown"}, - {label: "Addresses: None", hidden: true}, + {label: l18n.Sprintf("Status: Unknown")}, + {label: l18n.Sprintf("Addresses: None"), hidden: true}, {separator: true}, {separator: true}, - {label: "&Manage tunnels…", handler: tray.onManageTunnels, enabled: true, defawlt: true}, - {label: "&Import tunnel(s) from file…", handler: tray.onImport, enabled: true}, + {label: l18n.Sprintf("&Manage tunnels…"), handler: tray.onManageTunnels, enabled: true, defawlt: true}, + {label: l18n.Sprintf("&Import tunnel(s) from file…"), handler: tray.onImport, enabled: true}, {separator: true}, - {label: "&About WireGuard…", handler: tray.onAbout, enabled: true}, - {label: "E&xit", handler: onQuit, enabled: true}, + {label: l18n.Sprintf("&About WireGuard…"), handler: tray.onAbout, enabled: true}, + {label: l18n.Sprintf("E&xit"), handler: onQuit, enabled: true}, } { var action *walk.Action if item.separator { @@ -160,11 +160,11 @@ func (tray *Tray) addTunnelAction(tunnel *manager.Tunnel) { tray.mtw.tunnelsPage.listView.selectTunnel(tclosure.Name) tray.mtw.tabs.SetCurrentIndex(0) if oldState == manager.TunnelUnknown { - showErrorCustom(tray.mtw, "Failed to determine tunnel state", err.Error()) + showErrorCustom(tray.mtw, l18n.Sprintf("Failed to determine tunnel state"), err.Error()) } else if oldState == manager.TunnelStopped { - showErrorCustom(tray.mtw, "Failed to activate tunnel", err.Error()) + showErrorCustom(tray.mtw, l18n.Sprintf("Failed to activate tunnel"), err.Error()) } else if oldState == manager.TunnelStarted { - showErrorCustom(tray.mtw, "Failed to deactivate tunnel", err.Error()) + showErrorCustom(tray.mtw, l18n.Sprintf("Failed to deactivate tunnel"), err.Error()) } }) } @@ -213,7 +213,7 @@ func (tray *Tray) onTunnelChange(tunnel *manager.Tunnel, state manager.TunnelSta tray.updateGlobalState(globalState) tray.SetTunnelState(tunnel, state, err == nil) if !tray.mtw.Visible() && err != nil { - tray.ShowError("WireGuard Tunnel Error", err.Error()) + tray.ShowError(l18n.Sprintf("WireGuard Tunnel Error"), err.Error()) } }) } @@ -234,8 +234,9 @@ func (tray *Tray) updateGlobalState(globalState manager.TunnelState) { } } - tray.SetToolTip(fmt.Sprintf("WireGuard: %s", textForState(globalState, true))) - statusAction.SetText(fmt.Sprintf("Status: %s", textForState(globalState, false))) + tray.SetToolTip(l18n.Sprintf("WireGuard: %s", textForState(globalState, true))) + stateText := textForState(globalState, false) + statusAction.SetText(l18n.Sprintf("Status: %s", stateText)) switch globalState { case manager.TunnelStarting: @@ -274,13 +275,13 @@ func (tray *Tray) SetTunnelState(tunnel *manager.Tunnel, state manager.TunnelSta var sb strings.Builder for i, addr := range config.Interface.Addresses { if i > 0 { - sb.WriteString(", ") + sb.WriteString(l18n.EnumerationSeparator()) } sb.WriteString(addr.String()) } tray.mtw.Synchronize(func() { - activeCIDRsAction.SetText(fmt.Sprintf("Addresses: %s", sb.String())) + activeCIDRsAction.SetText(l18n.Sprintf("Addresses: %s", sb.String())) }) } }() @@ -288,21 +289,21 @@ func (tray *Tray) SetTunnelState(tunnel *manager.Tunnel, state manager.TunnelSta tunnelAction.SetChecked(true) if !wasChecked && showNotifications { icon, _ := iconWithOverlayForState(state, 128) - tray.ShowCustom("WireGuard Activated", fmt.Sprintf("The %s tunnel has been activated.", tunnel.Name), icon) + tray.ShowCustom(l18n.Sprintf("WireGuard Activated"), l18n.Sprintf("The %s tunnel has been activated.", tunnel.Name), icon) } case manager.TunnelStopped: tunnelAction.SetChecked(false) if wasChecked && showNotifications { icon, _ := loadSystemIcon("imageres", 26, 128) // TODO: this icon isn't very good... - tray.ShowCustom("WireGuard Deactivated", fmt.Sprintf("The %s tunnel has been deactivated.", tunnel.Name), icon) + tray.ShowCustom(l18n.Sprintf("WireGuard Deactivated"), l18n.Sprintf("The %s tunnel has been deactivated.", tunnel.Name), icon) } } } func (tray *Tray) UpdateFound() { action := walk.NewAction() - action.SetText("An Update is Available!") + action.SetText(l18n.Sprintf("An Update is Available!")) menuIcon, _ := loadSystemIcon("imageres", 1, 16) action.SetImage(menuIcon) action.SetDefault(true) @@ -319,7 +320,7 @@ func (tray *Tray) UpdateFound() { showUpdateBalloon := func() { icon, _ := loadSystemIcon("imageres", 1, 128) - tray.ShowCustom("WireGuard Update Available", "An update to WireGuard is now available. You are advised to update as soon as possible.", icon) + tray.ShowCustom(l18n.Sprintf("WireGuard Update Available"), l18n.Sprintf("An update to WireGuard is now available. You are advised to update as soon as possible."), icon) } timeSinceStart := time.Now().Sub(startTime) diff --git a/ui/tunnelspage.go b/ui/tunnelspage.go index aed8d157..d5933143 100644 --- a/ui/tunnelspage.go +++ b/ui/tunnelspage.go @@ -7,6 +7,7 @@ package ui import ( "archive/zip" + "errors" "fmt" "io/ioutil" "os" @@ -17,6 +18,7 @@ import ( "github.com/lxn/walk" "golang.zx2c4.com/wireguard/windows/conf" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/manager" ) @@ -45,7 +47,7 @@ func NewTunnelsPage() (*TunnelsPage, error) { } disposables.Add(tp) - tp.SetTitle("Tunnels") + tp.SetTitle(l18n.Sprintf("Tunnels")) tp.SetLayout(walk.NewHBoxLayout()) tp.listContainer, _ = walk.NewComposite(tp) @@ -101,7 +103,7 @@ func NewTunnelsPage() (*TunnelsPage, error) { tp.listView.CurrentIndexChanged().Attach(func() { editTunnel.SetEnabled(tp.listView.CurrentIndex() > -1) }) - editTunnel.SetText("&Edit") + editTunnel.SetText(l18n.Sprintf("&Edit")) editTunnel.Clicked().Attach(tp.onEditTunnel) disposables.Spare() @@ -142,7 +144,7 @@ func (tp *TunnelsPage) CreateToolbar() error { } tp.AddDisposable(addMenu) importAction := walk.NewAction() - importAction.SetText("&Import tunnel(s) from file…") + importAction.SetText(l18n.Sprintf("&Import tunnel(s) from file…")) importActionIcon, _ := loadSystemIcon("imageres", 3, 16) importAction.SetImage(importActionIcon) importAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyO}) @@ -150,7 +152,7 @@ func (tp *TunnelsPage) CreateToolbar() error { importAction.Triggered().Attach(tp.onImport) addMenu.Actions().Add(importAction) addAction := walk.NewAction() - addAction.SetText("Add &empty tunnel…") + addAction.SetText(l18n.Sprintf("Add &empty tunnel…")) addActionIcon, _ := loadSystemIcon("imageres", 2, 16) addAction.SetImage(addActionIcon) addAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyN}) @@ -159,7 +161,7 @@ func (tp *TunnelsPage) CreateToolbar() error { addMenuAction := walk.NewMenuAction(addMenu) addMenuActionIcon, _ := loadSystemIcon("shell32", 149, 16) addMenuAction.SetImage(addMenuActionIcon) - addMenuAction.SetText("Add Tunnel") + addMenuAction.SetText(l18n.Sprintf("Add Tunnel")) addMenuAction.SetToolTip(importAction.Text()) addMenuAction.Triggered().Attach(tp.onImport) tp.listToolbar.Actions().Add(addMenuAction) @@ -170,7 +172,7 @@ func (tp *TunnelsPage) CreateToolbar() error { deleteActionIcon, _ := loadSystemIcon("shell32", 131, 16) deleteAction.SetImage(deleteActionIcon) deleteAction.SetShortcut(walk.Shortcut{0, walk.KeyDelete}) - deleteAction.SetToolTip("Remove selected tunnel(s)") + deleteAction.SetToolTip(l18n.Sprintf("Remove selected tunnel(s)")) deleteAction.Triggered().Attach(tp.onDelete) tp.listToolbar.Actions().Add(deleteAction) tp.listToolbar.Actions().Add(walk.NewSeparatorAction()) @@ -178,7 +180,7 @@ func (tp *TunnelsPage) CreateToolbar() error { exportAction := walk.NewAction() exportActionIcon, _ := loadSystemIcon("imageres", 165, 16) // Or "shell32", 45? exportAction.SetImage(exportActionIcon) - exportAction.SetToolTip("Export all tunnels to zip…") + exportAction.SetToolTip(l18n.Sprintf("Export all tunnels to zip")) exportAction.Triggered().Attach(tp.onExportTunnels) tp.listToolbar.Actions().Add(exportAction) @@ -195,42 +197,42 @@ func (tp *TunnelsPage) CreateToolbar() error { } tp.listView.AddDisposable(contextMenu) toggleAction := walk.NewAction() - toggleAction.SetText("&Toggle") + toggleAction.SetText(l18n.Sprintf("&Toggle")) toggleAction.SetDefault(true) toggleAction.Triggered().Attach(tp.onTunnelsViewItemActivated) contextMenu.Actions().Add(toggleAction) contextMenu.Actions().Add(walk.NewSeparatorAction()) importAction2 := walk.NewAction() - importAction2.SetText("&Import tunnel(s) from file…") + importAction2.SetText(l18n.Sprintf("&Import tunnel(s) from file…")) importAction2.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyO}) importAction2.Triggered().Attach(tp.onImport) contextMenu.Actions().Add(importAction2) tp.ShortcutActions().Add(importAction2) addAction2 := walk.NewAction() - addAction2.SetText("Add &empty tunnel…") + addAction2.SetText(l18n.Sprintf("Add &empty tunnel…")) addAction2.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyN}) addAction2.Triggered().Attach(tp.onAddTunnel) contextMenu.Actions().Add(addAction2) tp.ShortcutActions().Add(addAction2) exportAction2 := walk.NewAction() - exportAction2.SetText("Export all tunnels to &zip…") + exportAction2.SetText(l18n.Sprintf("Export all tunnels to &zip…")) exportAction2.Triggered().Attach(tp.onExportTunnels) contextMenu.Actions().Add(exportAction2) contextMenu.Actions().Add(walk.NewSeparatorAction()) editAction := walk.NewAction() - editAction.SetText("Edit &selected tunnel…") + editAction.SetText(l18n.Sprintf("Edit &selected tunnel…")) editAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyE}) editAction.Triggered().Attach(tp.onEditTunnel) contextMenu.Actions().Add(editAction) tp.ShortcutActions().Add(editAction) deleteAction2 := walk.NewAction() - deleteAction2.SetText("&Remove selected tunnel(s)") + deleteAction2.SetText(l18n.Sprintf("&Remove selected tunnel(s)")) deleteAction2.SetShortcut(walk.Shortcut{0, walk.KeyDelete}) deleteAction2.Triggered().Attach(tp.onDelete) contextMenu.Actions().Add(deleteAction2) tp.listView.ShortcutActions().Add(deleteAction2) selectAllAction := walk.NewAction() - selectAllAction.SetText("Select &all") + selectAllAction.SetText(l18n.Sprintf("Select &all")) selectAllAction.SetShortcut(walk.Shortcut{walk.ModControl, walk.KeyA}) selectAllAction.Triggered().Attach(tp.onSelectAll) contextMenu.Actions().Add(selectAllAction) @@ -324,7 +326,7 @@ func (tp *TunnelsPage) importFiles(paths []string) { } if lastErr != nil || unparsedConfigs == nil { - syncedMsgBox("Error", fmt.Sprintf("Could not import selected configuration: %v", lastErr), walk.MsgBoxIconWarning) + syncedMsgBox(l18n.Sprintf("Error"), l18n.Sprintf("Could not import selected configuration: %v", lastErr), walk.MsgBoxIconWarning) return } @@ -335,7 +337,7 @@ func (tp *TunnelsPage) importFiles(paths []string) { existingTunnelList, err := manager.IPCClientTunnels() if err != nil { - syncedMsgBox("Error", fmt.Sprintf("Could not enumerate existing tunnels: %v", lastErr), walk.MsgBoxIconWarning) + syncedMsgBox(l18n.Sprintf("Error"), l18n.Sprintf("Could not enumerate existing tunnels: %v", lastErr), walk.MsgBoxIconWarning) return } existingLowerTunnels := make(map[string]bool, len(existingTunnelList)) @@ -347,7 +349,7 @@ func (tp *TunnelsPage) importFiles(paths []string) { tp.listView.SetSuspendTunnelsUpdate(true) for _, unparsedConfig := range unparsedConfigs { if existingLowerTunnels[strings.ToLower(unparsedConfig.Name)] { - lastErr = fmt.Errorf("Another tunnel already exists with the name ‘%s’", unparsedConfig.Name) + lastErr = errors.New(l18n.Sprintf("Another tunnel already exists with the name ‘%s’", unparsedConfig.Name)) continue } config, err := conf.FromWgQuickWithUnknownEncoding(unparsedConfig.Config, unparsedConfig.Name) @@ -367,13 +369,13 @@ func (tp *TunnelsPage) importFiles(paths []string) { m, n := configCount, len(unparsedConfigs) switch { case n == 1 && m != n: - syncedMsgBox("Error", fmt.Sprintf("Unable to import configuration: %v", lastErr), walk.MsgBoxIconWarning) + syncedMsgBox(l18n.Sprintf("Error"), l18n.Sprintf("Unable to import configuration: %v", lastErr), walk.MsgBoxIconWarning) case n == 1 && m == n: // nothing case m == n: - syncedMsgBox("Imported tunnels", fmt.Sprintf("Imported %d tunnels", m), walk.MsgBoxIconInformation) + syncedMsgBox(l18n.Sprintf("Imported tunnels"), l18n.Sprintf("Imported %d tunnels", m), walk.MsgBoxIconInformation) case m != n: - syncedMsgBox("Imported tunnels", fmt.Sprintf("Imported %d of %d tunnels", m, n), walk.MsgBoxIconWarning) + syncedMsgBox(l18n.Sprintf("Imported tunnels"), l18n.Sprintf("Imported %d of %d tunnels", m, n), walk.MsgBoxIconWarning) } }() } @@ -405,7 +407,7 @@ func (tp *TunnelsPage) exportTunnels(filePath string) { func (tp *TunnelsPage) addTunnel(config *conf.Config) { _, err := manager.IPCClientNewTunnel(config) if err != nil { - showErrorCustom(tp.Form(), "Unable to create tunnel", err.Error()) + showErrorCustom(tp.Form(), l18n.Sprintf("Unable to create tunnel"), err.Error()) } } @@ -422,11 +424,11 @@ func (tp *TunnelsPage) onTunnelsViewItemActivated() { if err != nil { tp.Synchronize(func() { if oldState == manager.TunnelUnknown { - showErrorCustom(tp.Form(), "Failed to determine tunnel state", err.Error()) + showErrorCustom(tp.Form(), l18n.Sprintf("Failed to determine tunnel state"), err.Error()) } else if oldState == manager.TunnelStopped { - showErrorCustom(tp.Form(), "Failed to activate tunnel", err.Error()) + showErrorCustom(tp.Form(), l18n.Sprintf("Failed to activate tunnel"), err.Error()) } else if oldState == manager.TunnelStarted { - showErrorCustom(tp.Form(), "Failed to deactivate tunnel", err.Error()) + showErrorCustom(tp.Form(), l18n.Sprintf("Failed to deactivate tunnel"), err.Error()) } }) return @@ -466,16 +468,20 @@ func (tp *TunnelsPage) onDelete() { return } - var topic string + var title, question string if len(indices) > 1 { - topic = fmt.Sprintf("%d tunnels", len(indices)) + tunnelCount := len(indices) + title = l18n.Sprintf("Delete %d tunnels", tunnelCount) + question = l18n.Sprintf("Are you sure you would like to delete %d tunnels?", tunnelCount) } else { - topic = fmt.Sprintf("‘%s’", tp.listView.model.tunnels[indices[0]].Name) + tunnelName := tp.listView.model.tunnels[indices[0]].Name + title = l18n.Sprintf("Delete tunnel ‘%s’", tunnelName) + question = l18n.Sprintf("Are you sure you would like to delete tunnel ‘%s’?", tunnelName) } if walk.DlgCmdNo == walk.MsgBox( tp.Form(), - fmt.Sprintf("Delete %s", topic), - fmt.Sprintf("Are you sure you would like to delete %s? You cannot undo this action.", topic), + title, + l18n.Sprintf("%s You cannot undo this action.", question), walk.MsgBoxYesNo|walk.MsgBoxIconWarning) { return } @@ -515,9 +521,9 @@ func (tp *TunnelsPage) onDelete() { if len(errors) > 0 { tp.listView.Synchronize(func() { if len(errors) == 1 { - showErrorCustom(tp.Form(), "Unable to delete tunnel", fmt.Sprintf("A tunnel was unable to be removed: %s", errors[0].Error())) + showErrorCustom(tp.Form(), l18n.Sprintf("Unable to delete tunnel"), l18n.Sprintf("A tunnel was unable to be removed: %s", errors[0].Error())) } else { - showErrorCustom(tp.Form(), "Unable to delete tunnels", fmt.Sprintf("%d tunnels were unable to be removed.", len(errors))) + showErrorCustom(tp.Form(), l18n.Sprintf("Unable to delete tunnels"), l18n.Sprintf("%d tunnels were unable to be removed.", len(errors))) } }) } @@ -530,8 +536,8 @@ func (tp *TunnelsPage) onSelectAll() { func (tp *TunnelsPage) onImport() { dlg := walk.FileDialog{ - Filter: "Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*", - Title: "Import tunnel(s) from file", + Filter: l18n.Sprintf("Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*"), + Title: l18n.Sprintf("Import tunnel(s) from file"), } if ok, _ := dlg.ShowOpenMultiple(tp.Form()); !ok { @@ -543,8 +549,8 @@ func (tp *TunnelsPage) onImport() { func (tp *TunnelsPage) onExportTunnels() { dlg := walk.FileDialog{ - Filter: "Configuration ZIP Files (*.zip)|*.zip", - Title: "Export tunnels to zip", + Filter: l18n.Sprintf("Configuration ZIP Files (*.zip)|*.zip"), + Title: l18n.Sprintf("Export tunnels to zip"), } if ok, _ := dlg.ShowSave(tp.Form()); !ok { @@ -571,7 +577,7 @@ func (tp *TunnelsPage) swapFiller(enabled bool) bool { func (tp *TunnelsPage) onTunnelsChanged() { if tp.swapFiller(tp.listView.model.RowCount() == 0) { - tp.fillerButton.SetText("Import tunnel(s) from file") + tp.fillerButton.SetText(l18n.Sprintf("Import tunnel(s) from file")) tp.fillerHandler = tp.onImport } } @@ -581,8 +587,9 @@ func (tp *TunnelsPage) onSelectedTunnelsChanged() { return } indices := tp.listView.SelectedIndexes() - if tp.swapFiller(len(indices) > 1) { - tp.fillerButton.SetText(fmt.Sprintf("Delete %d tunnels", len(indices))) + tunnelCount := len(indices) + if tp.swapFiller(tunnelCount > 1) { + tp.fillerButton.SetText(l18n.Sprintf("Delete %d tunnels", tunnelCount)) tp.fillerHandler = tp.onDelete } } @@ -15,6 +15,7 @@ import ( "github.com/lxn/win" "golang.org/x/sys/windows" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/manager" "golang.zx2c4.com/wireguard/windows/version" ) @@ -75,7 +76,7 @@ func RunUI() { tray.UpdateFound() } case manager.UpdateStateUpdatesDisabledUnofficialBuild: - mtw.SetTitle(mtw.Title() + " (unsigned build, no updates)") + mtw.SetTitle(l18n.Sprintf("%s (unsigned build, no updates)", mtw.Title())) } }) } @@ -100,7 +101,7 @@ func RunUI() { if shouldQuitManagerWhenExiting { _, err := manager.IPCClientQuit(true) if err != nil { - showErrorCustom(nil, "Error Exiting WireGuard", fmt.Sprintf("Unable to exit service due to: %v. You may want to stop WireGuard from the service manager.", err)) + showErrorCustom(nil, l18n.Sprintf("Error Exiting WireGuard"), l18n.Sprintf("Unable to exit service due to: %v. You may want to stop WireGuard from the service manager.", err)) } } } @@ -115,7 +116,7 @@ func showError(err error, owner walk.Form) bool { return false } - showErrorCustom(owner, "Error", err.Error()) + showErrorCustom(owner, l18n.Sprintf("Error"), err.Error()) return true } diff --git a/ui/updatepage.go b/ui/updatepage.go index 9e73781f..1ed0b74c 100644 --- a/ui/updatepage.go +++ b/ui/updatepage.go @@ -6,10 +6,9 @@ package ui import ( - "fmt" - "github.com/lxn/walk" + "golang.zx2c4.com/wireguard/windows/l18n" "golang.zx2c4.com/wireguard/windows/manager" "golang.zx2c4.com/wireguard/windows/updater" ) @@ -30,7 +29,7 @@ func NewUpdatePage() (*UpdatePage, error) { } disposables.Add(up) - up.SetTitle("An Update is Available!") + up.SetTitle(l18n.Sprintf("An Update is Available!")) tabIcon, _ := loadSystemIcon("imageres", 1, 16) up.SetImage(tabIcon) @@ -41,14 +40,14 @@ func NewUpdatePage() (*UpdatePage, error) { if err != nil { return nil, err } - instructions.SetText("An update to WireGuard is available. It is highly advisable to update without delay.") + instructions.SetText(l18n.Sprintf("An update to WireGuard is available. It is highly advisable to update without delay.")) instructions.SetMinMaxSize(walk.Size{1, 0}, walk.Size{0, 0}) status, err := walk.NewTextLabel(up) if err != nil { return nil, err } - status.SetText("Status: Waiting for user") + status.SetText(l18n.Sprintf("Status: Waiting for user")) status.SetMinMaxSize(walk.Size{1, 0}, walk.Size{0, 0}) bar, err := walk.NewProgressBar(up) @@ -63,7 +62,7 @@ func NewUpdatePage() (*UpdatePage, error) { } updateIcon, _ := loadSystemIcon("shell32", 46, 32) button.SetImage(updateIcon) - button.SetText("Update Now") + button.SetText(l18n.Sprintf("Update Now")) walk.NewVSpacer(up) @@ -75,7 +74,7 @@ func NewUpdatePage() (*UpdatePage, error) { bar.SetVisible(true) bar.SetMarqueeMode(true) up.SetSuspended(false) - status.SetText("Status: Waiting for updater service") + status.SetText(l18n.Sprintf("Status: Waiting for updater service")) } } @@ -97,7 +96,7 @@ func NewUpdatePage() (*UpdatePage, error) { err := manager.IPCClientUpdate() if err != nil { switchToReadyState() - status.SetText(fmt.Sprintf("Error: %v. Please try again.", err)) + status.SetText(l18n.Sprintf("Error: %v. Please try again.", err)) } }) @@ -106,11 +105,13 @@ func NewUpdatePage() (*UpdatePage, error) { switchToUpdatingState() if dp.Error != nil { switchToReadyState() - status.SetText(fmt.Sprintf("Error: %v. Please try again.", dp.Error)) + err := dp.Error + status.SetText(l18n.Sprintf("Error: %v. Please try again.", err)) return } if len(dp.Activity) > 0 { - status.SetText(fmt.Sprintf("Status: %s", dp.Activity)) + stateText := dp.Activity + status.SetText(l18n.Sprintf("Status: %s", stateText)) } if dp.BytesTotal > 0 { bar.SetMarqueeMode(false) @@ -123,7 +124,7 @@ func NewUpdatePage() (*UpdatePage, error) { } if dp.Complete { switchToReadyState() - status.SetText("Status: Complete!") + status.SetText(l18n.Sprintf("Status: Complete!")) return } }) diff --git a/zgotext.go b/zgotext.go new file mode 100644 index 00000000..4e96cb3f --- /dev/null +++ b/zgotext.go @@ -0,0 +1,357 @@ +// Code generated by running "go generate" in golang.org/x/text. DO NOT EDIT. + +package main + +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": &dictionary{index: enIndex, data: enData}, + } + fallback := language.MustParse("en") + cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback)) + if err != nil { + panic(err) + } + message.DefaultCatalog = cat +} + +var messageKeyToIndex = map[string]int{ + "%.2f\u00a0GiB": 21, + "%.2f\u00a0KiB": 19, + "%.2f\u00a0MiB": 20, + "%.2f\u00a0TiB": 22, + "%d day(s)": 13, + "%d hour(s)": 14, + "%d minute(s)": 15, + "%d second(s)": 16, + "%d tunnels were unable to be removed.": 157, + "%d year(s)": 12, + "%d\u00a0B": 18, + "%s\n\nPlease consult the log for more information.": 110, + "%s (out of date)": 111, + "%s (unsigned build, no updates)": 162, + "%s You cannot undo this action.": 153, + "%s ago": 17, + "%s received, %s sent": 70, + "%s: %q": 23, + "&About WireGuard…": 108, + "&Activate": 57, + "&Block untunneled traffic (kill-switch)": 81, + "&Configuration:": 85, + "&Copy": 101, + "&Deactivate": 56, + "&Edit": 132, + "&Import tunnel(s) from file…": 118, + "&Manage tunnels…": 117, + "&Name:": 78, + "&Public key:": 79, + "&Remove selected tunnel(s)": 140, + "&Save": 83, + "&Save to file…": 103, + "&Toggle": 137, + "(no argument): elevate and install manager service": 1, + "(unknown)": 80, + "A name is required.": 87, + "A tunnel was unable to be removed: %s": 155, + "About WireGuard": 50, + "Activating": 96, + "Active": 95, + "Add &empty tunnel…": 133, + "Add Tunnel": 134, + "Addresses:": 61, + "Addresses: %s": 123, + "Addresses: None": 116, + "All peers must have public keys": 44, + "Allowed IPs:": 64, + "An Update is Available!": 128, + "An interface must have a private key": 42, + "An update to WireGuard is available. It is highly advisable to update without delay.": 165, + "An update to WireGuard is now available. You are advised to update as soon as possible.": 130, + "Another tunnel already exists with the name ‘%s’": 143, + "Another tunnel already exists with the name ‘%s’.": 91, + "App version: %s\nGo backend version: %s\nGo version: %s\nOperating system: %s\nArchitecture: %s": 52, + "Are you sure you would like to delete %d tunnels?": 150, + "Are you sure you would like to delete tunnel ‘%s’?": 152, + "Brackets must contain an IPv6 address": 28, + "Cancel": 84, + "Close": 53, + "Command Line Options": 3, + "Configuration Files (*.zip, *.conf)|*.zip;*.conf|All Files (*.*)|*.*": 158, + "Configuration ZIP Files (*.zip)|*.zip": 160, + "Could not enumerate existing tunnels: %v": 142, + "Could not import selected configuration: %v": 141, + "Create new tunnel": 76, + "DNS servers:": 62, + "Deactivating": 98, + "Delete %d tunnels": 149, + "Delete tunnel ‘%s’": 151, + "E&xit": 119, + "Edit &selected tunnel…": 139, + "Edit tunnel": 77, + "Endpoint:": 65, + "Error": 0, + "Error Exiting WireGuard": 163, + "Error in getting configuration": 45, + "Error: %v. Please try again.": 169, + "Export all tunnels to &zip…": 138, + "Export all tunnels to zip": 136, + "Export log to file": 107, + "Export tunnels to zip": 161, + "Failed to activate tunnel": 72, + "Failed to deactivate tunnel": 73, + "Failed to determine tunnel state": 71, + "File ‘%s’ already exists.\n\nDo you want to overwrite it?": 94, + "Import tunnel(s) from file": 159, + "Imported %d of %d tunnels": 147, + "Imported %d tunnels": 146, + "Imported tunnels": 145, + "Inactive": 97, + "Interface: %s": 74, + "Invalid IP address": 24, + "Invalid MTU": 29, + "Invalid config key is missing an equals separator": 38, + "Invalid endpoint host": 27, + "Invalid key for [Interface] section": 40, + "Invalid key for [Peer] section": 41, + "Invalid key for interface section": 46, + "Invalid key for peer section": 48, + "Invalid key: %v": 32, + "Invalid name": 86, + "Invalid network prefix length": 25, + "Invalid persistent keepalive": 31, + "Invalid port": 30, + "Key must have a value": 39, + "Keys must decode to exactly 32 bytes": 33, + "Latest handshake:": 67, + "Line must occur in a section": 37, + "Listen port:": 59, + "Log": 100, + "Log message": 105, + "MTU:": 60, + "Missing port from endpoint": 26, + "Now": 10, + "Number must be a number between 0 and 2^64-1: %v": 34, + "Peer": 75, + "Persistent keepalive:": 66, + "Preshared key:": 63, + "Protocol version must be 1": 47, + "Public key:": 58, + "Remove selected tunnel(s)": 135, + "Select &all": 102, + "Status:": 55, + "Status: %s": 122, + "Status: Complete!": 170, + "Status: Unknown": 115, + "Status: Waiting for updater service": 168, + "Status: Waiting for user": 166, + "System clock wound backward!": 11, + "Text Files (*.txt)|*.txt|All Files (*.*)|*.*": 106, + "The %s tunnel has been activated.": 125, + "The %s tunnel has been deactivated.": 127, + "Time": 104, + "Transfer:": 68, + "Tunnel Error": 109, + "Tunnel already exists": 90, + "Tunnel name is not valid": 36, + "Tunnel name ‘%s’ is invalid.": 88, + "Tunnels": 131, + "Two commas in a row": 35, + "Unable to create new configuration": 92, + "Unable to create tunnel": 148, + "Unable to delete tunnel": 154, + "Unable to delete tunnels": 156, + "Unable to determine whether the process is running under WOW64: %v": 4, + "Unable to exit service due to: %v. You may want to stop WireGuard from the service manager.": 164, + "Unable to import configuration: %v": 144, + "Unable to list existing tunnels": 89, + "Unable to open current process token: %v": 6, + "Unable to wait for WireGuard window to appear: %v": 113, + "Unknown state": 99, + "Update Now": 167, + "Usage: %s [\n%s]": 2, + "When a configuration has exactly one peer, and that peer has an allowed IPs containing at least one of 0.0.0.0/0 or ::/0, then the tunnel service engages a firewall ruleset to block all traffic that is neither to nor from the tunnel interface, with special exceptions for DHCP and NDP.": 82, + "WireGuard Activated": 124, + "WireGuard Deactivated": 126, + "WireGuard Detection Error": 112, + "WireGuard Tunnel Error": 120, + "WireGuard Update Available": 129, + "WireGuard is running, but the UI is only accessible from desktops of the Builtin %s group.": 8, + "WireGuard logo image": 51, + "WireGuard may only be used by users who are a member of the Builtin %s group.": 7, + "WireGuard system tray icon did not appear after 30 seconds.": 9, + "WireGuard: %s": 121, + "WireGuard: Deactivated": 114, + "Writing file failed": 93, + "You must use the 64-bit version of WireGuard on this computer.": 5, + "[EnumerationSeparator]": 49, + "[none specified]": 43, + "enabled": 69, + "http2: Framer %p: failed to decode just-written frame": 171, + "http2: Framer %p: read %v": 173, + "http2: Framer %p: wrote %v": 172, + "http2: decoded hpack field %+v": 174, + "♥ &Donate!": 54, +} + +var enIndex = []uint32{ // 176 elements + // Entry 0 - 1F + 0x00000000, 0x00000006, 0x00000039, 0x0000004f, + 0x00000064, 0x000000aa, 0x000000e9, 0x00000115, + 0x00000166, 0x000001c4, 0x00000200, 0x00000204, + 0x00000221, 0x00000241, 0x0000025f, 0x0000027f, + 0x000002a3, 0x000002c7, 0x000002d1, 0x000002da, + 0x000002e7, 0x000002f4, 0x00000301, 0x0000030e, + 0x0000031b, 0x0000032e, 0x0000034c, 0x00000367, + 0x0000037d, 0x000003a3, 0x000003af, 0x000003bc, + // Entry 20 - 3F + 0x000003d9, 0x000003ec, 0x00000411, 0x00000445, + 0x00000459, 0x00000472, 0x0000048f, 0x000004c1, + 0x000004d7, 0x000004fb, 0x0000051a, 0x0000053f, + 0x00000550, 0x00000570, 0x0000058f, 0x000005b1, + 0x000005cc, 0x000005e9, 0x000005ec, 0x000005fc, + 0x00000611, 0x0000067c, 0x00000682, 0x0000068f, + 0x00000697, 0x000006a3, 0x000006ad, 0x000006b9, + 0x000006c6, 0x000006cb, 0x000006d6, 0x000006e3, + // Entry 40 - 5F + 0x000006f2, 0x000006ff, 0x00000709, 0x0000071f, + 0x00000731, 0x0000073b, 0x00000743, 0x0000075e, + 0x0000077f, 0x00000799, 0x000007b5, 0x000007c6, + 0x000007cb, 0x000007dd, 0x000007e9, 0x000007f0, + 0x000007fd, 0x00000807, 0x0000082f, 0x0000094d, + 0x00000953, 0x0000095a, 0x0000096a, 0x00000977, + 0x0000098b, 0x000009af, 0x000009cf, 0x000009e5, + 0x00000a1e, 0x00000a41, 0x00000a55, 0x00000a94, + // Entry 60 - 7F + 0x00000a9b, 0x00000aa6, 0x00000aaf, 0x00000abc, + 0x00000aca, 0x00000ace, 0x00000ad4, 0x00000ae0, + 0x00000af1, 0x00000af6, 0x00000b02, 0x00000b2f, + 0x00000b42, 0x00000b56, 0x00000b63, 0x00000b97, + 0x00000bab, 0x00000bc5, 0x00000bfa, 0x00000c11, + 0x00000c21, 0x00000c31, 0x00000c44, 0x00000c63, + 0x00000c69, 0x00000c80, 0x00000c91, 0x00000c9f, + 0x00000cb0, 0x00000cc4, 0x00000ce9, 0x00000cff, + // Entry 80 - 9F + 0x00000d26, 0x00000d3e, 0x00000d59, 0x00000db1, + 0x00000db9, 0x00000dbf, 0x00000dd4, 0x00000ddf, + 0x00000df9, 0x00000e13, 0x00000e1b, 0x00000e39, + 0x00000e52, 0x00000e6d, 0x00000e9c, 0x00000ec8, + 0x00000f00, 0x00000f26, 0x00000f37, 0x00000f6d, + 0x00000fb4, 0x00000fcc, 0x00000ffe, 0x00001070, + 0x0000108a, 0x000010c4, 0x000010e7, 0x000010ff, + 0x00001128, 0x00001141, 0x0000119a, 0x000011df, + // Entry A0 - BF + 0x000011fa, 0x00001220, 0x00001236, 0x00001259, + 0x00001271, 0x000012d0, 0x00001325, 0x0000133e, + 0x00001349, 0x0000136d, 0x0000138d, 0x0000139f, + 0x000013d8, 0x000013f9, 0x00001419, 0x0000143b, +} // Size: 716 bytes + +const enData string = "" + // Size: 5179 bytes + "\x02Error\x02(no argument): elevate and install manager service\x02Usage" + + ": %[1]s [\x0a%[2]s]\x02Command Line Options\x02Unable to determine wheth" + + "er the process is running under WOW64: %[1]v\x02You must use the 64-bit " + + "version of WireGuard on this computer.\x02Unable to open current process" + + " token: %[1]v\x02WireGuard may only be used by users who are a member of" + + " the Builtin %[1]s group.\x02WireGuard is running, but the UI is only ac" + + "cessible from desktops of the Builtin %[1]s group.\x02WireGuard system t" + + "ray icon did not appear after 30 seconds.\x02Now\x02System clock wound b" + + "ackward!\x14\x01\x81\x01\x00\x02\x0b\x02%[1]d year\x00\x0c\x02%[1]d year" + + "s\x14\x01\x81\x01\x00\x02\x0a\x02%[1]d day\x00\x0b\x02%[1]d days\x14\x01" + + "\x81\x01\x00\x02\x0b\x02%[1]d hour\x00\x0c\x02%[1]d hours\x14\x01\x81" + + "\x01\x00\x02\x0d\x02%[1]d minute\x00\x0e\x02%[1]d minutes\x14\x01\x81" + + "\x01\x00\x02\x0d\x02%[1]d second\x00\x0e\x02%[1]d seconds\x02%[1]s ago" + + "\x02%[1]d\u00a0B\x02%.2[1]f\u00a0KiB\x02%.2[1]f\u00a0MiB\x02%.2[1]f" + + "\u00a0GiB\x02%.2[1]f\u00a0TiB\x02%[1]s: %[2]q\x02Invalid IP address\x02I" + + "nvalid network prefix length\x02Missing port from endpoint\x02Invalid en" + + "dpoint host\x02Brackets must contain an IPv6 address\x02Invalid MTU\x02I" + + "nvalid port\x02Invalid persistent keepalive\x02Invalid key: %[1]v\x02Key" + + "s must decode to exactly 32 bytes\x02Number must be a number between 0 a" + + "nd 2^64-1: %[1]v\x02Two commas in a row\x02Tunnel name is not valid\x02L" + + "ine must occur in a section\x02Invalid config key is missing an equals s" + + "eparator\x02Key must have a value\x02Invalid key for [Interface] section" + + "\x02Invalid key for [Peer] section\x02An interface must have a private k" + + "ey\x02[none specified]\x02All peers must have public keys\x02Error in ge" + + "tting configuration\x02Invalid key for interface section\x02Protocol ver" + + "sion must be 1\x02Invalid key for peer section\x02, \x02About WireGuard" + + "\x02WireGuard logo image\x02App version: %[1]s\x0aGo backend version: %[" + + "2]s\x0aGo version: %[3]s\x0aOperating system: %[4]s\x0aArchitecture: %[5" + + "]s\x02Close\x02♥ &Donate!\x02Status:\x02&Deactivate\x02&Activate\x02Publ" + + "ic key:\x02Listen port:\x02MTU:\x02Addresses:\x02DNS servers:\x02Preshar" + + "ed key:\x02Allowed IPs:\x02Endpoint:\x02Persistent keepalive:\x02Latest " + + "handshake:\x02Transfer:\x02enabled\x02%[1]s received, %[2]s sent\x02Fail" + + "ed to determine tunnel state\x02Failed to activate tunnel\x02Failed to d" + + "eactivate tunnel\x02Interface: %[1]s\x02Peer\x02Create new tunnel\x02Edi" + + "t tunnel\x02&Name:\x02&Public key:\x02(unknown)\x02&Block untunneled tra" + + "ffic (kill-switch)\x02When a configuration has exactly one peer, and tha" + + "t peer has an allowed IPs containing at least one of 0.0.0.0/0 or ::/0, " + + "then the tunnel service engages a firewall ruleset to block all traffic " + + "that is neither to nor from the tunnel interface, with special exception" + + "s for DHCP and NDP.\x02&Save\x02Cancel\x02&Configuration:\x02Invalid nam" + + "e\x02A name is required.\x02Tunnel name ‘%[1]s’ is invalid.\x02Unable to" + + " list existing tunnels\x02Tunnel already exists\x02Another tunnel alread" + + "y exists with the name ‘%[1]s’.\x02Unable to create new configuration" + + "\x02Writing file failed\x02File ‘%[1]s’ already exists.\x0a\x0aDo you wa" + + "nt to overwrite it?\x02Active\x02Activating\x02Inactive\x02Deactivating" + + "\x02Unknown state\x02Log\x02&Copy\x02Select &all\x02&Save to file…\x02Ti" + + "me\x02Log message\x02Text Files (*.txt)|*.txt|All Files (*.*)|*.*\x02Exp" + + "ort log to file\x02&About WireGuard…\x02Tunnel Error\x02%[1]s\x0a\x0aPle" + + "ase consult the log for more information.\x02%[1]s (out of date)\x02Wire" + + "Guard Detection Error\x02Unable to wait for WireGuard window to appear: " + + "%[1]v\x02WireGuard: Deactivated\x02Status: Unknown\x02Addresses: None" + + "\x02&Manage tunnels…\x02&Import tunnel(s) from file…\x02E&xit\x02WireGua" + + "rd Tunnel Error\x02WireGuard: %[1]s\x02Status: %[1]s\x02Addresses: %[1]s" + + "\x02WireGuard Activated\x02The %[1]s tunnel has been activated.\x02WireG" + + "uard Deactivated\x02The %[1]s tunnel has been deactivated.\x02An Update " + + "is Available!\x02WireGuard Update Available\x02An update to WireGuard is" + + " now available. You are advised to update as soon as possible.\x02Tunnel" + + "s\x02&Edit\x02Add &empty tunnel…\x02Add Tunnel\x02Remove selected tunnel" + + "(s)\x02Export all tunnels to zip\x02&Toggle\x02Export all tunnels to &zi" + + "p…\x02Edit &selected tunnel…\x02&Remove selected tunnel(s)\x02Could not " + + "import selected configuration: %[1]v\x02Could not enumerate existing tun" + + "nels: %[1]v\x02Another tunnel already exists with the name ‘%[1]s’\x02Un" + + "able to import configuration: %[1]v\x02Imported tunnels\x14\x01\x81\x01" + + "\x00\x02\x16\x02Imported %[1]d tunnel\x00\x17\x02Imported %[1]d tunnels" + + "\x14\x02\x80\x01\x02\x1f\x02Imported %[1]d of %[2]d tunnel\x00 \x02Impor" + + "ted %[1]d of %[2]d tunnels\x02Unable to create tunnel\x14\x01\x81\x01" + + "\x00\x02\x14\x02Delete %[1]d tunnel\x00\x15\x02Delete %[1]d tunnels\x14" + + "\x01\x81\x01\x00\x024\x02Are you sure you would like to delete %[1]d tun" + + "nel?\x005\x02Are you sure you would like to delete %[1]d tunnels?\x02Del" + + "ete tunnel ‘%[1]s’\x02Are you sure you would like to delete tunnel ‘%[1]" + + "s’?\x02%[1]s You cannot undo this action.\x02Unable to delete tunnel\x02" + + "A tunnel was unable to be removed: %[1]s\x02Unable to delete tunnels\x14" + + "\x01\x81\x01\x00\x02'\x02%[1]d tunnel was unable to be removed.\x00)\x02" + + "%[1]d tunnels were unable to be removed.\x02Configuration Files (*.zip, " + + "*.conf)|*.zip;*.conf|All Files (*.*)|*.*\x02Import tunnel(s) from file" + + "\x02Configuration ZIP Files (*.zip)|*.zip\x02Export tunnels to zip\x02%[" + + "1]s (unsigned build, no updates)\x02Error Exiting WireGuard\x02Unable to" + + " exit service due to: %[1]v. You may want to stop WireGuard from the ser" + + "vice manager.\x02An update to WireGuard is available. It is highly advis" + + "able to update without delay.\x02Status: Waiting for user\x02Update Now" + + "\x02Status: Waiting for updater service\x02Error: %[1]v. Please try agai" + + "n.\x02Status: Complete!\x02http2: Framer %[1]p: failed to decode just-wr" + + "itten frame\x02http2: Framer %[1]p: wrote %[2]v\x02http2: Framer %[1]p: " + + "read %[2]v\x02http2: decoded hpack field %+[1]v" + + // Total table size 5895 bytes (5KiB); checksum: ED8BBF53 |