Skip to content

Commit b1a2826

Browse files
authored
#237 return i18n key instead of empty string and #239 implemented i18n interface (#241)
1 parent 20063c5 commit b1a2826

File tree

4 files changed

+221
-130
lines changed

4 files changed

+221
-130
lines changed

aah.go

+19-9
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ type Application struct {
116116
router *router.Router
117117
eventStore *EventStore
118118
bindMgr *bindManager
119-
i18n *i18n.I18n
119+
i18n i18n.I18ner
120120
securityMgr *security.Manager
121121
viewMgr *viewManager
122122
staticMgr *staticManager
@@ -739,15 +739,23 @@ func (a *Application) initLog() error {
739739

740740
const keyLocale = "Locale"
741741

742+
// RegisterI18n method is used to register the i18n message store
743+
// into aah appplication the implements interface `i18n.I18ner`.
744+
func (a *Application) RegisterI18n(ms i18n.I18ner) {
745+
a.Lock()
746+
defer a.Unlock()
747+
a.i18n = ms
748+
}
749+
742750
// I18n method returns aah application I18n store instance.
743-
func (a *Application) I18n() *i18n.I18n {
751+
func (a *Application) I18n() i18n.I18ner {
744752
return a.i18n
745753
}
746754

747755
// DefaultI18nLang method returns application i18n default language if
748756
// configured otherwise framework defaults to "en".
749757
func (a *Application) DefaultI18nLang() string {
750-
return a.Config().StringDefault("i18n.default", "en")
758+
return a.I18n().DefaultLocale()
751759
}
752760

753761
func (a *Application) initI18n() error {
@@ -756,14 +764,16 @@ func (a *Application) initI18n() error {
756764
// i18n directory not exists, scenario could be only API application
757765
return nil
758766
}
759-
760-
ai18n := i18n.NewWithVFS(a.VFS())
761-
ai18n.DefaultLocale = a.DefaultI18nLang()
762-
if err := ai18n.Load(i18nPath); err != nil {
767+
ai18n := i18n.New(
768+
a.Log(),
769+
i18n.DefaultLocale(a.Config().StringDefault("i18n.default", "en")),
770+
i18n.VFS(a.VFS()),
771+
i18n.Dirs(i18nPath),
772+
)
773+
if err := ai18n.Init(); err != nil {
763774
return err
764775
}
765-
766-
a.i18n = ai18n
776+
a.RegisterI18n(ai18n)
767777
return nil
768778
}
769779

i18n/i18n.go

+132-70
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,18 @@
33
// license that can be found in the LICENSE file.
44

55
// 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.
88
//
99
// Message filename format is `message.<Language-ID>`. Language ID is combination
1010
// of `Language + Region` or `Language` value. aah framework implements Language
1111
// code is as per two-letter `ISO 639-1` standard and Region code is as per two-letter
1212
// `ISO 3166-1` standard.
1313
//
1414
// Supported message file extension formats are (incasesensitive)
15+
//
1516
// 1) Language + Region => en-us | en-US
17+
//
1618
// 2) Language => en
1719
//
1820
// For Example:
@@ -34,73 +36,123 @@ import (
3436
"path/filepath"
3537
"regexp"
3638
"strings"
39+
"sync"
3740

3841
"aahframe.work/ahttp"
3942
"aahframe.work/config"
40-
"aahframe.work/essentials"
4143
"aahframe.work/log"
4244
"aahframe.work/vfs"
4345
)
4446

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+
4554
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
4655
// Package methods
4756
//______________________________________________________________________________
4857

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,
6067
}
68+
for _, opt := range opts {
69+
opt(msgStore)
70+
}
71+
return msgStore
6172
}
6273

6374
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
64-
// I18n methods
75+
// I18n options type and methods
6576
//______________________________________________________________________________
6677

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)
7281

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+
}
7589
}
7690

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+
}
8599

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)
92114
}
93115
}
94116
return nil
95117
})
96-
} else { // if it's a file
97-
s.processMsgFile(path)
98118
}
99119
}
120+
}
100121

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+
}
102135
}
103136

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+
104156
// Lookup returns value by given key, locale and it supports formatting a message
105157
// before its return. If given message key or store doesn't exists for given locale;
106158
// Lookup method returns empty string.
@@ -109,82 +161,104 @@ func (s *I18n) Load(paths ...string) error {
109161
// * language and region-id (e.g.: en-US)
110162
// * language (e.g.: en)
111163
func (s *I18n) Lookup(locale *ahttp.Locale, key string, args ...interface{}) string {
164+
s.RLock()
165+
defer s.RUnlock()
112166
// assign default locale if nil
113167
if locale == nil {
114-
locale = ahttp.NewLocale(s.DefaultLocale)
168+
locale = ahttp.NewLocale(s.defaultLocale)
115169
}
116170

117171
// Lookup by language and region-id. For eg.: en-us
118172
store := s.findStoreByLocale(locale.String())
119173
if store == nil {
120-
log.Tracef("Locale (%v) doesn't exists in message store", locale)
121174
goto langStore
122175
}
123-
log.Tracef("Message is retrieved from locale: %v, key: %v", locale, key)
124176
if msg, found := retriveValue(store, key, args...); found {
177+
s.log.Tracef("i18n message is retrieved from locale: %v, key: %v", locale, key)
125178
return msg
126179
}
127180

128181
langStore:
182+
// Lookup by language. For eg.: en
129183
store = s.findStoreByLocale(locale.Language)
130184
if store == nil {
131-
log.Tracef("Locale (%v) doesn't exists in message store", locale.Language)
132185
goto defaultStore
133186
}
134-
log.Tracef("Message is retrieved from locale: %v, key: %v", locale.Language, key)
135187
if msg, found := retriveValue(store, key, args...); found {
188+
s.log.Tracef("i18n message is retrieved from locale: %v, key: %v", locale.Language, key)
136189
return msg
137190
}
138191

139192
defaultStore: // fallback to `i18n.default` config value.
140-
store = s.findStoreByLocale(s.DefaultLocale)
193+
store = s.findStoreByLocale(s.defaultLocale)
141194
if store == nil {
142195
goto notExists
143196
}
144-
log.Tracef("Message is retrieved with 'i18n.default': %v, key: %v", s.DefaultLocale, key)
145197
if msg, found := retriveValue(store, key, args...); found {
198+
s.log.Tracef("i18n message is retrieved from locale: %v, key: %v", s.defaultLocale, key)
146199
return msg
147200
}
148201

149202
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
152211
}
153212

154213
// Locales returns all the loaded locales from message store
155214
func (s *I18n) Locales() []string {
215+
s.RLock()
216+
defer s.RUnlock()
156217
var locales []string
157-
for l := range s.Store {
218+
for l := range s.store {
158219
locales = append(locales, l)
159220
}
160221
return locales
161222
}
162223

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+
163235
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
164236
// I18n Unexported methods
165237
//______________________________________________________________________________
166238

167-
func (s *I18n) processMsgFile(file string) {
239+
func (s *I18n) add2Store(file string) error {
168240
key := strings.ToLower(filepath.Ext(file)[1:])
241+
s.log.Tracef("Adding into i18n message store [%v: %v]", key, file)
169242
msgFile, err := config.LoadFile(file)
170243
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)
172245
}
173246

174247
// 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 {
177251
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)
179253
}
180254
} else {
181-
log.Tracef("Adding to message store [%v: %v]", key, file)
182-
s.Store[key] = msgFile
255+
s.store[key] = msgFile
183256
}
257+
return nil
184258
}
185259

186260
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 {
188262
return store
189263
}
190264
return nil
@@ -201,17 +275,5 @@ func retriveValue(store *config.Config, key string, args ...interface{}) (string
201275
}
202276
return msg, found
203277
}
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
217279
}

0 commit comments

Comments
 (0)