3
3
// license that can be found in the LICENSE file.
4
4
5
5
// Package i18n is internationalization and localization support for aah
6
- // framework. Messages config format is `forge` config syntax (go- aah/config)
7
- // which is similar to HOCON syntax aka typesafe config .
6
+ // framework. Messages store config format is same as aah configuration.
7
+ // Refer to https://docs.aahframework.org/configuration.html .
8
8
//
9
9
// Message filename format is `message.<Language-ID>`. Language ID is combination
10
10
// of `Language + Region` or `Language` value. aah framework implements Language
11
11
// code is as per two-letter `ISO 639-1` standard and Region code is as per two-letter
12
12
// `ISO 3166-1` standard.
13
13
//
14
14
// Supported message file extension formats are (incasesensitive)
15
+ //
15
16
// 1) Language + Region => en-us | en-US
17
+ //
16
18
// 2) Language => en
17
19
//
18
20
// For Example:
@@ -34,73 +36,123 @@ import (
34
36
"path/filepath"
35
37
"regexp"
36
38
"strings"
39
+ "sync"
37
40
38
41
"aahframe.work/ahttp"
39
42
"aahframe.work/config"
40
- "aahframe.work/essentials"
41
43
"aahframe.work/log"
42
44
"aahframe.work/vfs"
43
45
)
44
46
47
+ // I18ner interface is used to implement i18n message store.
48
+ type I18ner interface {
49
+ Lookup (locale * ahttp.Locale , key string , args ... interface {}) string
50
+ DefaultLocale () string
51
+ Locales () []string
52
+ }
53
+
45
54
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
46
55
// Package methods
47
56
//______________________________________________________________________________
48
57
49
- // New method creates aah i18n message store
50
- func New () * I18n {
51
- return NewWithVFS (nil )
52
- }
53
-
54
- // NewWithVFS method creates aah i18n message store with given Virtual FileSystem.
55
- func NewWithVFS (fs * vfs.VFS ) * I18n {
56
- return & I18n {
57
- Store : make (map [string ]* config.Config ),
58
- fileExtRegex : `messages\.[a-z]{2}(\-[a-zA-Z]{2})?$` ,
59
- vfs : fs ,
58
+ // New method creates aah i18n message store with given options.
59
+ func New (l log.Loggerer , opts ... Option ) * I18n {
60
+ msgStore := & I18n {
61
+ RWMutex : sync.RWMutex {},
62
+ store : make (map [string ]* config.Config ),
63
+ fileExtRegex : regexp .MustCompile (`messages\.[a-z]{2}(\-[a-zA-Z]{2})?$` ),
64
+ defaultLocale : "en" ,
65
+ files : make ([]string , 0 ),
66
+ log : l ,
60
67
}
68
+ for _ , opt := range opts {
69
+ opt (msgStore )
70
+ }
71
+ return msgStore
61
72
}
62
73
63
74
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
64
- // I18n methods
75
+ // I18n options type and methods
65
76
//______________________________________________________________________________
66
77
67
- // I18n holds the message store and related information for internationalization
68
- // and localization.
69
- type I18n struct {
70
- Store map [string ]* config.Config
71
- DefaultLocale string
78
+ // Option type to provide configuration options
79
+ // to create i18n message store.
80
+ type Option func (* I18n )
72
81
73
- fileExtRegex string
74
- vfs * vfs.VFS
82
+ // DefaultLocale option func is to message store default locale.
83
+ func DefaultLocale (locale string ) Option {
84
+ return func (i * I18n ) {
85
+ i .Lock ()
86
+ defer i .Unlock ()
87
+ i .defaultLocale = locale
88
+ }
75
89
}
76
90
77
- // Load processes the given message file or directory and adds to the
78
- // message store
79
- func ( s * I18n ) Load ( paths ... string ) error {
80
- for _ , path := range paths {
81
- if ! vfs . IsExists ( s . vfs , path ) {
82
- log . Warnf ( "Path: %v not exists, let's move on" , path )
83
- continue
84
- }
91
+ // Dirs option func is to set aah VFS instance.
92
+ func VFS ( fs vfs. FileSystem ) Option {
93
+ return func ( i * I18n ) {
94
+ i . Lock ()
95
+ defer i . Unlock ()
96
+ i . fs = fs
97
+ }
98
+ }
85
99
86
- if s .isDir (path ) {
87
- _ = vfs .Walk (s .vfs , path , func (fpath string , f os.FileInfo , _ error ) error {
88
- if ! f .IsDir () {
89
- match , err := regexp .MatchString (s .fileExtRegex , f .Name ())
90
- if err == nil && match {
91
- s .processMsgFile (fpath )
100
+ // Dirs option func is to supply n no. of directory path.
101
+ func Dirs (dirs ... string ) Option {
102
+ return func (i * I18n ) {
103
+ i .Lock ()
104
+ defer i .Unlock ()
105
+ for _ , d := range dirs {
106
+ if ! vfs .IsDir (i .fs , d ) {
107
+ i .log .Warnf ("i18n: %v not exists or error, let's move on" , d )
108
+ continue
109
+ }
110
+ _ = vfs .Walk (i .fs , d , func (fpath string , fi os.FileInfo , _ error ) error {
111
+ if ! fi .IsDir () {
112
+ if i .fileExtRegex .MatchString (fi .Name ()) {
113
+ i .files = append (i .files , fpath )
92
114
}
93
115
}
94
116
return nil
95
117
})
96
- } else { // if it's a file
97
- s .processMsgFile (path )
98
118
}
99
119
}
120
+ }
100
121
101
- return nil
122
+ // Files option func is to supply n no. of file path.
123
+ func Files (files ... string ) Option {
124
+ return func (i * I18n ) {
125
+ i .Lock ()
126
+ defer i .Unlock ()
127
+ for _ , f := range files {
128
+ if ! vfs .IsExists (i .fs , f ) {
129
+ i .log .Warnf ("i18n: %v not exists, let's move on" , f )
130
+ continue
131
+ }
132
+ i .files = append (i .files , f )
133
+ }
134
+ }
102
135
}
103
136
137
+ //‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
138
+ // I18n methods
139
+ //______________________________________________________________________________
140
+
141
+ // I18n holds the message store and related information for internationalization
142
+ // and localization.
143
+ type I18n struct {
144
+ sync.RWMutex
145
+ store map [string ]* config.Config
146
+ defaultLocale string
147
+ files []string
148
+ fileExtRegex * regexp.Regexp
149
+ fs vfs.FileSystem
150
+ log log.Loggerer
151
+ }
152
+
153
+ // interface check
154
+ var _ I18ner = (* I18n )(nil )
155
+
104
156
// Lookup returns value by given key, locale and it supports formatting a message
105
157
// before its return. If given message key or store doesn't exists for given locale;
106
158
// Lookup method returns empty string.
@@ -109,82 +161,104 @@ func (s *I18n) Load(paths ...string) error {
109
161
// * language and region-id (e.g.: en-US)
110
162
// * language (e.g.: en)
111
163
func (s * I18n ) Lookup (locale * ahttp.Locale , key string , args ... interface {}) string {
164
+ s .RLock ()
165
+ defer s .RUnlock ()
112
166
// assign default locale if nil
113
167
if locale == nil {
114
- locale = ahttp .NewLocale (s .DefaultLocale )
168
+ locale = ahttp .NewLocale (s .defaultLocale )
115
169
}
116
170
117
171
// Lookup by language and region-id. For eg.: en-us
118
172
store := s .findStoreByLocale (locale .String ())
119
173
if store == nil {
120
- log .Tracef ("Locale (%v) doesn't exists in message store" , locale )
121
174
goto langStore
122
175
}
123
- log .Tracef ("Message is retrieved from locale: %v, key: %v" , locale , key )
124
176
if msg , found := retriveValue (store , key , args ... ); found {
177
+ s .log .Tracef ("i18n message is retrieved from locale: %v, key: %v" , locale , key )
125
178
return msg
126
179
}
127
180
128
181
langStore:
182
+ // Lookup by language. For eg.: en
129
183
store = s .findStoreByLocale (locale .Language )
130
184
if store == nil {
131
- log .Tracef ("Locale (%v) doesn't exists in message store" , locale .Language )
132
185
goto defaultStore
133
186
}
134
- log .Tracef ("Message is retrieved from locale: %v, key: %v" , locale .Language , key )
135
187
if msg , found := retriveValue (store , key , args ... ); found {
188
+ s .log .Tracef ("i18n message is retrieved from locale: %v, key: %v" , locale .Language , key )
136
189
return msg
137
190
}
138
191
139
192
defaultStore: // fallback to `i18n.default` config value.
140
- store = s .findStoreByLocale (s .DefaultLocale )
193
+ store = s .findStoreByLocale (s .defaultLocale )
141
194
if store == nil {
142
195
goto notExists
143
196
}
144
- log .Tracef ("Message is retrieved with 'i18n.default': %v, key: %v" , s .DefaultLocale , key )
145
197
if msg , found := retriveValue (store , key , args ... ); found {
198
+ s .log .Tracef ("i18n message is retrieved from locale: %v, key: %v" , s .defaultLocale , key )
146
199
return msg
147
200
}
148
201
149
202
notExists:
150
- log .Warnf ("i18n key not found: %s" , key )
151
- return ""
203
+ return key
204
+ }
205
+
206
+ // DefaultLocale method returns the i18n store's default locale.
207
+ func (s * I18n ) DefaultLocale () string {
208
+ s .RLock ()
209
+ defer s .RUnlock ()
210
+ return s .defaultLocale
152
211
}
153
212
154
213
// Locales returns all the loaded locales from message store
155
214
func (s * I18n ) Locales () []string {
215
+ s .RLock ()
216
+ defer s .RUnlock ()
156
217
var locales []string
157
- for l := range s .Store {
218
+ for l := range s .store {
158
219
locales = append (locales , l )
159
220
}
160
221
return locales
161
222
}
162
223
224
+ // Load method loads message files into message store.
225
+ // Returns error for any failures.
226
+ func (s * I18n ) Init () error {
227
+ for _ , f := range s .files {
228
+ if err := s .add2Store (f ); err != nil {
229
+ return err
230
+ }
231
+ }
232
+ return nil
233
+ }
234
+
163
235
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
164
236
// I18n Unexported methods
165
237
//______________________________________________________________________________
166
238
167
- func (s * I18n ) processMsgFile (file string ) {
239
+ func (s * I18n ) add2Store (file string ) error {
168
240
key := strings .ToLower (filepath .Ext (file )[1 :])
241
+ s .log .Tracef ("Adding into i18n message store [%v: %v]" , key , file )
169
242
msgFile , err := config .LoadFile (file )
170
243
if err != nil {
171
- log .Errorf ("Unable to load message file: %v, error: %v" , file , err )
244
+ return fmt .Errorf ("i18n: unable to process message file: %v, error: %v" , file , err )
172
245
}
173
246
174
247
// merge messages if key is already exists otherwise add it
175
- if ms , exists := s .Store [key ]; exists {
176
- log .Tracef ("Key[%v] is already exists, let's merge it" , key )
248
+ s .Lock ()
249
+ defer s .Unlock ()
250
+ if ms , exists := s .store [key ]; exists {
177
251
if err = ms .Merge (msgFile ); err != nil {
178
- log .Errorf ("Error while merging message file: %v" , file )
252
+ return fmt .Errorf ("i18n: error while adding into message store file: %v" , file )
179
253
}
180
254
} else {
181
- log .Tracef ("Adding to message store [%v: %v]" , key , file )
182
- s .Store [key ] = msgFile
255
+ s .store [key ] = msgFile
183
256
}
257
+ return nil
184
258
}
185
259
186
260
func (s * I18n ) findStoreByLocale (locale string ) * config.Config {
187
- if store , exists := s .Store [strings .ToLower (locale )]; exists {
261
+ if store , exists := s .store [strings .ToLower (locale )]; exists {
188
262
return store
189
263
}
190
264
return nil
@@ -201,17 +275,5 @@ func retriveValue(store *config.Config, key string, args ...interface{}) (string
201
275
}
202
276
return msg , found
203
277
}
204
- return "" , false
205
- }
206
-
207
- func (s * I18n ) isDir (name string ) bool {
208
- if s .vfs == nil {
209
- return ess .IsDir (name )
210
- }
211
-
212
- fi , err := s .vfs .Lstat (name )
213
- if err != nil {
214
- return false
215
- }
216
- return fi .IsDir ()
278
+ return key , false
217
279
}
0 commit comments