@@ -25,174 +25,268 @@ var (
25
25
)
26
26
27
27
type locale struct {
28
+ // This mutex will be set if we have live-reload enabled (e.g. dev mode)
29
+ reloadMu * sync.RWMutex
30
+
28
31
store * LocaleStore
29
32
langName string
30
- textMap map [int ]string // the map key (idx) is generated by store's textIdxMap
33
+
34
+ idxToMsgMap map [int ]string // the map idx is generated by store's trKeyToIdxMap
31
35
32
36
sourceFileName string
33
37
sourceFileInfo os.FileInfo
34
38
lastReloadCheckTime time.Time
35
39
}
36
40
37
41
type LocaleStore struct {
38
- reloadMu * sync.Mutex // for non-prod(dev), use a mutex for live-reload. for prod, no mutex, no live-reload.
42
+ // This mutex will be set if we have live-reload enabled (e.g. dev mode)
43
+ reloadMu * sync.RWMutex
39
44
40
45
langNames []string
41
46
langDescs []string
47
+ localeMap map [string ]* locale
42
48
43
- localeMap map [ string ] * locale
44
- textIdxMap map [string ]int
49
+ // this needs to be locked when live-reloading
50
+ trKeyToIdxMap map [string ]int
45
51
46
52
defaultLang string
47
53
}
48
54
49
55
func NewLocaleStore (isProd bool ) * LocaleStore {
50
- ls := & LocaleStore {localeMap : make (map [string ]* locale ), textIdxMap : make (map [string ]int )}
56
+ store := & LocaleStore {localeMap : make (map [string ]* locale ), trKeyToIdxMap : make (map [string ]int )}
51
57
if ! isProd {
52
- ls .reloadMu = & sync.Mutex {}
58
+ store .reloadMu = & sync.RWMutex {}
53
59
}
54
- return ls
60
+ return store
55
61
}
56
62
57
63
// AddLocaleByIni adds locale by ini into the store
58
- // if source is a string, then the file is loaded. in dev mode, the file can be live-reloaded
64
+ // if source is a string, then the file is loaded. In dev mode, this file will be checked for live-reloading
59
65
// if source is a []byte, then the content is used
60
- func (ls * LocaleStore ) AddLocaleByIni (langName , langDesc string , source interface {}) error {
61
- if _ , ok := ls .localeMap [langName ]; ok {
66
+ // Note: this is not concurrent safe
67
+ func (store * LocaleStore ) AddLocaleByIni (langName , langDesc string , source interface {}) error {
68
+ if _ , ok := store .localeMap [langName ]; ok {
62
69
return ErrLocaleAlreadyExist
63
70
}
64
71
65
- lc := & locale {store : ls , langName : langName }
72
+ l := & locale {store : store , langName : langName }
73
+ if store .reloadMu != nil {
74
+ l .reloadMu = & sync.RWMutex {}
75
+ l .reloadMu .Lock () // Arguably this is not necessary as AddLocaleByIni isn't concurrent safe - but for consistency we do this
76
+ defer l .reloadMu .Unlock ()
77
+ }
78
+
66
79
if fileName , ok := source .(string ); ok {
67
- lc .sourceFileName = fileName
68
- lc .sourceFileInfo , _ = os .Stat (fileName ) // live-reload only works for regular files. the error can be ignored
80
+ l .sourceFileName = fileName
81
+ l .sourceFileInfo , _ = os .Stat (fileName ) // live-reload only works for regular files. the error can be ignored
82
+ }
83
+
84
+ var err error
85
+ l .idxToMsgMap , err = store .readIniToIdxToMsgMap (source )
86
+ if err != nil {
87
+ return err
69
88
}
70
89
71
- ls .langNames = append (ls .langNames , langName )
72
- ls .langDescs = append (ls .langDescs , langDesc )
73
- ls .localeMap [lc .langName ] = lc
90
+ store .langNames = append (store .langNames , langName )
91
+ store .langDescs = append (store .langDescs , langDesc )
92
+
93
+ store .localeMap [l .langName ] = l
74
94
75
- return ls . reloadLocaleByIni ( langName , source )
95
+ return nil
76
96
}
77
97
78
- func (ls * LocaleStore ) reloadLocaleByIni (langName string , source interface {}) error {
98
+ // readIniToIdxToMsgMap will read a provided ini and creates an idxToMsgMap
99
+ func (store * LocaleStore ) readIniToIdxToMsgMap (source interface {}) (map [int ]string , error ) {
79
100
iniFile , err := ini .LoadSources (ini.LoadOptions {
80
101
IgnoreInlineComment : true ,
81
102
UnescapeValueCommentSymbols : true ,
82
103
}, source )
83
104
if err != nil {
84
- return fmt .Errorf ("unable to load ini: %w" , err )
105
+ return nil , fmt .Errorf ("unable to load ini: %w" , err )
85
106
}
86
107
iniFile .BlockMode = false
87
108
88
- lc := ls .localeMap [langName ]
89
- lc .textMap = make (map [int ]string )
109
+ idxToMsgMap := make (map [int ]string )
110
+
111
+ if store .reloadMu != nil {
112
+ store .reloadMu .Lock ()
113
+ defer store .reloadMu .Unlock ()
114
+ }
115
+
90
116
for _ , section := range iniFile .Sections () {
91
117
for _ , key := range section .Keys () {
118
+
92
119
var trKey string
93
120
if section .Name () == "" || section .Name () == "DEFAULT" {
94
121
trKey = key .Name ()
95
122
} else {
96
123
trKey = section .Name () + "." + key .Name ()
97
124
}
98
- textIdx , ok := ls .textIdxMap [trKey ]
125
+
126
+ // Instead of storing the key strings in multiple different maps we compute a idx which will act as numeric code for key
127
+ // This reduces the size of the locale idxToMsgMaps
128
+ idx , ok := store .trKeyToIdxMap [trKey ]
99
129
if ! ok {
100
- textIdx = len (ls . textIdxMap )
101
- ls . textIdxMap [trKey ] = textIdx
130
+ idx = len (store . trKeyToIdxMap )
131
+ store . trKeyToIdxMap [trKey ] = idx
102
132
}
103
- lc . textMap [ textIdx ] = key .Value ()
133
+ idxToMsgMap [ idx ] = key .Value ()
104
134
}
105
135
}
106
136
iniFile = nil
107
- return nil
137
+ return idxToMsgMap , nil
138
+ }
139
+
140
+ func (store * LocaleStore ) idxForTrKey (trKey string ) (int , bool ) {
141
+ if store .reloadMu != nil {
142
+ store .reloadMu .RLock ()
143
+ defer store .reloadMu .RUnlock ()
144
+ }
145
+ idx , ok := store .trKeyToIdxMap [trKey ]
146
+ return idx , ok
108
147
}
109
148
110
- func (ls * LocaleStore ) HasLang (langName string ) bool {
111
- _ , ok := ls .localeMap [langName ]
149
+ // HasLang reports if a language is available in the store
150
+ func (store * LocaleStore ) HasLang (langName string ) bool {
151
+ _ , ok := store .localeMap [langName ]
112
152
return ok
113
153
}
114
154
115
- func (ls * LocaleStore ) ListLangNameDesc () (names , desc []string ) {
116
- return ls .langNames , ls .langDescs
155
+ // ListLangNameDesc reports if a language available in the store
156
+ func (store * LocaleStore ) ListLangNameDesc () (names , desc []string ) {
157
+ return store .langNames , store .langDescs
117
158
}
118
159
119
160
// SetDefaultLang sets default language as a fallback
120
- func (ls * LocaleStore ) SetDefaultLang (lang string ) {
121
- ls .defaultLang = lang
161
+ func (store * LocaleStore ) SetDefaultLang (lang string ) {
162
+ store .defaultLang = lang
122
163
}
123
164
124
165
// Tr translates content to target language. fall back to default language.
125
- func (ls * LocaleStore ) Tr (lang , trKey string , trArgs ... interface {}) string {
126
- l , ok := ls .localeMap [lang ]
166
+ func (store * LocaleStore ) Tr (lang , trKey string , trArgs ... interface {}) string {
167
+ l , ok := store .localeMap [lang ]
127
168
if ! ok {
128
- l , ok = ls .localeMap [ls .defaultLang ]
169
+ l , ok = store .localeMap [store .defaultLang ]
129
170
}
171
+
130
172
if ok {
131
173
return l .Tr (trKey , trArgs ... )
132
174
}
133
175
return trKey
134
176
}
135
177
178
+ // reloadIfNeeded will check if the locale needs to be reloaded
179
+ // this function will assume that the l.reloadMu has been RLocked if it already exists
180
+ func (l * locale ) reloadIfNeeded () {
181
+ if l .reloadMu == nil {
182
+ return
183
+ }
184
+
185
+ now := time .Now ()
186
+ if now .Sub (l .lastReloadCheckTime ) < time .Second || l .sourceFileInfo == nil || l .sourceFileName == "" {
187
+ return
188
+ }
189
+
190
+ l .reloadMu .RUnlock ()
191
+ l .reloadMu .Lock () // (NOTE: a pre-emption can occur between these two locks so we need to recheck)
192
+ defer l .reloadMu .RLock ()
193
+ defer l .reloadMu .Unlock ()
194
+
195
+ if now .Sub (l .lastReloadCheckTime ) < time .Second || l .sourceFileInfo == nil || l .sourceFileName == "" {
196
+ return
197
+ }
198
+
199
+ l .lastReloadCheckTime = now
200
+ sourceFileInfo , err := os .Stat (l .sourceFileName )
201
+ if err != nil || sourceFileInfo .ModTime ().Equal (l .sourceFileInfo .ModTime ()) {
202
+ return
203
+ }
204
+
205
+ idxToMsgMap , err := l .store .readIniToIdxToMsgMap (l .sourceFileName )
206
+ if err == nil {
207
+ l .idxToMsgMap = idxToMsgMap
208
+ } else {
209
+ log .Error ("Unable to live-reload the locale file %q, err: %v" , l .sourceFileName , err )
210
+ }
211
+
212
+ // We will set the sourceFileInfo to this file to prevent repeated attempts to re-load this broken file
213
+ l .sourceFileInfo = sourceFileInfo
214
+ }
215
+
136
216
// Tr translates content to locale language. fall back to default language.
137
217
func (l * locale ) Tr (trKey string , trArgs ... interface {}) string {
138
- if l .store .reloadMu != nil {
139
- l .store .reloadMu .Lock ()
140
- defer l .store .reloadMu .Unlock ()
141
- now := time .Now ()
142
- if now .Sub (l .lastReloadCheckTime ) >= time .Second && l .sourceFileInfo != nil && l .sourceFileName != "" {
143
- l .lastReloadCheckTime = now
144
- if sourceFileInfo , err := os .Stat (l .sourceFileName ); err == nil && ! sourceFileInfo .ModTime ().Equal (l .sourceFileInfo .ModTime ()) {
145
- if err = l .store .reloadLocaleByIni (l .langName , l .sourceFileName ); err == nil {
146
- l .sourceFileInfo = sourceFileInfo
147
- } else {
148
- log .Error ("unable to live-reload the locale file %q, err: %v" , l .sourceFileName , err )
149
- }
150
- }
151
- }
218
+ if l .reloadMu != nil {
219
+ l .reloadMu .RLock ()
220
+ defer l .reloadMu .RUnlock ()
221
+ l .reloadIfNeeded ()
152
222
}
223
+
153
224
msg , _ := l .tryTr (trKey , trArgs ... )
154
225
return msg
155
226
}
156
227
157
228
func (l * locale ) tryTr (trKey string , trArgs ... interface {}) (msg string , found bool ) {
158
229
trMsg := trKey
159
- textIdx , ok := l .store .textIdxMap [trKey ]
230
+
231
+ // convert the provided trKey to a common idx from the store
232
+ idx , ok := l .store .idxForTrKey (trKey )
233
+
160
234
if ok {
161
- if msg , found = l .textMap [ textIdx ]; found {
162
- trMsg = msg // use current translation
235
+ if msg , found = l .idxToMsgMap [ idx ]; found {
236
+ trMsg = msg // use the translation that we have found
163
237
} else if l .langName != l .store .defaultLang {
238
+ // No translation available in our current language... fallback to the default language
239
+
240
+ // Attempt to get the default language from the locale store
164
241
if def , ok := l .store .localeMap [l .store .defaultLang ]; ok {
165
- return def .tryTr (trKey , trArgs ... )
242
+
243
+ if def .reloadMu != nil {
244
+ def .reloadMu .RLock ()
245
+ def .reloadIfNeeded ()
246
+ }
247
+ if msg , found = def .idxToMsgMap [idx ]; found {
248
+ trMsg = msg // use the translation that we have found
249
+ }
250
+ if def .reloadMu != nil {
251
+ def .reloadMu .RUnlock ()
252
+ }
166
253
}
167
- } else if ! setting .IsProd {
168
- log .Error ("missing i18n translation key: %q" , trKey )
169
254
}
170
255
}
171
256
172
- if len (trArgs ) > 0 {
173
- fmtArgs := make ([]interface {}, 0 , len (trArgs ))
174
- for _ , arg := range trArgs {
175
- val := reflect .ValueOf (arg )
176
- if val .Kind () == reflect .Slice {
177
- // before, it can accept Tr(lang, key, a, [b, c], d, [e, f]) as Sprintf(msg, a, b, c, d, e, f), it's an unstable behavior
178
- // now, we restrict the strange behavior and only support:
179
- // 1. Tr(lang, key, [slice-items]) as Sprintf(msg, items...)
180
- // 2. Tr(lang, key, args...) as Sprintf(msg, args...)
181
- if len (trArgs ) == 1 {
182
- for i := 0 ; i < val .Len (); i ++ {
183
- fmtArgs = append (fmtArgs , val .Index (i ).Interface ())
184
- }
185
- } else {
186
- log .Error ("the args for i18n shouldn't contain uncertain slices, key=%q, args=%v" , trKey , trArgs )
187
- break
257
+ if ! found && ! setting .IsProd {
258
+ log .Error ("missing i18n translation key: %q" , trKey )
259
+ }
260
+
261
+ if len (trArgs ) == 0 {
262
+ return trMsg , found
263
+ }
264
+
265
+ fmtArgs := make ([]interface {}, 0 , len (trArgs ))
266
+ for _ , arg := range trArgs {
267
+ val := reflect .ValueOf (arg )
268
+ if val .Kind () == reflect .Slice {
269
+ // Previously, we would accept Tr(lang, key, a, [b, c], d, [e, f]) as Sprintf(msg, a, b, c, d, e, f)
270
+ // but this is an unstable behavior.
271
+ //
272
+ // So we restrict the accepted arguments to either:
273
+ //
274
+ // 1. Tr(lang, key, [slice-items]) as Sprintf(msg, items...)
275
+ // 2. Tr(lang, key, args...) as Sprintf(msg, args...)
276
+ if len (trArgs ) == 1 {
277
+ for i := 0 ; i < val .Len (); i ++ {
278
+ fmtArgs = append (fmtArgs , val .Index (i ).Interface ())
188
279
}
189
280
} else {
190
- fmtArgs = append (fmtArgs , arg )
281
+ log .Error ("the args for i18n shouldn't contain uncertain slices, key=%q, args=%v" , trKey , trArgs )
282
+ break
191
283
}
284
+ } else {
285
+ fmtArgs = append (fmtArgs , arg )
192
286
}
193
- return fmt .Sprintf (trMsg , fmtArgs ... ), found
194
287
}
195
- return trMsg , found
288
+
289
+ return fmt .Sprintf (trMsg , fmtArgs ... ), found
196
290
}
197
291
198
292
// ResetDefaultLocales resets the current default locales
0 commit comments