Skip to content

Commit 1ca1388

Browse files
committed
implement internationalization
We don't have the convenience of Go templates with React, so this updates the internationalization scheme to do everything with Javascript. New translations are separated from old translations by the newui build tag, but a utility is added to import translations from the old set if there is a matching key-value pair in the new set.
1 parent 1edc0da commit 1ca1388

28 files changed

Lines changed: 561 additions & 74 deletions

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ dex/testing/loadbot/loadbot
3535
bin/
3636
bin-v*/
3737
client/webserver/site/template-builder/template-builder
38+
client/webserver/newui/locales/cmd/importlocales/importlocales
3839
dex/testing/btc/harnesschain.tar.gz
3940
client/asset/btc/electrum/example/server/server
4041
client/asset/btc/electrum/example/wallet/wallet

client/webserver/api.go

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,8 @@ func (s *WebServer) apiLocale(w http.ResponseWriter, r *http.Request) {
10061006
return
10071007
}
10081008
resp := make(map[string]string)
1009-
for translationID, defaultTranslation := range enUS {
1009+
defaultLocale := localesMap["en-US"]
1010+
for translationID, defaultTranslation := range defaultLocale {
10101011
t, found := m[translationID]
10111012
if !found {
10121013
t = defaultTranslation
@@ -1605,21 +1606,23 @@ func (s *WebServer) apiUser(w http.ResponseWriter, r *http.Request) {
16051606
}
16061607

16071608
response := struct {
1608-
User *core.User `json:"user"`
1609-
Lang string `json:"lang"`
1610-
Langs []string `json:"langs"`
1611-
Inited bool `json:"inited"`
1612-
OK bool `json:"ok"`
1613-
OnionUrl string `json:"onionUrl"`
1614-
MMStatus *mm.Status `json:"mmStatus"`
1609+
User *core.User `json:"user"`
1610+
Lang string `json:"lang"`
1611+
Langs []string `json:"langs"`
1612+
Inited bool `json:"inited"`
1613+
OK bool `json:"ok"`
1614+
OnionUrl string `json:"onionUrl"`
1615+
MMStatus *mm.Status `json:"mmStatus"`
1616+
NewVersionAvailable bool `json:"newVersionAvailable"`
16151617
}{
1616-
User: u,
1617-
Lang: s.lang.Load().(string),
1618-
Langs: s.langs,
1619-
Inited: s.core.IsInitialized(),
1620-
OK: true,
1621-
OnionUrl: s.onion,
1622-
MMStatus: mmStatus,
1618+
User: u,
1619+
Lang: s.lang.Load().(string),
1620+
Langs: s.langs,
1621+
Inited: s.core.IsInitialized(),
1622+
OK: true,
1623+
OnionUrl: s.onion,
1624+
MMStatus: mmStatus,
1625+
NewVersionAvailable: s.newAppVersionAvailable,
16231626
}
16241627
writeJSON(w, response)
16251628
}

client/webserver/jsintl_newui.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
//go:build newui
2+
3+
// This code is available on the terms of the project LICENSE.md file,
4+
// also available online at https://blueoakcouncil.org/license/1.0.0.
5+
6+
package webserver
7+
8+
import (
9+
"decred.org/dcrdex/client/intl"
10+
"decred.org/dcrdex/client/webserver/newui/locales"
11+
)
12+
13+
var localesMap = locales.Locales
14+
15+
// RegisterTranslations registers translations with the init package for
16+
// translator worksheet preparation.
17+
func RegisterTranslations() {
18+
const callerID = "js"
19+
20+
for lang, ts := range localesMap {
21+
r := intl.NewRegistrar(callerID, lang, len(ts))
22+
for translationID, t := range ts {
23+
r.Register(translationID, t)
24+
}
25+
}
26+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
//go:build !newui
2+
3+
// This code is available on the terms of the project LICENSE.md file,
4+
// also available online at https://blueoakcouncil.org/license/1.0.0.
5+
16
package webserver
27

38
import "decred.org/dcrdex/client/intl"

client/webserver/locales/locales.go

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,14 @@ import (
77
"golang.org/x/text/language"
88
)
99

10-
var (
11-
Locales map[string]map[string]*intl.Translation
12-
)
10+
var Locales = map[string]map[string]*intl.Translation{
11+
"en-US": EnUS,
12+
"pt-BR": PtBr,
13+
"zh-CN": ZhCN,
14+
"pl-PL": PlPL,
15+
"de-DE": DeDE,
16+
"ar": Ar,
17+
}
1318

1419
// RegisterTranslations registers translations with the init package for
1520
// translator worksheet preparation.
@@ -24,15 +29,6 @@ func RegisterTranslations() {
2429
}
2530

2631
func init() {
27-
Locales = map[string]map[string]*intl.Translation{
28-
"en-US": EnUS,
29-
"pt-BR": PtBr,
30-
"zh-CN": ZhCN,
31-
"pl-PL": PlPL,
32-
"de-DE": DeDE,
33-
"ar": Ar,
34-
}
35-
3632
for localeName := range Locales {
3733
_, err := language.Parse(localeName)
3834
if err != nil {
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// This code is available on the terms of the project LICENSE.md file,
2+
// also available online at https://blueoakcouncil.org/license/1.0.0.
3+
package locales
4+
5+
import "decred.org/dcrdex/client/intl"
6+
7+
var Ar = map[string]*intl.Translation{
8+
"Unlock": {T: "الغاء القفل"},
9+
"Assets": {T: "الأصول"},
10+
"Receive": {T: "استلام"},
11+
"Send": {T: "ارسال"},
12+
"Swap": {T: "المقايضة"},
13+
"Trade": {T: "تداول"},
14+
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"fmt"
6+
"maps"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
"slices"
11+
"strings"
12+
13+
"decred.org/dcrdex/client/intl"
14+
oldloc "decred.org/dcrdex/client/webserver/locales"
15+
newloc "decred.org/dcrdex/client/webserver/newui/locales"
16+
)
17+
18+
/*
19+
importlocales looks for entries in the EnUS translations map that don't have
20+
translations in other maps, and then checks the locales from the old UI to
21+
see if there is a translation there that can be used, and updates the files.
22+
*/
23+
24+
type mapMatch struct {
25+
filename string
26+
oldMap map[string]*intl.Translation
27+
newMap map[string]*intl.Translation
28+
}
29+
30+
var mapMatches = []mapMatch{
31+
{"ar.go", oldloc.Ar, newloc.Ar},
32+
{"de-de.go", oldloc.DeDE, newloc.DeDE},
33+
{"pl-pl.go", oldloc.PlPL, newloc.PlPL},
34+
{"pt-br.go", oldloc.PtBr, newloc.PtBr},
35+
{"zh-cn.go", oldloc.ZhCN, newloc.ZhCN},
36+
}
37+
38+
func main() {
39+
newuiLocalesDir, _ := filepath.Abs("../../")
40+
41+
// Extract & sort keys
42+
orderedKeys := slices.Collect(maps.Keys(newloc.EnUS))
43+
slices.Sort(orderedKeys)
44+
45+
// Determine which ones to transfer
46+
var transferKeys []string
47+
for _, key := range orderedKeys {
48+
newTrans := newloc.EnUS[key]
49+
oldTrans, okOld := oldloc.EnUS[key]
50+
if okOld && oldTrans.T == newTrans.T {
51+
transferKeys = append(transferKeys, key)
52+
}
53+
}
54+
55+
for _, match := range mapMatches {
56+
fpath := filepath.Join(newuiLocalesDir, match.filename)
57+
content, err := os.ReadFile(fpath)
58+
if err != nil {
59+
fmt.Println("Error reading file:", fpath)
60+
continue
61+
}
62+
63+
idx := bytes.LastIndexByte(content, '}')
64+
if idx == -1 {
65+
fmt.Println("No closing brace found in", fpath)
66+
continue
67+
}
68+
69+
var newLines []string
70+
for _, key := range transferKeys {
71+
// check if key already exists in actual target map
72+
if _, exists := match.newMap[key]; exists {
73+
continue
74+
}
75+
76+
if oldVal, ok := match.oldMap[key]; ok {
77+
// We need to format it properly.
78+
if oldVal.Version != 0 {
79+
newLines = append(newLines, fmt.Sprintf("\t%q: {Version: %d, T: %q},", key, oldVal.Version, oldVal.T))
80+
} else {
81+
newLines = append(newLines, fmt.Sprintf("\t%q: {T: %q},", key, oldVal.T))
82+
}
83+
}
84+
}
85+
86+
if len(newLines) > 0 {
87+
prefix := string(content[:idx])
88+
prefix = strings.TrimRight(prefix, " \n\r\t")
89+
if !strings.HasSuffix(prefix, "{") {
90+
prefix += "\n"
91+
} else {
92+
prefix += "\n"
93+
}
94+
95+
newContent := prefix + strings.Join(newLines, "\n") + "\n}\n"
96+
err = os.WriteFile(fpath, []byte(newContent), 0644)
97+
if err != nil {
98+
fmt.Println("Error writing file:", fpath, err)
99+
} else {
100+
fmt.Printf("Updated %s with %d entries\n", match.filename, len(newLines))
101+
102+
cmd := exec.Command("gofmt", "-w", fpath)
103+
if err := cmd.Run(); err != nil {
104+
fmt.Println("Error running gofmt on:", fpath, err)
105+
}
106+
}
107+
}
108+
}
109+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// This code is available on the terms of the project LICENSE.md file,
2+
// also available online at https://blueoakcouncil.org/license/1.0.0.
3+
package locales
4+
5+
import "decred.org/dcrdex/client/intl"
6+
7+
var DeDE = map[string]*intl.Translation{
8+
"Unlock": {T: "Entsperren"},
9+
"Assets": {T: "Assets"},
10+
"Receive": {T: "Erhalte "},
11+
"Send": {T: "Senden"},
12+
"Swap": {T: "Swap"},
13+
"Trade": {T: "Handeln"},
14+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// This code is available on the terms of the project LICENSE.md file,
2+
// also available online at https://blueoakcouncil.org/license/1.0.0.
3+
package locales
4+
5+
import "decred.org/dcrdex/client/intl"
6+
7+
var EnUS = map[string]*intl.Translation{
8+
"prompt_for_seed": {T: "Are you restoring Bison Wallet from a seed?"},
9+
"seed_display_warning": {T: "Your seed is shown below. Write it down and keep it safe. You will need it to restore your wallet if you lose your device."},
10+
"Welcome Back!": {T: "Welcome Back!"},
11+
"Unlock": {T: "Unlock"},
12+
"password": {T: "password"},
13+
"Assets": {T: "Assets"},
14+
"Receive": {T: "Receive"},
15+
"Send": {T: "Send"},
16+
"Swap": {T: "Swap"},
17+
"VIEWS": {T: "VIEWS"},
18+
"FAVORITES": {T: "FAVORITES"},
19+
"Logout": {T: "Logout"},
20+
"Portfolio": {T: "Portfolio"},
21+
"Trade": {T: "Trade"},
22+
"New": {T: "New"},
23+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// This code is available on the terms of the project LICENSE.md file,
2+
// also available online at https://blueoakcouncil.org/license/1.0.0.
3+
package locales
4+
5+
import "decred.org/dcrdex/client/intl"
6+
7+
var Locales = map[string]map[string]*intl.Translation{
8+
"en-US": EnUS,
9+
"pt-BR": PtBr,
10+
"zh-CN": ZhCN,
11+
"pl-PL": PlPL,
12+
"de-DE": DeDE,
13+
"ar": Ar,
14+
}

0 commit comments

Comments
 (0)