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  }