github.com/orderbynull/buffalo@v0.11.1/middleware/i18n/i18n.go (about)

     1  package i18n
     2  
     3  import (
     4  	"fmt"
     5  	"path/filepath"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/gobuffalo/buffalo"
    10  	"github.com/gobuffalo/packr"
    11  	"github.com/nicksnyder/go-i18n/i18n"
    12  	"github.com/nicksnyder/go-i18n/i18n/language"
    13  	"github.com/nicksnyder/go-i18n/i18n/translation"
    14  	"github.com/pkg/errors"
    15  )
    16  
    17  // LanguageFinder can be implemented for custom finding of search
    18  // languages. This can be useful if you want to load a user's language
    19  // from something like a database. See Middleware() for more information
    20  // on how the default implementation searches for languages.
    21  type LanguageFinder func(*Translator, buffalo.Context) []string
    22  
    23  // Translator for handling all your i18n needs.
    24  type Translator struct {
    25  	// Box - where are the files?
    26  	Box packr.Box
    27  	// DefaultLanguage - default is passed as a parameter on New.
    28  	DefaultLanguage string
    29  	// CookieName - name of the cookie to find the desired language.
    30  	// default is "lang"
    31  	CookieName string
    32  	// SessionName - name of the session to find the desired language.
    33  	// default is "lang"
    34  	SessionName string
    35  	// HelperName - name of the view helper. default is "t"
    36  	HelperName     string
    37  	LanguageFinder LanguageFinder
    38  }
    39  
    40  // Load translations from the t.Box.
    41  func (t *Translator) Load() error {
    42  	return t.Box.Walk(func(path string, f packr.File) error {
    43  		b, err := t.Box.MustBytes(path)
    44  		if err != nil {
    45  			return errors.Wrapf(err, "unable to read locale file %s", path)
    46  		}
    47  
    48  		base := filepath.Base(path)
    49  		dir := filepath.Dir(path)
    50  
    51  		// Add a prefix to the loaded string, to avoid collision with an ISO lang code
    52  		err = i18n.ParseTranslationFileBytes(fmt.Sprintf("%sbuff%s", dir, base), b)
    53  		if err != nil {
    54  			return errors.Wrapf(err, "unable to parse locale file %s", base)
    55  		}
    56  		return nil
    57  	})
    58  }
    59  
    60  // AddTranslation directly, without using a file. This is useful if you wish to load translations
    61  // from a database, instead of disk.
    62  func (t *Translator) AddTranslation(lang *language.Language, translations ...translation.Translation) {
    63  	i18n.AddTranslation(lang, translations...)
    64  }
    65  
    66  // New Translator. Requires a packr.Box that points to the location
    67  // of the translation files, as well as a default language. This will
    68  // also call t.Load() and load the translations from disk.
    69  func New(box packr.Box, language string) (*Translator, error) {
    70  	t := &Translator{
    71  		Box:             box,
    72  		DefaultLanguage: language,
    73  		CookieName:      "lang",
    74  		SessionName:     "lang",
    75  		HelperName:      "t",
    76  		LanguageFinder:  defaultLanguageFinder,
    77  	}
    78  	return t, t.Load()
    79  }
    80  
    81  // Middleware for loading the translations for the language(s)
    82  // selected. By default languages are loaded in the following order:
    83  //
    84  // Cookie - "lang"
    85  // Session - "lang"
    86  // Header - "Accept-Language"
    87  // Default - "en-US"
    88  //
    89  // These values can be changed on the Translator itself. In development
    90  // model the translation files will be reloaded on each request.
    91  func (t *Translator) Middleware() buffalo.MiddlewareFunc {
    92  	return func(next buffalo.Handler) buffalo.Handler {
    93  		return func(c buffalo.Context) error {
    94  
    95  			// in development reload the translations
    96  			if c.Value("env").(string) == "development" {
    97  				err := t.Load()
    98  				if err != nil {
    99  					return err
   100  				}
   101  			}
   102  
   103  			// set languages in context, if not set yet
   104  			if langs := c.Value("languages"); langs == nil {
   105  				c.Set("languages", t.LanguageFinder(t, c))
   106  			}
   107  
   108  			// set translator
   109  			if T := c.Value("T"); T == nil {
   110  				langs := c.Value("languages").([]string)
   111  				T, err := i18n.Tfunc(langs[0], langs[1:]...)
   112  				if err != nil {
   113  					c.Logger().Warn(err)
   114  					c.Logger().Warn("Your locale files are probably empty or missing")
   115  				}
   116  				c.Set("T", T)
   117  			}
   118  
   119  			// set up the helper function for the views:
   120  			c.Set(t.HelperName, func(s string, i ...interface{}) string {
   121  				return t.Translate(c, s, i...)
   122  			})
   123  			return next(c)
   124  		}
   125  	}
   126  }
   127  
   128  // Translate returns the translation of the string identified by translationID.
   129  //
   130  // See https://github.com/nicksnyder/go-i18n
   131  //
   132  // If there is no translation for translationID, then the translationID itself is returned.
   133  // This makes it easy to identify missing translations in your app.
   134  //
   135  // If translationID is a non-plural form, then the first variadic argument may be a map[string]interface{}
   136  // or struct that contains template data.
   137  //
   138  // If translationID is a plural form, the function accepts two parameter signatures
   139  // 1. T(count int, data struct{})
   140  // The first variadic argument must be an integer type
   141  // (int, int8, int16, int32, int64) or a float formatted as a string (e.g. "123.45").
   142  // The second variadic argument may be a map[string]interface{} or struct{} that contains template data.
   143  // 2. T(data struct{})
   144  // data must be a struct{} or map[string]interface{} that contains a Count field and the template data,
   145  // Count field must be an integer type (int, int8, int16, int32, int64)
   146  // or a float formatted as a string (e.g. "123.45").
   147  func (t *Translator) Translate(c buffalo.Context, translationID string, args ...interface{}) string {
   148  	T := c.Value("T").(i18n.TranslateFunc)
   149  	return T(translationID, args...)
   150  }
   151  
   152  // AvailableLanguages gets the list of languages provided by the app
   153  func (t *Translator) AvailableLanguages() []string {
   154  	lt := i18n.LanguageTags()
   155  	sort.Strings(lt)
   156  	return lt
   157  }
   158  
   159  func defaultLanguageFinder(t *Translator, c buffalo.Context) []string {
   160  	langs := []string{}
   161  
   162  	r := c.Request()
   163  
   164  	// try to get the language from a cookie:
   165  	if cookie, err := r.Cookie(t.CookieName); err == nil {
   166  		if cookie.Value != "" {
   167  			langs = append(langs, cookie.Value)
   168  		}
   169  	}
   170  
   171  	// try to get the language from the session
   172  	if s := c.Session().Get(t.SessionName); s != nil {
   173  		langs = append(langs, s.(string))
   174  	}
   175  
   176  	// try to get the language from a header:
   177  	acceptLang := r.Header.Get("Accept-Language")
   178  	if acceptLang != "" {
   179  		langs = append(langs, parseAcceptLanguage(acceptLang)...)
   180  	}
   181  
   182  	// finally set the default app language as fallback
   183  	langs = append(langs, t.DefaultLanguage)
   184  	return langs
   185  }
   186  
   187  // Inspired from https://siongui.github.io/2015/02/22/go-parse-accept-language/
   188  // Parse an Accept-Language string to get usable lang values for i18n system
   189  func parseAcceptLanguage(acptLang string) []string {
   190  	var lqs []string
   191  
   192  	langQStrs := strings.Split(acptLang, ",")
   193  	for _, langQStr := range langQStrs {
   194  		trimedLangQStr := strings.Trim(langQStr, " ")
   195  
   196  		langQ := strings.Split(trimedLangQStr, ";")
   197  		lq := langQ[0]
   198  		lqs = append(lqs, lq)
   199  	}
   200  	return lqs
   201  }