@@ -25,174 +25,268 @@ var (
2525)
2626
2727type locale struct {
28+ // This mutex will be set if we have live-reload enabled (e.g. dev mode)
29+ reloadMu * sync.RWMutex
30+
2831 store * LocaleStore
2932 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
3135
3236 sourceFileName string
3337 sourceFileInfo os.FileInfo
3438 lastReloadCheckTime time.Time
3539}
3640
3741type 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
3944
4045 langNames []string
4146 langDescs []string
47+ localeMap map [string ]* locale
4248
43- localeMap map [ string ] * locale
44- textIdxMap map [string ]int
49+ // this needs to be locked when live-reloading
50+ trKeyToIdxMap map [string ]int
4551
4652 defaultLang string
4753}
4854
4955func 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 )}
5157 if ! isProd {
52- ls .reloadMu = & sync.Mutex {}
58+ store .reloadMu = & sync.RWMutex {}
5359 }
54- return ls
60+ return store
5561}
5662
5763// 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
5965// 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 {
6269 return ErrLocaleAlreadyExist
6370 }
6471
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+
6679 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
6988 }
7089
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
7494
75- return ls . reloadLocaleByIni ( langName , source )
95+ return nil
7696}
7797
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 ) {
79100 iniFile , err := ini .LoadSources (ini.LoadOptions {
80101 IgnoreInlineComment : true ,
81102 UnescapeValueCommentSymbols : true ,
82103 }, source )
83104 if err != nil {
84- return fmt .Errorf ("unable to load ini: %w" , err )
105+ return nil , fmt .Errorf ("unable to load ini: %w" , err )
85106 }
86107 iniFile .BlockMode = false
87108
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+
90116 for _ , section := range iniFile .Sections () {
91117 for _ , key := range section .Keys () {
118+
92119 var trKey string
93120 if section .Name () == "" || section .Name () == "DEFAULT" {
94121 trKey = key .Name ()
95122 } else {
96123 trKey = section .Name () + "." + key .Name ()
97124 }
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 ]
99129 if ! ok {
100- textIdx = len (ls . textIdxMap )
101- ls . textIdxMap [trKey ] = textIdx
130+ idx = len (store . trKeyToIdxMap )
131+ store . trKeyToIdxMap [trKey ] = idx
102132 }
103- lc . textMap [ textIdx ] = key .Value ()
133+ idxToMsgMap [ idx ] = key .Value ()
104134 }
105135 }
106136 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
108147}
109148
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 ]
112152 return ok
113153}
114154
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
117158}
118159
119160// 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
122163}
123164
124165// 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 ]
127168 if ! ok {
128- l , ok = ls .localeMap [ls .defaultLang ]
169+ l , ok = store .localeMap [store .defaultLang ]
129170 }
171+
130172 if ok {
131173 return l .Tr (trKey , trArgs ... )
132174 }
133175 return trKey
134176}
135177
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+
136216// Tr translates content to locale language. fall back to default language.
137217func (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 ()
152222 }
223+
153224 msg , _ := l .tryTr (trKey , trArgs ... )
154225 return msg
155226}
156227
157228func (l * locale ) tryTr (trKey string , trArgs ... interface {}) (msg string , found bool ) {
158229 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+
160234 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
163237 } 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
164241 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+ }
166253 }
167- } else if ! setting .IsProd {
168- log .Error ("missing i18n translation key: %q" , trKey )
169254 }
170255 }
171256
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 ())
188279 }
189280 } 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
191283 }
284+ } else {
285+ fmtArgs = append (fmtArgs , arg )
192286 }
193- return fmt .Sprintf (trMsg , fmtArgs ... ), found
194287 }
195- return trMsg , found
288+
289+ return fmt .Sprintf (trMsg , fmtArgs ... ), found
196290}
197291
198292// ResetDefaultLocales resets the current default locales
0 commit comments