github.com/Azareal/Gosora@v0.0.0-20210729070923-553e66b59003/common/phrases/phrases.go (about) 1 /* 2 * 3 * Gosora Phrase System 4 * Copyright Azareal 2017 - 2020 5 * 6 */ 7 package phrases 8 9 import ( 10 "encoding/json" 11 "errors" 12 "fmt" 13 "io/ioutil" 14 "log" 15 "os" 16 "path/filepath" 17 "strconv" 18 "strings" 19 "sync" 20 "sync/atomic" 21 "time" 22 ) 23 24 // TODO: Add a phrase store? 25 // TODO: Let the admin edit phrases from inside the Control Panel? How should we persist these? Should we create a copy of the langpack or edit the primaries? Use the changeLangpack mutex for this? 26 // nolint Be quiet megacheck, this *is* used 27 var currentLangPack atomic.Value 28 var langPackCount int // TODO: Use atomics for this 29 30 // TODO: We'll be implementing the level phrases in the software proper very very soon! 31 type LevelPhrases struct { 32 Level string 33 LevelMax string // ? Add a max level setting? 34 35 // Override the phrase for individual levels, if the phrases exist 36 Levels []string // index = level 37 } 38 39 // ! For the sake of thread safety, you must never modify a *LanguagePack directly, but to create a copy of it and overwrite the entry in the sync.Map 40 type LanguagePack struct { 41 Name string 42 IsoCode string 43 ModTime time.Time 44 //LastUpdated string 45 46 // Should we use a sync map or a struct for these? It would be nice, if we could keep all the phrases consistent. 47 Levels LevelPhrases 48 Perms map[string]string 49 SettingPhrases map[string]string 50 PermPresets map[string]string 51 Accounts map[string]string // TODO: Apply these phrases in the software proper 52 UserAgents map[string]string 53 OperatingSystems map[string]string 54 HumanLanguages map[string]string 55 Errors map[string]string // Temp stand-in 56 ErrorsBytes map[string][]byte 57 NoticePhrases map[string]string 58 PageTitles map[string]string 59 TmplPhrases map[string]string 60 TmplPhrasesPrefixes map[string]map[string]string // [prefix][name]phrase 61 62 TmplIndicesToPhrases [][][]byte // [tmplID][index]phrase 63 } 64 65 // TODO: Add the ability to edit language JSON files from the Control Panel and automatically scan the files for changes 66 var langPacks sync.Map // nolint it is used 67 var langTmplIndicesToNames [][]string // [tmplID][index]phraseName 68 69 func InitPhrases(lang string) error { 70 log.Print("Loading the language packs") 71 err := filepath.Walk("./langs", func(path string, f os.FileInfo, err error) error { 72 if f.IsDir() { 73 return nil 74 } 75 if err != nil { 76 return err 77 } 78 79 ext := filepath.Ext("/langs/" + path) 80 if ext != ".json" { 81 log.Printf("Found a '%s' in /langs/", ext) 82 return nil 83 } 84 85 data, err := ioutil.ReadFile(path) 86 if err != nil { 87 return err 88 } 89 90 var langPack LanguagePack 91 err = json.Unmarshal(data, &langPack) 92 if err != nil { 93 return err 94 } 95 langPack.ModTime = f.ModTime() 96 97 langPack.ErrorsBytes = make(map[string][]byte) 98 for name, phrase := range langPack.Errors { 99 langPack.ErrorsBytes[name] = []byte(phrase) 100 } 101 102 // [prefix][name]phrase 103 langPack.TmplPhrasesPrefixes = make(map[string]map[string]string) 104 conMap := make(map[string]string) // Cache phrase strings so we can de-dupe items to reduce memory use. There appear to be some minor improvements with this, although we would need a more thorough check to be sure. 105 for name, phrase := range langPack.TmplPhrases { 106 _, ok := conMap[phrase] 107 if !ok { 108 conMap[phrase] = phrase 109 } 110 cItem := conMap[phrase] 111 prefix := strings.Split(name, ".")[0] 112 _, ok = langPack.TmplPhrasesPrefixes[prefix] 113 if !ok { 114 langPack.TmplPhrasesPrefixes[prefix] = make(map[string]string) 115 } 116 langPack.TmplPhrasesPrefixes[prefix][name] = cItem 117 } 118 119 // [prefix][name]phrase 120 /*langPack.TmplPhrasesPrefixes = make(map[string]map[string]string) 121 for name, phrase := range langPack.TmplPhrases { 122 prefix := strings.Split(name, ".")[0] 123 _, ok := langPack.TmplPhrasesPrefixes[prefix] 124 if !ok { 125 langPack.TmplPhrasesPrefixes[prefix] = make(map[string]string) 126 } 127 langPack.TmplPhrasesPrefixes[prefix][name] = phrase 128 }*/ 129 130 langPack.TmplIndicesToPhrases = make([][][]byte, len(langTmplIndicesToNames)) 131 for tmplID, phraseNames := range langTmplIndicesToNames { 132 phraseSet := make([][]byte, len(phraseNames)) 133 for index, phraseName := range phraseNames { 134 phrase, ok := langPack.TmplPhrases[phraseName] 135 if !ok { 136 log.Printf("langPack.TmplPhrases: %+v\n", langPack.TmplPhrases) 137 panic("Couldn't find template phrase '" + phraseName + "'") 138 } 139 phraseSet[index] = []byte(phrase) 140 } 141 langPack.TmplIndicesToPhrases[tmplID] = phraseSet 142 TmplIndexCallback(tmplID, phraseSet) 143 } 144 145 log.Print("Adding the '" + langPack.Name + "' language pack") 146 langPacks.Store(langPack.Name, &langPack) 147 langPackCount++ 148 149 return nil 150 }) 151 if err != nil { 152 return err 153 } 154 if langPackCount == 0 { 155 return errors.New("You don't have any language packs") 156 } 157 158 langPack, ok := langPacks.Load(lang) 159 if !ok { 160 return errors.New("Couldn't find the " + lang + " language pack") 161 } 162 currentLangPack.Store(langPack) 163 return nil 164 } 165 166 // TODO: Implement this 167 func LoadLangPack(name string) error { 168 _ = name 169 return nil 170 } 171 172 // TODO: Implement this 173 func SaveLangPack(langPack *LanguagePack) error { 174 _ = langPack 175 return nil 176 } 177 178 func GetLangPack() *LanguagePack { 179 return currentLangPack.Load().(*LanguagePack) 180 } 181 182 func GetLevelPhrase(level int) string { 183 levelPhrases := currentLangPack.Load().(*LanguagePack).Levels 184 if len(levelPhrases.Levels) > 0 && level < len(levelPhrases.Levels) { 185 return strings.Replace(levelPhrases.Levels[level], "{0}", strconv.Itoa(level), -1) 186 } 187 return strings.Replace(levelPhrases.Level, "{0}", strconv.Itoa(level), -1) 188 } 189 190 func GetPermPhrase(name string) string { 191 res, ok := currentLangPack.Load().(*LanguagePack).Perms[name] 192 if !ok { 193 return getPlaceholder("perms", name) 194 } 195 return res 196 } 197 198 func GetSettingPhrase(name string) string { 199 res, ok := currentLangPack.Load().(*LanguagePack).SettingPhrases[name] 200 if !ok { 201 return getPlaceholder("settings", name) 202 } 203 return res 204 } 205 206 func GetAllSettingPhrases() map[string]string { 207 return currentLangPack.Load().(*LanguagePack).SettingPhrases 208 } 209 210 func GetAllPermPresets() map[string]string { 211 return currentLangPack.Load().(*LanguagePack).PermPresets 212 } 213 214 func GetAccountPhrase(name string) string { 215 res, ok := currentLangPack.Load().(*LanguagePack).Accounts[name] 216 if !ok { 217 return getPlaceholder("account", name) 218 } 219 return res 220 } 221 222 func GetUserAgentPhrase(name string) (string, bool) { 223 res, ok := currentLangPack.Load().(*LanguagePack).UserAgents[name] 224 if !ok { 225 return "", false 226 } 227 return res, true 228 } 229 230 func GetOSPhrase(name string) (string, bool) { 231 res, ok := currentLangPack.Load().(*LanguagePack).OperatingSystems[name] 232 if !ok { 233 return "", false 234 } 235 return res, true 236 } 237 238 func GetHumanLangPhrase(name string) (string, bool) { 239 res, ok := currentLangPack.Load().(*LanguagePack).HumanLanguages[name] 240 if !ok { 241 return getPlaceholder("humanlang", name), false 242 } 243 return res, true 244 } 245 246 // TODO: Does comma ok work with multi-dimensional maps? 247 func GetErrorPhrase(name string) string { 248 res, ok := currentLangPack.Load().(*LanguagePack).Errors[name] 249 if !ok { 250 return getPlaceholder("error", name) 251 } 252 return res 253 } 254 func GetErrorPhraseBytes(name string) []byte { 255 res, ok := currentLangPack.Load().(*LanguagePack).ErrorsBytes[name] 256 if !ok { 257 return getPlaceholderBytes("error", name) 258 } 259 return res 260 } 261 262 func GetNoticePhrase(name string) string { 263 res, ok := currentLangPack.Load().(*LanguagePack).NoticePhrases[name] 264 if !ok { 265 return getPlaceholder("notices", name) 266 } 267 return res 268 } 269 270 func GetTitlePhrase(name string) string { 271 res, ok := currentLangPack.Load().(*LanguagePack).PageTitles[name] 272 if !ok { 273 return getPlaceholder("title", name) 274 } 275 return res 276 } 277 278 func GetTitlePhrasef(name string, params ...interface{}) string { 279 res, ok := currentLangPack.Load().(*LanguagePack).PageTitles[name] 280 if !ok { 281 return getPlaceholder("title", name) 282 } 283 return fmt.Sprintf(res, params...) 284 } 285 286 func GetTmplPhrase(name string) string { 287 res, ok := currentLangPack.Load().(*LanguagePack).TmplPhrases[name] 288 if !ok { 289 return getPlaceholder("tmpl", name) 290 } 291 return res 292 } 293 294 func GetTmplPhrasef(name string, params ...interface{}) string { 295 res, ok := currentLangPack.Load().(*LanguagePack).TmplPhrases[name] 296 if !ok { 297 return getPlaceholder("tmpl", name) 298 } 299 return fmt.Sprintf(res, params...) 300 } 301 302 func GetTmplPhrases() map[string]string { 303 return currentLangPack.Load().(*LanguagePack).TmplPhrases 304 } 305 306 func GetTmplPhrasesByPrefix(prefix string) (phrases map[string]string, ok bool) { 307 res, ok := currentLangPack.Load().(*LanguagePack).TmplPhrasesPrefixes[prefix] 308 return res, ok 309 } 310 311 func getPlaceholder(prefix, suffix string) string { 312 return "{lang." + prefix + "[" + suffix + "]}" 313 } 314 func getPlaceholderBytes(prefix, suffix string) []byte { 315 return []byte("{lang." + prefix + "[" + suffix + "]}") 316 } 317 318 // ! Please don't mutate *LanguagePack 319 func GetCurrentLangPack() *LanguagePack { 320 return currentLangPack.Load().(*LanguagePack) 321 } 322 323 // ? - Use runtime reflection for updating phrases? 324 // TODO: Implement these 325 func AddPhrase() { 326 327 } 328 func UpdatePhrase() { 329 330 } 331 func DeletePhrase() { 332 333 } 334 335 // TODO: Use atomics to store the pointer of the current active langpack? 336 // nolint 337 func ChangeLanguagePack(name string) (exists bool) { 338 pack, ok := langPacks.Load(name) 339 if !ok { 340 return false 341 } 342 currentLangPack.Store(pack) 343 return true 344 } 345 346 func CurrentLanguagePackName() (name string) { 347 return currentLangPack.Load().(*LanguagePack).Name 348 } 349 350 func GetLanguagePackByName(name string) (pack *LanguagePack, ok bool) { 351 packInt, ok := langPacks.Load(name) 352 if !ok { 353 return nil, false 354 } 355 return packInt.(*LanguagePack), true 356 } 357 358 // Template Transpiler Stuff 359 360 func RegisterTmplPhraseNames(phraseNames []string) (tmplID int) { 361 langTmplIndicesToNames = append(langTmplIndicesToNames, phraseNames) 362 return len(langTmplIndicesToNames) - 1 363 } 364 365 func GetTmplPhrasesBytes(tmplID int) [][]byte { 366 return currentLangPack.Load().(*LanguagePack).TmplIndicesToPhrases[tmplID] 367 } 368 369 // New 370 371 var indexCallbacks []func([][]byte) 372 373 func TmplIndexCallback(tmplID int, phraseSet [][]byte) { 374 indexCallbacks[tmplID](phraseSet) 375 } 376 377 func AddTmplIndexCallback(h func([][]byte)) { 378 indexCallbacks = append(indexCallbacks, h) 379 }