Browse Source

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>
master
Simon Rozman 4 years ago
committed by Jason A. Donenfeld
parent
commit
8a476b3261
24 changed files with 2669 additions and 247 deletions
  1. +1
    -0
      .gitignore
  2. +5
    -0
      Makefile
  3. +22
    -0
      README.md
  4. +4
    -0
      build.bat
  5. +17
    -27
      conf/config.go
  6. +32
    -31
      conf/parser.go
  7. +1
    -0
      go.sum
  8. +65
    -0
      l18n/l18n.go
  9. +1949
    -0
      locales/en/messages.gotext.json
  10. +20
    -18
      main.go
  11. +37
    -27
      resources.rc
  12. +6
    -6
      ui/aboutdialog.go
  13. +27
    -27
      ui/confview.go
  14. +17
    -17
      ui/editdialog.go
  15. +4
    -3
      ui/filesave.go
  16. +6
    -5
      ui/iconprovider.go
  17. +10
    -9
      ui/logpage.go
  18. +4
    -3
      ui/managewindow.go
  19. +3
    -2
      ui/raise.go
  20. +21
    -20
      ui/tray.go
  21. +45
    -38
      ui/tunnelspage.go
  22. +4
    -3
      ui/ui.go
  23. +12
    -11
      ui/updatepage.go
  24. +357
    -0
      zgotext.go

+ 1
- 0
.gitignore View File

@@ -6,6 +6,7 @@
/amd64

# Misc
/locales/*/out.gotext.json
/sign.bat
*.swp
*.bak


+ 5
- 0
Makefile View File

@@ -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)


+ 22
- 0
README.md View File

@@ -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:


+ 4
- 0
build.bat View File

@@ -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" (


+ 17
- 27
conf/config.go View File

@@ -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() {


+ 32
- 31
conf/parser.go View File

@@ -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}
}
}
}


+ 1
- 0
go.sum View File

@@ -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=


+ 65
- 0
l18n/l18n.go View File

@@ -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 ‘リンゴ、オレンジ、イチゴ’. */)
}

+ 1949
- 0
locales/en/messages.gotext.json
File diff suppressed because it is too large
View File


+ 20
- 18
main.go View 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 {


+ 37
- 27
resources.rc View File

@@ -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/"
)

+ 6
- 6
ui/aboutdialog.go View File

@@ -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


+ 27
- 27
ui/confview.go View File

@@ -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()


+ 17
- 17
ui/editdialog.go View File

@@ -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
}



+ 4
- 3
ui/filesave.go View File

@@ -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


+ 6
- 5
ui/iconprovider.go View File

@@ -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 {


+ 10
- 9
ui/logpage.go View File

@@ -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()


+ 4
- 3
ui/managewindow.go View File

@@ -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


+ 3
- 2
ui/raise.go View File

@@ -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


+ 21
- 20
ui/tray.go View File

@@ -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)


+ 45
- 38
ui/tunnelspage.go View File

@@ -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
}
}

+ 4
- 3
ui/ui.go View File

@@ -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
}


+ 12
- 11
ui/updatepage.go View File

@@ -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
}
})


+ 357
- 0
zgotext.go View File

@@ -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

Loading…
Cancel
Save