github.com/System-Glitch/goyave/v2@v2.10.3-0.20200819142921-51011e75d504/lang/lang.go (about) 1 package lang 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io/ioutil" 7 "os" 8 "strings" 9 "sync" 10 11 "github.com/System-Glitch/goyave/v2/config" 12 "github.com/System-Glitch/goyave/v2/helper" 13 14 "github.com/System-Glitch/goyave/v2/helper/filesystem" 15 ) 16 17 type validationLines struct { 18 // Default messages for rules 19 rules map[string]string 20 21 // Attribute-specific rules messages 22 fields map[string]attribute 23 } 24 25 type attribute struct { 26 // The value with which the :field placeholder will be replaced 27 Name string `json:"name"` 28 29 // A custom message for when a rule doesn't pass with this attribute 30 Rules map[string]string `json:"rules"` 31 } 32 33 // language represents a full language 34 type language struct { 35 lines map[string]string 36 validation validationLines 37 } 38 39 var languages map[string]language 40 var mutex = &sync.RWMutex{} 41 42 func (l *language) clone() language { 43 cpy := language{ 44 lines: make(map[string]string, len(l.lines)), 45 validation: validationLines{ 46 rules: make(map[string]string, len(l.validation.rules)), 47 fields: make(map[string]attribute, len(l.validation.fields)), 48 }, 49 } 50 51 mergeMap(cpy.lines, l.lines) 52 mergeMap(cpy.validation.rules, l.validation.rules) 53 54 for key, attr := range l.validation.fields { 55 attrCpy := attribute{ 56 Name: attr.Name, 57 Rules: make(map[string]string, len(attr.Rules)), 58 } 59 mergeMap(attrCpy.Rules, attrCpy.Rules) 60 cpy.validation.fields[key] = attrCpy 61 } 62 63 return cpy 64 } 65 66 // LoadDefault load the fallback language ("en-US"). 67 // This function is intended for internal use only. 68 func LoadDefault() { 69 mutex.Lock() 70 defer mutex.Unlock() 71 languages = make(map[string]language, 1) 72 languages["en-US"] = enUS.clone() 73 } 74 75 // LoadAllAvailableLanguages loads every language directory 76 // in the "resources/lang" directory if it exists. 77 func LoadAllAvailableLanguages() { 78 mutex.Lock() 79 defer mutex.Unlock() 80 sep := string(os.PathSeparator) 81 workingDir, err := os.Getwd() 82 if err != nil { 83 panic(err) 84 } 85 langDirectory := workingDir + sep + "resources" + sep + "lang" + sep 86 if filesystem.IsDirectory(langDirectory) { 87 files, err := ioutil.ReadDir(langDirectory) 88 if err != nil { 89 panic(err) 90 } 91 92 for _, f := range files { 93 if f.IsDir() { 94 load(f.Name(), langDirectory+sep+f.Name()) 95 } 96 } 97 } 98 } 99 100 // Load a language directory. 101 // 102 // Directory structure of a language directory: 103 // en-UK 104 // ├─ locale.json (contains the normal language lines) 105 // ├─ rules.json (contains the validation messages) 106 // └─ attributes.json (contains the attribute-specific validation messages) 107 // 108 // Each file is optional. 109 func Load(language, path string) { 110 mutex.Lock() 111 defer mutex.Unlock() 112 if filesystem.IsDirectory(path) { 113 load(language, path) 114 } else { 115 panic(fmt.Sprintf("Failed loading language \"%s\", directory \"%s\" doesn't exist", language, path)) 116 } 117 } 118 119 func load(lang string, path string) { 120 langStruct := language{} 121 sep := string(os.PathSeparator) 122 readLangFile(path+sep+"locale.json", &langStruct.lines) 123 readLangFile(path+sep+"rules.json", &langStruct.validation.rules) 124 readLangFile(path+sep+"fields.json", &langStruct.validation.fields) 125 126 if existingLang, exists := languages[lang]; exists { 127 mergeLang(existingLang, langStruct) 128 } else { 129 languages[lang] = langStruct 130 } 131 } 132 133 func readLangFile(path string, dest interface{}) { 134 if filesystem.FileExists(path) { 135 langFile, _ := os.Open(path) 136 defer langFile.Close() 137 138 errParse := json.NewDecoder(langFile).Decode(&dest) 139 if errParse != nil { 140 panic(errParse) 141 } 142 } 143 } 144 145 func mergeLang(dst language, src language) { 146 mergeMap(dst.lines, src.lines) 147 mergeMap(dst.validation.rules, src.validation.rules) 148 149 for key, value := range src.validation.fields { 150 if attr, exists := dst.validation.fields[key]; !exists { 151 dst.validation.fields[key] = value 152 } else { 153 attr.Name = value.Name 154 if attr.Rules == nil { 155 attr.Rules = make(map[string]string) 156 } 157 mergeMap(attr.Rules, value.Rules) 158 dst.validation.fields[key] = attr 159 } 160 } 161 } 162 163 func mergeMap(dst map[string]string, src map[string]string) { 164 for key, value := range src { 165 dst[key] = value 166 } 167 } 168 169 // Get a language line. 170 // 171 // For validation rules and attributes messages, use a dot-separated path: 172 // - "validation.rules.<rule_name>" 173 // - "validation.fields.<field_name>" 174 // - "validation.fields.<field_name>.<rule_name>" 175 // For normal lines, just use the name of the line. Note that if you have 176 // a line called "validation", it won't conflict with the dot-separated paths. 177 // 178 // If not found, returns the exact "line" attribute. 179 // 180 // The placeholders parameter is a variadic associative slice of placeholders and their 181 // replacement. In the following example, the placeholder ":username" will be replaced 182 // with the Name field in the user struct. 183 // 184 // lang.Get("en-US", "greetings", ":username", user.Name) 185 func Get(lang string, line string, placeholders ...string) string { 186 if !IsAvailable(lang) { 187 return line 188 } 189 190 mutex.RLock() 191 defer mutex.RUnlock() 192 if strings.Count(line, ".") > 0 { 193 path := strings.Split(line, ".") 194 if path[0] == "validation" { 195 switch path[1] { 196 case "rules": 197 if len(path) < 3 { 198 return line 199 } 200 return convertEmptyLine(line, languages[lang].validation.rules[strings.Join(path[2:], ".")], placeholders) 201 case "fields": 202 len := len(path) 203 if len < 3 { 204 return line 205 } 206 attr := languages[lang].validation.fields[path[2]] 207 if len == 4 { 208 if attr.Rules == nil { 209 return line 210 } 211 return convertEmptyLine(line, attr.Rules[path[3]], placeholders) 212 } else if len == 3 { 213 return convertEmptyLine(line, attr.Name, placeholders) 214 } else { 215 return line 216 } 217 default: 218 return line 219 } 220 } 221 } 222 223 return convertEmptyLine(line, languages[lang].lines[line], placeholders) 224 } 225 226 func processPlaceholders(message string, values []string) string { 227 length := len(values) - 1 228 for i := 0; i < length; i += 2 { 229 message = strings.ReplaceAll(message, values[i], values[i+1]) 230 } 231 return message 232 } 233 234 func convertEmptyLine(entry, line string, placeholders []string) string { 235 if line == "" { 236 return entry 237 } 238 return processPlaceholders(line, placeholders) 239 } 240 241 // IsAvailable returns true if the language is available. 242 func IsAvailable(lang string) bool { 243 mutex.RLock() 244 defer mutex.RUnlock() 245 _, exists := languages[lang] 246 return exists 247 } 248 249 // GetAvailableLanguages returns a slice of all loaded languages. 250 // This can be used to generate different routes for all languages 251 // supported by your applications. 252 // 253 // /en/products 254 // /fr/produits 255 // ... 256 func GetAvailableLanguages() []string { 257 mutex.RLock() 258 defer mutex.RUnlock() 259 langs := []string{} 260 for lang := range languages { 261 langs = append(langs, lang) 262 } 263 return langs 264 } 265 266 // DetectLanguage detects the language to use based on the given lang string. 267 // The given lang string can use the HTTP "Accept-Language" header format. 268 // 269 // If "*" is provided, the default language will be used. 270 // If multiple languages are given, the first available language will be used, 271 // and if none are available, the default language will be used. 272 // If no variant is given (for example "en"), the first available variant will be used. 273 // For example, if "en-US" and "en-UK" are available and the request accepts "en", 274 // "en-US" will be used. 275 func DetectLanguage(lang string) string { 276 values := helper.ParseMultiValuesHeader(lang) 277 for _, l := range values { 278 if l.Value == "*" { // Accept anything, so return default language 279 break 280 } 281 if IsAvailable(l.Value) { 282 return l.Value 283 } 284 for key := range languages { 285 if strings.HasPrefix(key, l.Value) { 286 return key 287 } 288 } 289 } 290 291 return config.GetString("app.defaultLanguage") 292 }