-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathi18n.go
217 lines (191 loc) · 6.78 KB
/
i18n.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
// Copyright (c) Jeevanandam M. (https://github.com/jeevatkm)
// aahframework.org/i18n source code and usage is governed by a MIT style
// license that can be found in the LICENSE file.
// Package i18n is internationalization and localization support for aah
// framework. Messages config format is `forge` config syntax (go-aah/config)
// which is similar to HOCON syntax aka typesafe config.
//
// Message filename format is `message.<Language-ID>`. Language ID is combination
// of `Language + Region` or `Language` value. aah framework implements Language
// code is as per two-letter `ISO 639-1` standard and Region code is as per two-letter
// `ISO 3166-1` standard.
//
// Supported message file extension formats are (incasesensitive)
// 1) Language + Region => en-us | en-US
// 2) Language => en
//
// For Example:
// message.en-US or message.en-us
// message.en-GB or message.en-gb
// message.en-CA or message.en-ca
// message.en
// message.es
// message.zh
// message.nl
// etc.
//
// Note: Sub directories is supported, so you can organize message files.
package i18n
import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"aahframework.org/ahttp.v0"
"aahframework.org/config.v0"
"aahframework.org/essentials.v0"
"aahframework.org/log.v0"
"aahframework.org/vfs.v0"
)
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Package methods
//______________________________________________________________________________
// New method creates aah i18n message store
func New() *I18n {
return NewWithVFS(nil)
}
// NewWithVFS method creates aah i18n message store with given Virtual FileSystem.
func NewWithVFS(fs *vfs.VFS) *I18n {
return &I18n{
Store: make(map[string]*config.Config),
fileExtRegex: `messages\.[a-z]{2}(\-[a-zA-Z]{2})?$`,
vfs: fs,
}
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// I18n methods
//______________________________________________________________________________
// I18n holds the message store and related information for internationalization
// and localization.
type I18n struct {
Store map[string]*config.Config
DefaultLocale string
fileExtRegex string
vfs *vfs.VFS
}
// Load processes the given message file or directory and adds to the
// message store
func (s *I18n) Load(paths ...string) error {
for _, path := range paths {
if !vfs.IsExists(s.vfs, path) {
log.Warnf("Path: %v not exists, let's move on", path)
continue
}
if s.isDir(path) {
_ = vfs.Walk(s.vfs, path, func(fpath string, f os.FileInfo, _ error) error {
if !f.IsDir() {
match, err := regexp.MatchString(s.fileExtRegex, f.Name())
if err == nil && match {
s.processMsgFile(fpath)
}
}
return nil
})
} else { // if it's a file
s.processMsgFile(path)
}
}
return nil
}
// Lookup returns value by given key, locale and it supports formatting a message
// before its return. If given message key or store doesn't exists for given locale;
// Lookup method returns empty string.
// Lookup(locale, "i.love.aah.framework", "yes")
// The sequence and fallback order of message fetch from store is -
// * language and region-id (e.g.: en-US)
// * language (e.g.: en)
func (s *I18n) Lookup(locale *ahttp.Locale, key string, args ...interface{}) string {
// assign default locale if nil
if locale == nil {
locale = ahttp.NewLocale(s.DefaultLocale)
}
// Lookup by language and region-id. For eg.: en-us
store := s.findStoreByLocale(locale.String())
if store == nil {
log.Tracef("Locale (%v) doesn't exists in message store", locale)
goto langStore
}
log.Tracef("Message is retrieved from locale: %v, key: %v", locale, key)
if msg, found := retriveValue(store, key, args...); found {
return msg
}
langStore:
store = s.findStoreByLocale(locale.Language)
if store == nil {
log.Tracef("Locale (%v) doesn't exists in message store", locale.Language)
goto defaultStore
}
log.Tracef("Message is retrieved from locale: %v, key: %v", locale.Language, key)
if msg, found := retriveValue(store, key, args...); found {
return msg
}
defaultStore: // fallback to `i18n.default` config value.
store = s.findStoreByLocale(s.DefaultLocale)
if store == nil {
goto notExists
}
log.Tracef("Message is retrieved with 'i18n.default': %v, key: %v", s.DefaultLocale, key)
if msg, found := retriveValue(store, key, args...); found {
return msg
}
notExists:
log.Warnf("i18n key not found: %s", key)
return ""
}
// Locales returns all the loaded locales from message store
func (s *I18n) Locales() []string {
var locales []string
for l := range s.Store {
locales = append(locales, l)
}
return locales
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// I18n Unexported methods
//______________________________________________________________________________
func (s *I18n) processMsgFile(file string) {
key := strings.ToLower(filepath.Ext(file)[1:])
msgFile, err := config.VFSLoadFile(s.vfs, file)
if err != nil {
log.Errorf("Unable to load message file: %v, error: %v", file, err)
}
// merge messages if key is already exists otherwise add it
if ms, exists := s.Store[key]; exists {
log.Tracef("Key[%v] is already exists, let's merge it", key)
if err = ms.Merge(msgFile); err != nil {
log.Errorf("Error while merging message file: %v", file)
}
} else {
log.Tracef("Adding to message store [%v: %v]", key, file)
s.Store[key] = msgFile
}
}
func (s *I18n) findStoreByLocale(locale string) *config.Config {
if store, exists := s.Store[strings.ToLower(locale)]; exists {
return store
}
return nil
}
//‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾‾
// Package Unexported methods
//______________________________________________________________________________
func retriveValue(store *config.Config, key string, args ...interface{}) (string, bool) {
if msg, found := store.String(key); found {
if len(args) > 0 {
return fmt.Sprintf(msg, args...), found
}
return msg, found
}
return "", false
}
func (s *I18n) isDir(name string) bool {
if s.vfs == nil {
return ess.IsDir(name)
}
fi, err := s.vfs.Lstat(name)
if err != nil {
return false
}
return fi.IsDir()
}