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  }