/
lang.go
292 lines (259 loc) 路 7.48 KB
/
lang.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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
package lang
import (
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strings"
"sync"
"github.com/System-Glitch/goyave/v3/config"
"github.com/System-Glitch/goyave/v3/helper"
"github.com/System-Glitch/goyave/v3/helper/filesystem"
)
type validationLines struct {
// Default messages for rules
rules map[string]string
// Attribute-specific rules messages
fields map[string]attribute
}
type attribute struct {
// The value with which the :field placeholder will be replaced
Name string `json:"name"`
// A custom message for when a rule doesn't pass with this attribute
Rules map[string]string `json:"rules"`
}
// language represents a full language
type language struct {
lines map[string]string
validation validationLines
}
var languages map[string]language
var mutex = &sync.RWMutex{}
func (l *language) clone() language {
cpy := language{
lines: make(map[string]string, len(l.lines)),
validation: validationLines{
rules: make(map[string]string, len(l.validation.rules)),
fields: make(map[string]attribute, len(l.validation.fields)),
},
}
mergeMap(cpy.lines, l.lines)
mergeMap(cpy.validation.rules, l.validation.rules)
for key, attr := range l.validation.fields {
attrCpy := attribute{
Name: attr.Name,
Rules: make(map[string]string, len(attr.Rules)),
}
mergeMap(attrCpy.Rules, attrCpy.Rules)
cpy.validation.fields[key] = attrCpy
}
return cpy
}
// LoadDefault load the fallback language ("en-US").
// This function is intended for internal use only.
func LoadDefault() {
mutex.Lock()
defer mutex.Unlock()
languages = make(map[string]language, 1)
languages["en-US"] = enUS.clone()
}
// LoadAllAvailableLanguages loads every language directory
// in the "resources/lang" directory if it exists.
func LoadAllAvailableLanguages() {
mutex.Lock()
defer mutex.Unlock()
sep := string(os.PathSeparator)
workingDir, err := os.Getwd()
if err != nil {
panic(err)
}
langDirectory := workingDir + sep + "resources" + sep + "lang" + sep
if filesystem.IsDirectory(langDirectory) {
files, err := ioutil.ReadDir(langDirectory)
if err != nil {
panic(err)
}
for _, f := range files {
if f.IsDir() {
load(f.Name(), langDirectory+sep+f.Name())
}
}
}
}
// Load a language directory.
//
// Directory structure of a language directory:
// en-UK
// 鈹溾攢 locale.json (contains the normal language lines)
// 鈹溾攢 rules.json (contains the validation messages)
// 鈹斺攢 attributes.json (contains the attribute-specific validation messages)
//
// Each file is optional.
func Load(language, path string) {
mutex.Lock()
defer mutex.Unlock()
if filesystem.IsDirectory(path) {
load(language, path)
} else {
panic(fmt.Sprintf("Failed loading language \"%s\", directory \"%s\" doesn't exist", language, path))
}
}
func load(lang string, path string) {
langStruct := language{}
sep := string(os.PathSeparator)
readLangFile(path+sep+"locale.json", &langStruct.lines)
readLangFile(path+sep+"rules.json", &langStruct.validation.rules)
readLangFile(path+sep+"fields.json", &langStruct.validation.fields)
if existingLang, exists := languages[lang]; exists {
mergeLang(existingLang, langStruct)
} else {
languages[lang] = langStruct
}
}
func readLangFile(path string, dest interface{}) {
if filesystem.FileExists(path) {
langFile, _ := os.Open(path)
defer langFile.Close()
errParse := json.NewDecoder(langFile).Decode(&dest)
if errParse != nil {
panic(errParse)
}
}
}
func mergeLang(dst language, src language) {
mergeMap(dst.lines, src.lines)
mergeMap(dst.validation.rules, src.validation.rules)
for key, value := range src.validation.fields {
if attr, exists := dst.validation.fields[key]; !exists {
dst.validation.fields[key] = value
} else {
attr.Name = value.Name
if attr.Rules == nil {
attr.Rules = make(map[string]string)
}
mergeMap(attr.Rules, value.Rules)
dst.validation.fields[key] = attr
}
}
}
func mergeMap(dst map[string]string, src map[string]string) {
for key, value := range src {
dst[key] = value
}
}
// Get a language line.
//
// For validation rules and attributes messages, use a dot-separated path:
// - "validation.rules.<rule_name>"
// - "validation.fields.<field_name>"
// - "validation.fields.<field_name>.<rule_name>"
// For normal lines, just use the name of the line. Note that if you have
// a line called "validation", it won't conflict with the dot-separated paths.
//
// If not found, returns the exact "line" attribute.
//
// The placeholders parameter is a variadic associative slice of placeholders and their
// replacement. In the following example, the placeholder ":username" will be replaced
// with the Name field in the user struct.
//
// lang.Get("en-US", "greetings", ":username", user.Name)
func Get(lang string, line string, placeholders ...string) string {
if !IsAvailable(lang) {
return line
}
mutex.RLock()
defer mutex.RUnlock()
if strings.Count(line, ".") > 0 {
path := strings.Split(line, ".")
if path[0] == "validation" {
switch path[1] {
case "rules":
if len(path) < 3 {
return line
}
return convertEmptyLine(line, languages[lang].validation.rules[strings.Join(path[2:], ".")], placeholders)
case "fields":
len := len(path)
if len < 3 {
return line
}
attr := languages[lang].validation.fields[path[2]]
if len == 4 {
if attr.Rules == nil {
return line
}
return convertEmptyLine(line, attr.Rules[path[3]], placeholders)
} else if len == 3 {
return convertEmptyLine(line, attr.Name, placeholders)
} else {
return line
}
default:
return line
}
}
}
return convertEmptyLine(line, languages[lang].lines[line], placeholders)
}
func processPlaceholders(message string, values []string) string {
length := len(values) - 1
for i := 0; i < length; i += 2 {
message = strings.ReplaceAll(message, values[i], values[i+1])
}
return message
}
func convertEmptyLine(entry, line string, placeholders []string) string {
if line == "" {
return entry
}
return processPlaceholders(line, placeholders)
}
// IsAvailable returns true if the language is available.
func IsAvailable(lang string) bool {
mutex.RLock()
defer mutex.RUnlock()
_, exists := languages[lang]
return exists
}
// GetAvailableLanguages returns a slice of all loaded languages.
// This can be used to generate different routes for all languages
// supported by your applications.
//
// /en/products
// /fr/produits
// ...
func GetAvailableLanguages() []string {
mutex.RLock()
defer mutex.RUnlock()
langs := []string{}
for lang := range languages {
langs = append(langs, lang)
}
return langs
}
// DetectLanguage detects the language to use based on the given lang string.
// The given lang string can use the HTTP "Accept-Language" header format.
//
// If "*" is provided, the default language will be used.
// If multiple languages are given, the first available language will be used,
// and if none are available, the default language will be used.
// If no variant is given (for example "en"), the first available variant will be used.
// For example, if "en-US" and "en-UK" are available and the request accepts "en",
// "en-US" will be used.
func DetectLanguage(lang string) string {
values := helper.ParseMultiValuesHeader(lang)
for _, l := range values {
if l.Value == "*" { // Accept anything, so return default language
break
}
if IsAvailable(l.Value) {
return l.Value
}
for key := range languages {
if strings.HasPrefix(key, l.Value) {
return key
}
}
}
return config.GetString("app.defaultLanguage")
}