Skip to content

Commit cef4806

Browse files
authored
Server side localization (#71)
* Improve chat message pushes for APNS * Fix comments and adjust code placement * Add basic utilities for localizing text and fiat amount arguments * Update pushes to use server-side localization * Fix exchange data pushes * Pull in v1.12.3 proto APIs * Localize content in chat service * Export SimulatedUserLocale variable * Fix issue with RTL languages and Kin amounts localized to default language which is LTR * Pushes now refer to user locale from preferences table * Chat service now refers to user locale from preferences table * Trim any spaces for Kin amount suffix
1 parent b968fb5 commit cef4806

File tree

15 files changed

+652
-183
lines changed

15 files changed

+652
-183
lines changed

go.mod

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ require (
2121
github.com/mr-tron/base58 v1.2.0
2222
github.com/newrelic/go-agent/v3 v3.20.1
2323
github.com/newrelic/go-agent/v3/integrations/nrpgx v1.0.0
24+
github.com/nicksnyder/go-i18n/v2 v2.4.0
2425
github.com/ory/dockertest/v3 v3.7.0
2526
github.com/oschwald/maxminddb-golang v1.11.0
2627
github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58
@@ -36,7 +37,7 @@ require (
3637
github.com/ybbus/jsonrpc v2.1.2+incompatible
3738
golang.org/x/crypto v0.14.0
3839
golang.org/x/net v0.17.0
39-
golang.org/x/text v0.13.0
40+
golang.org/x/text v0.14.0
4041
golang.org/x/time v0.0.0-20191024005414-555d28b269f0
4142
google.golang.org/grpc v1.51.0
4243
google.golang.org/protobuf v1.28.1
@@ -104,6 +105,6 @@ require (
104105
google.golang.org/appengine/v2 v2.0.1 // indirect
105106
google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6 // indirect
106107
gopkg.in/ini.v1 v1.51.0 // indirect
107-
gopkg.in/yaml.v2 v2.3.0 // indirect
108+
gopkg.in/yaml.v2 v2.4.0 // indirect
108109
gopkg.in/yaml.v3 v3.0.1 // indirect
109110
)

go.sum

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,9 @@ firebase.google.com/go/v4 v4.8.0 h1:ooJqjFEh1G6DQ5+wyb/RAXAgku0E2RzJeH6WauSpWSo=
6363
firebase.google.com/go/v4 v4.8.0/go.mod h1:y+j6xX7BgBco/XaN+YExIBVm6pzvYutheDV3nprvbWc=
6464
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8=
6565
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8=
66-
github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ=
6766
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
67+
github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8=
68+
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
6869
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
6970
github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc=
7071
github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs=
@@ -410,6 +411,8 @@ github.com/newrelic/go-agent/v3 v3.20.1 h1:xxhPjE/j4z7n82FQV4izRjIkd4E10q4flqgzM
410411
github.com/newrelic/go-agent/v3 v3.20.1/go.mod h1:rT6ZUxJc5rQbWLyCtjqQCOcfb01lKRFbc1yMQkcboWM=
411412
github.com/newrelic/go-agent/v3/integrations/nrpgx v1.0.0 h1:5pj3uXyWB0fpgbeK1yW51go6Y57uRG8F7w5Nu6kIiCQ=
412413
github.com/newrelic/go-agent/v3/integrations/nrpgx v1.0.0/go.mod h1:G4vsr8xgPwFxxwJSbE982D7rswRFEfoCaXPQWWWQyQo=
414+
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
415+
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
413416
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
414417
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
415418
github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U=
@@ -750,8 +753,8 @@ golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
750753
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
751754
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
752755
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
753-
golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
754-
golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
756+
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
757+
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
755758
golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
756759
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
757760
golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs=
@@ -1012,8 +1015,9 @@ gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
10121015
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
10131016
gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
10141017
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
1015-
gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
10161018
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
1019+
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
1020+
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
10171021
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
10181022
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
10191023
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

pkg/code/data/internal.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import (
99

1010
"github.com/google/uuid"
1111
"github.com/jmoiron/sqlx"
12+
"github.com/pkg/errors"
13+
"golang.org/x/text/language"
1214

1315
"github.com/code-payments/code-server/pkg/cache"
1416
currency_lib "github.com/code-payments/code-server/pkg/currency"
@@ -408,6 +410,7 @@ type DatabaseData interface {
408410
// --------------------------------------------------------------------------------
409411
SaveUserPreferences(ctx context.Context, record *preferences.Record) error
410412
GetUserPreferences(ctx context.Context, id *user.DataContainerID) (*preferences.Record, error)
413+
GetUserLocale(ctx context.Context, owner string) (language.Tag, error)
411414

412415
// ExecuteInTx executes fn with a single DB transaction that is scoped to the call.
413416
// This enables more complex transactions that can span many calls across the provider.
@@ -1455,3 +1458,24 @@ func (dp *DatabaseProvider) SaveUserPreferences(ctx context.Context, record *pre
14551458
func (dp *DatabaseProvider) GetUserPreferences(ctx context.Context, id *user.DataContainerID) (*preferences.Record, error) {
14561459
return dp.preferences.Get(ctx, id)
14571460
}
1461+
func (dp *DatabaseProvider) GetUserLocale(ctx context.Context, owner string) (language.Tag, error) {
1462+
verificationRecord, err := dp.GetLatestPhoneVerificationForAccount(ctx, owner)
1463+
if err != nil {
1464+
return language.Und, errors.Wrap(err, "error getting latest phone verification record")
1465+
}
1466+
1467+
dataContainerRecord, err := dp.GetUserDataContainerByPhone(ctx, owner, verificationRecord.PhoneNumber)
1468+
if err != nil {
1469+
return language.Und, errors.Wrap(err, "error getting data container record")
1470+
}
1471+
1472+
userPreferencesRecord, err := dp.GetUserPreferences(ctx, dataContainerRecord.ID)
1473+
switch err {
1474+
case nil:
1475+
return userPreferencesRecord.Locale, nil
1476+
case preferences.ErrPreferencesNotFound:
1477+
return preferences.GetDefaultLocale(), nil
1478+
default:
1479+
return language.Und, errors.Wrap(err, "error getting user preferences record")
1480+
}
1481+
}

pkg/code/data/preferences/preferences.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,8 @@ func GetDefaultPreferences(id *user.DataContainerID) *Record {
5959
LastUpdatedAt: time.Now(),
6060
}
6161
}
62+
63+
// GetDefaultLocale returns the default locale setting
64+
func GetDefaultLocale() language.Tag {
65+
return defaultLocale
66+
}

pkg/code/localization/currency.go

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
1+
package localization
2+
3+
import (
4+
"strings"
5+
6+
"golang.org/x/text/language"
7+
"golang.org/x/text/message"
8+
"golang.org/x/text/number"
9+
10+
currency_lib "github.com/code-payments/code-server/pkg/currency"
11+
"github.com/pkg/errors"
12+
13+
chatpb "github.com/code-payments/code-protobuf-api/generated/go/chat/v1"
14+
)
15+
16+
var symbolByCode = map[currency_lib.Code]string{
17+
currency_lib.AED: "د.إ",
18+
currency_lib.AFN: "؋",
19+
currency_lib.ALL: "Lek",
20+
currency_lib.ANG: "ƒ",
21+
currency_lib.AOA: "Kz",
22+
currency_lib.ARS: "$",
23+
currency_lib.AUD: "$",
24+
currency_lib.AWG: "ƒ",
25+
currency_lib.AZN: "₼",
26+
currency_lib.BAM: "KM",
27+
currency_lib.BDT: "৳",
28+
currency_lib.BBD: "$",
29+
currency_lib.BGN: "лв",
30+
currency_lib.BMD: "$",
31+
currency_lib.BND: "$",
32+
currency_lib.BOB: "$b",
33+
currency_lib.BRL: "R$",
34+
currency_lib.BSD: "$",
35+
currency_lib.BWP: "P",
36+
currency_lib.BYN: "Br",
37+
currency_lib.BZD: "BZ$",
38+
currency_lib.CAD: "$",
39+
currency_lib.CHF: "CHF",
40+
currency_lib.CLP: "$",
41+
currency_lib.CNY: "¥",
42+
currency_lib.COP: "$",
43+
currency_lib.CRC: "₡",
44+
currency_lib.CUC: "$",
45+
currency_lib.CUP: "₱",
46+
currency_lib.CZK: "Kč",
47+
currency_lib.DKK: "kr",
48+
currency_lib.DOP: "RD$",
49+
currency_lib.EGP: "£",
50+
currency_lib.ERN: "£",
51+
currency_lib.EUR: "€",
52+
currency_lib.FJD: "$",
53+
currency_lib.FKP: "£",
54+
currency_lib.GBP: "£",
55+
currency_lib.GEL: "₾",
56+
currency_lib.GGP: "£",
57+
currency_lib.GHS: "¢",
58+
currency_lib.GIP: "£",
59+
currency_lib.GNF: "FG",
60+
currency_lib.GTQ: "Q",
61+
currency_lib.GYD: "$",
62+
currency_lib.HKD: "$",
63+
currency_lib.HNL: "L",
64+
currency_lib.HRK: "kn",
65+
currency_lib.HUF: "Ft",
66+
currency_lib.IDR: "Rp",
67+
currency_lib.ILS: "₪",
68+
currency_lib.IMP: "£",
69+
currency_lib.INR: "₹",
70+
currency_lib.IRR: "﷼",
71+
currency_lib.ISK: "kr",
72+
currency_lib.JEP: "£",
73+
currency_lib.JMD: "J$",
74+
currency_lib.JPY: "¥",
75+
currency_lib.KGS: "лв",
76+
currency_lib.KHR: "៛",
77+
currency_lib.KMF: "CF",
78+
currency_lib.KPW: "₩",
79+
currency_lib.KRW: "₩",
80+
currency_lib.KYD: "$",
81+
currency_lib.KZT: "лв",
82+
currency_lib.LAK: "₭",
83+
currency_lib.LBP: "£",
84+
currency_lib.LKR: "₨",
85+
currency_lib.LRD: "$",
86+
currency_lib.LTL: "Lt",
87+
currency_lib.LVL: "Ls",
88+
currency_lib.MGA: "Ar",
89+
currency_lib.MKD: "ден",
90+
currency_lib.MMK: "K",
91+
currency_lib.MNT: "₮",
92+
currency_lib.MUR: "₨",
93+
currency_lib.MXN: "$",
94+
currency_lib.MYR: "RM",
95+
currency_lib.MZN: "MT",
96+
currency_lib.NAD: "$",
97+
currency_lib.NGN: "₦",
98+
currency_lib.NIO: "C$",
99+
currency_lib.NOK: "kr",
100+
currency_lib.NPR: "₨",
101+
currency_lib.NZD: "$",
102+
currency_lib.OMR: "﷼",
103+
currency_lib.PAB: "B/.",
104+
currency_lib.PEN: "S/.",
105+
currency_lib.PHP: "₱",
106+
currency_lib.PKR: "₨",
107+
currency_lib.PLN: "zł",
108+
currency_lib.PYG: "Gs",
109+
currency_lib.QAR: "﷼",
110+
currency_lib.RON: "lei",
111+
currency_lib.RSD: "Дин.",
112+
currency_lib.RUB: "₽",
113+
currency_lib.RWF: "RF",
114+
currency_lib.SAR: "﷼",
115+
currency_lib.SBD: "$",
116+
currency_lib.SCR: "₨",
117+
currency_lib.SEK: "kr",
118+
currency_lib.SGD: "$",
119+
currency_lib.SHP: "£",
120+
currency_lib.SOS: "S",
121+
currency_lib.SRD: "$",
122+
currency_lib.SSP: "£",
123+
currency_lib.STD: "Db",
124+
currency_lib.SVC: "$",
125+
currency_lib.SYP: "£",
126+
currency_lib.THB: "฿",
127+
currency_lib.TOP: "T$",
128+
currency_lib.TRY: "₺",
129+
currency_lib.TTD: "TT$",
130+
currency_lib.TWD: "NT$",
131+
currency_lib.UAH: "₴",
132+
currency_lib.USD: "$",
133+
currency_lib.UYU: "$U",
134+
currency_lib.UZS: "лв",
135+
currency_lib.VND: "₫",
136+
currency_lib.XCD: "$",
137+
currency_lib.YER: "﷼",
138+
currency_lib.ZAR: "R",
139+
currency_lib.ZMW: "ZK",
140+
}
141+
142+
// FormatFiat formats a currency amount into a string in the provided locale
143+
func FormatFiat(locale language.Tag, code currency_lib.Code, amount float64, ofKin bool) (string, error) {
144+
isRtlScript := isRtlScript(locale)
145+
146+
decimals := 2
147+
if code == currency_lib.KIN {
148+
decimals = 0
149+
amount = float64(uint64(amount))
150+
}
151+
152+
printer := message.NewPrinter(locale)
153+
amountAsDecimal := number.Decimal(amount, number.Scale(decimals))
154+
formattedAmount := printer.Sprint(amountAsDecimal)
155+
156+
symbol := symbolByCode[code]
157+
158+
suffixKey := CoreOfKin
159+
if code == currency_lib.KIN {
160+
suffixKey = CoreKin
161+
}
162+
163+
localizedSuffix, localizedIn, err := localizeKey(locale, suffixKey)
164+
if err != nil {
165+
return "", err
166+
}
167+
if !ofKin {
168+
localizedSuffix = ""
169+
}
170+
localizedSuffix = strings.TrimSpace(localizedSuffix)
171+
172+
if isRtlScript && !isDefaultLocale(*localizedIn) {
173+
if len(localizedSuffix) > 0 {
174+
localizedSuffix = localizedSuffix + " "
175+
}
176+
return localizedSuffix + formattedAmount + symbol, nil
177+
}
178+
179+
if len(localizedSuffix) > 0 {
180+
localizedSuffix = " " + localizedSuffix
181+
}
182+
return symbol + formattedAmount + localizedSuffix, nil
183+
}
184+
185+
// LocalizeFiatWithVerb is like FormatFiat, but includes a verb for the interaction
186+
// with the currency amount
187+
func LocalizeFiatWithVerb(locale language.Tag, verb chatpb.ExchangeDataContent_Verb, code currency_lib.Code, amount float64, ofKin bool) (string, error) {
188+
localizedAmount, err := FormatFiat(locale, code, amount, ofKin)
189+
if err != nil {
190+
return "", err
191+
}
192+
193+
var key string
194+
var isAmountBeforeVerb bool
195+
switch verb {
196+
case chatpb.ExchangeDataContent_GAVE:
197+
key = VerbGave
198+
case chatpb.ExchangeDataContent_RECEIVED:
199+
key = VerbReceived
200+
case chatpb.ExchangeDataContent_WITHDREW:
201+
key = VerbWithdrew
202+
case chatpb.ExchangeDataContent_DEPOSITED:
203+
key = VerbDeposited
204+
case chatpb.ExchangeDataContent_SENT:
205+
key = VerbSpent
206+
case chatpb.ExchangeDataContent_RETURNED:
207+
key = VerbReturned
208+
isAmountBeforeVerb = true
209+
case chatpb.ExchangeDataContent_SPENT:
210+
key = VerbSpent
211+
case chatpb.ExchangeDataContent_PAID:
212+
key = VerbPaid
213+
case chatpb.ExchangeDataContent_PURCHASED:
214+
key = VerbPurchased
215+
default:
216+
return "", errors.Errorf("verb %s is not supported", verb)
217+
}
218+
219+
localizedVerbText, localizedIn, err := localizeKey(locale, key)
220+
if err != nil {
221+
return "", err
222+
}
223+
224+
if isRtlScript(locale) && !isDefaultLocale(*localizedIn) {
225+
isAmountBeforeVerb = !isAmountBeforeVerb
226+
}
227+
228+
if isAmountBeforeVerb {
229+
return localizedAmount + " " + localizedVerbText, nil
230+
}
231+
return localizedVerbText + " " + localizedAmount, nil
232+
}

0 commit comments

Comments
 (0)