taldir

Directory service to resolve wallet mailboxes by messenger addresses
Log | Files | Refs | Submodules | README | LICENSE

i18n.go (16021B)


      1 // Package i18n provides internalization and localization features.
      2 package i18n
      3 
      4 import (
      5 	"net"
      6 	"net/http"
      7 	"os"
      8 	"strings"
      9 	"sync"
     10 
     11 	"github.com/kataras/i18n/internal"
     12 
     13 	"golang.org/x/net/publicsuffix"
     14 	"golang.org/x/text/language"
     15 )
     16 
     17 // Default keeps a package-level pre-loaded `I18n` instance.
     18 // The default glob pattern is "./locales/*/*" which accepts folder
     19 // structure as:
     20 // - ./locales
     21 //   - el-GR
     22 //   - filename.yaml
     23 //   - filename.toml
     24 //   - filename.json
     25 //   - en-US
     26 //   - ...
     27 //   - zh-CN
     28 //   - ...
     29 //   - ...
     30 //
     31 // The default language depends on the first lookup, please use the package-level `SetDefaultLanguage`
     32 // to set a default language as you are not able to customize the language lists from here.
     33 //
     34 // See `New` package-level function to declare a fresh new, customized, `I18n` instance.
     35 var Default *I18n
     36 
     37 func init() {
     38 	Default, _ = New(Glob("./locales/*/*"))
     39 }
     40 
     41 // SetDefaultLanguage changes the default language of the `Default` `I18n` instance.
     42 func SetDefaultLanguage(langCode string) bool {
     43 	return Default.SetDefault(langCode)
     44 }
     45 
     46 type (
     47 	// Map is just an alias of the map[string]interface{} type.
     48 	Map = map[string]interface{}
     49 
     50 	// Locale is the type which the `Localizer.GetLocale` method returns.
     51 	// It serves the translations based on "key" or format. See its `GetMessage`.
     52 	Locale = internal.Locale
     53 
     54 	// MessageFunc is the function type to modify the behavior when a key or language was not found.
     55 	// All language inputs fallback to the default locale if not matched.
     56 	// This is why this signature accepts both input and matched languages, so caller
     57 	// can provide better messages.
     58 	//
     59 	// The first parameter is set to the client real input of the language,
     60 	// the second one is set to the matched language (default one if input wasn't matched)
     61 	// and the third and forth are the translation format/key and its optional arguments.
     62 	MessageFunc = internal.MessageFunc
     63 
     64 	// Loader accepts a `Matcher` and should return a `Localizer`.
     65 	// Functions that implement this type should load locale files.
     66 	Loader func(m *Matcher) (Localizer, error)
     67 
     68 	// Localizer is the interface which returned from a `Loader`.
     69 	// Types that implement this interface should be able to retrieve a `Locale`
     70 	// based on the language index.
     71 	Localizer interface {
     72 		// GetLocale should return a valid `Locale` based on the language index.
     73 		// It will always match the Loader.Matcher.Languages[index].
     74 		// It may return the default language if nothing else matches based on custom localizer's criteria.
     75 		GetLocale(index int) *Locale
     76 	}
     77 )
     78 
     79 // I18n is the structure which keeps the i18n configuration and implements Localization and internationalization features.
     80 type I18n struct {
     81 	localizer Localizer
     82 	matcher   *Matcher
     83 
     84 	loader Loader
     85 	mu     sync.Mutex
     86 
     87 	// If not nil, this request's context key can be used to identify the current language.
     88 	// The found language(in this case, by path or subdomain) will be also filled with the current language on `Router` method.
     89 	ContextKey interface{}
     90 	// DefaultMessageFunc is the field which can be used
     91 	// to modify the behavior when a key or language was not found.
     92 	// All language inputs fallback to the default locale if not matched.
     93 	// This is why this one accepts both input and matched languages,
     94 	// so the caller can be more expressful knowing those.
     95 	//
     96 	// Defaults to nil.
     97 	DefaultMessageFunc MessageFunc
     98 	// ExtractFunc is the type signature for declaring custom logic
     99 	// to extract the language tag name.
    100 	ExtractFunc func(*http.Request) string
    101 	// If not empty, it is language identifier by url query.
    102 	URLParameter string
    103 	// If not empty, it is language identifier by cookie of this name.
    104 	Cookie string
    105 	// If true then a subdomain can be a language identifier too.
    106 	Subdomain bool
    107 	// If true then it will return empty string when translation for a a specific language's key was not found.
    108 	// Defaults to false, fallback defaultLang:key will be used.
    109 	Strict bool
    110 }
    111 
    112 // makeTags converts language codes to language Tags.
    113 func makeTags(languages ...string) (tags []language.Tag) {
    114 	for _, lang := range languages {
    115 		tag, err := language.Parse(lang)
    116 		if err == nil && tag != language.Und {
    117 			tags = append(tags, tag)
    118 		}
    119 	}
    120 
    121 	return
    122 }
    123 
    124 // New returns a new `I18n` instance.
    125 // It contains a `Router` wrapper to (local) redirect subdomains and path prefixes too.
    126 //
    127 // The "languages" input parameter is optional and if not empty then only these languages
    128 // will be used for translations and the rest (if any) will be skipped.
    129 // the first parameter of "loader" which lookups for translations inside files.
    130 func New(loader Loader, languages ...string) (*I18n, error) {
    131 	tags := makeTags(languages...)
    132 
    133 	i := new(I18n)
    134 	i.loader = loader
    135 	i.matcher = &Matcher{
    136 		strict:             len(tags) > 0,
    137 		Languages:          tags,
    138 		matcher:            language.NewMatcher(tags),
    139 		defaultMessageFunc: i.DefaultMessageFunc,
    140 	}
    141 
    142 	if err := i.reload(); err != nil {
    143 		return nil, err
    144 	}
    145 
    146 	return i, nil
    147 }
    148 
    149 // reload loads the language files from the provided Loader,
    150 // the `New` package-level function preloads those files already.
    151 func (i *I18n) reload() error { // May be an exported function, if requested.
    152 	i.mu.Lock()
    153 	defer i.mu.Unlock()
    154 
    155 	localizer, err := i.loader(i.matcher)
    156 	if err != nil {
    157 		return err
    158 	}
    159 
    160 	i.localizer = localizer
    161 	return nil
    162 }
    163 
    164 // SetDefault changes the default language.
    165 // Please avoid using this method; the default behavior will accept
    166 // the first language of the registered tags as the default one.
    167 func (i *I18n) SetDefault(langCode string) bool {
    168 	t, err := language.Parse(langCode)
    169 	if err != nil {
    170 		return false
    171 	}
    172 
    173 	if tag, index, conf := i.matcher.Match(t); conf > language.Low {
    174 		if l, ok := i.localizer.(interface {
    175 			SetDefault(int) bool
    176 		}); ok {
    177 			if l.SetDefault(index) {
    178 				tags := i.matcher.Languages
    179 				// set the order
    180 				tags[index] = tags[0]
    181 				tags[0] = tag
    182 
    183 				i.matcher.Languages = tags
    184 				i.matcher.matcher = language.NewMatcher(tags)
    185 				return true
    186 			}
    187 		}
    188 	}
    189 
    190 	return false
    191 }
    192 
    193 // Matcher implements the languae.Matcher.
    194 // It contains the original language Matcher and keeps an ordered
    195 // list of the registered languages for further use (see `Loader` implementation).
    196 type Matcher struct {
    197 	strict    bool
    198 	Languages []language.Tag
    199 	matcher   language.Matcher
    200 	// defaultMessageFunc passed by the i18n structure.
    201 	defaultMessageFunc MessageFunc
    202 }
    203 
    204 var _ language.Matcher = (*Matcher)(nil)
    205 
    206 // Match returns the best match for any of the given tags, along with
    207 // a unique index associated with the returned tag and a confidence
    208 // score.
    209 func (m *Matcher) Match(t ...language.Tag) (language.Tag, int, language.Confidence) {
    210 	return m.matcher.Match(t...)
    211 }
    212 
    213 // MatchOrAdd acts like Match but it checks and adds a language tag, if not found,
    214 // when the `Matcher.strict` field is true (when no tags are provided by the caller)
    215 // and they should be dynamically added to the list.
    216 func (m *Matcher) MatchOrAdd(t language.Tag) (tag language.Tag, index int, conf language.Confidence) {
    217 	tag, index, conf = m.Match(t)
    218 	if conf <= language.Low && !m.strict {
    219 		// not found, add it now.
    220 		m.Languages = append(m.Languages, t)
    221 		tag = t
    222 		index = len(m.Languages) - 1
    223 		conf = language.Exact
    224 		m.matcher = language.NewMatcher(m.Languages) // reset matcher to include the new language.
    225 	}
    226 
    227 	return
    228 }
    229 
    230 // ParseLanguageFiles returns a map of language indexes and
    231 // their associated files based on the "fileNames".
    232 func (m *Matcher) ParseLanguageFiles(fileNames []string) (map[int][]string, error) {
    233 	languageFiles := make(map[int][]string)
    234 
    235 	for _, fileName := range fileNames {
    236 		index := parsePath(m, fileName)
    237 		if index == -1 {
    238 			continue
    239 		}
    240 
    241 		languageFiles[index] = append(languageFiles[index], fileName)
    242 	}
    243 
    244 	return languageFiles, nil
    245 }
    246 
    247 func parsePath(m *Matcher, path string) int {
    248 	if t, ok := parseLanguage(path); ok {
    249 		if _, index, conf := m.MatchOrAdd(t); conf > language.Low {
    250 			return index
    251 		}
    252 	}
    253 
    254 	return -1
    255 }
    256 
    257 func parseLanguageName(m *Matcher, name string) int {
    258 	if t, err := language.Parse(name); err == nil {
    259 		if _, index, conf := m.MatchOrAdd(t); conf > language.Low {
    260 			return index
    261 		}
    262 	}
    263 
    264 	return -1
    265 }
    266 
    267 func reverseStrings(s []string) []string {
    268 	for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
    269 		s[i], s[j] = s[j], s[i]
    270 	}
    271 	return s
    272 }
    273 
    274 func parseLanguage(path string) (language.Tag, bool) {
    275 	if idx := strings.LastIndexByte(path, '.'); idx > 0 {
    276 		path = path[0:idx]
    277 	}
    278 
    279 	// path = strings.ReplaceAll(path, "..", "")
    280 
    281 	names := strings.FieldsFunc(path, func(r rune) bool {
    282 		return r == '_' || r == os.PathSeparator || r == '/' || r == '.'
    283 	})
    284 
    285 	names = reverseStrings(names) // see https://github.com/kataras/i18n/issues/1
    286 
    287 	for _, s := range names {
    288 		t, err := language.Parse(s)
    289 		if err != nil {
    290 			continue
    291 		}
    292 
    293 		return t, true
    294 	}
    295 
    296 	return language.Und, false
    297 }
    298 
    299 // TryMatchString will try to match the "s" with a registered language tag.
    300 // It returns -1 as the language index and false if not found.
    301 func (i *I18n) TryMatchString(s string) (language.Tag, int, bool) {
    302 	if tag, err := language.Parse(s); err == nil {
    303 		if tag, index, conf := i.matcher.Match(tag); conf > language.Low {
    304 			return tag, index, true
    305 		}
    306 	}
    307 
    308 	return language.Und, -1, false
    309 }
    310 
    311 // Tr is package-level function which calls the `Default.Tr` method.
    312 //
    313 // See `I18n#Tr` method for more.
    314 func Tr(lang, format string, args ...interface{}) string {
    315 	return Default.Tr(lang, format, args...)
    316 }
    317 
    318 // Tr returns a translated message based on the "lang" language code
    319 // and its key(format) with any optional arguments attached to it.
    320 //
    321 // It returns an empty string if "lang" not matched, unless DefaultMessageFunc.
    322 // It returns the default language's translation if "key" not matched, unless DefaultMessageFunc.
    323 func (i *I18n) Tr(lang, format string, args ...interface{}) (msg string) {
    324 	_, index, ok := i.TryMatchString(lang)
    325 	if !ok {
    326 		index = 0
    327 	}
    328 
    329 	langMatched := ""
    330 
    331 	loc := i.localizer.GetLocale(index)
    332 	if loc != nil {
    333 		langMatched = loc.Language()
    334 
    335 		msg = loc.GetMessage(format, args...)
    336 		if msg == "" && i.DefaultMessageFunc == nil && !i.Strict && index > 0 {
    337 			// it's not the default/fallback language and not message found for that lang:key.
    338 			msg = i.localizer.GetLocale(0).GetMessage(format, args...)
    339 		}
    340 	}
    341 
    342 	if msg == "" && i.DefaultMessageFunc != nil {
    343 		msg = i.DefaultMessageFunc(lang, langMatched, format, args...)
    344 	}
    345 
    346 	return
    347 }
    348 
    349 const acceptLanguageHeaderKey = "Accept-Language"
    350 
    351 // GetLocale is package-level function which calls the `Default.GetLocale` method.
    352 //
    353 // See `I18n#GetLocale` method for more.
    354 func GetLocale(r *http.Request) *Locale {
    355 	return Default.GetLocale(r)
    356 }
    357 
    358 // GetLocale returns the found locale of a request.
    359 // It will return the first registered language if nothing else matched.
    360 func (i *I18n) GetLocale(r *http.Request) *Locale {
    361 	var (
    362 		index int
    363 		ok    bool
    364 	)
    365 
    366 	if i.ContextKey != nil {
    367 		if v := r.Context().Value(i.ContextKey); v != nil {
    368 			if s, isString := v.(string); isString {
    369 				if v == "default" {
    370 					index = 0 // no need to call `TryMatchString` and spend time.
    371 				} else {
    372 					_, index, _ = i.TryMatchString(s)
    373 				}
    374 
    375 				locale := i.localizer.GetLocale(index)
    376 				if locale == nil {
    377 					return nil
    378 				}
    379 
    380 				return locale
    381 			}
    382 		}
    383 	}
    384 
    385 	if !ok && i.ExtractFunc != nil {
    386 		if v := i.ExtractFunc(r); v != "" {
    387 			_, index, ok = i.TryMatchString(v)
    388 		}
    389 	}
    390 
    391 	if !ok && i.URLParameter != "" {
    392 		if v := r.URL.Query().Get(i.URLParameter); v != "" {
    393 			_, index, ok = i.TryMatchString(v)
    394 		}
    395 	}
    396 
    397 	if !ok && i.Cookie != "" {
    398 		cookie, err := r.Cookie(i.Cookie)
    399 		if err == nil {
    400 			_, index, ok = i.TryMatchString(cookie.Value) // url.QueryUnescape(cookie.Value)
    401 		}
    402 	}
    403 
    404 	if !ok && i.Subdomain {
    405 		if v, _ := getSubdomain(r); v != "" {
    406 			_, index, ok = i.TryMatchString(v)
    407 		}
    408 	}
    409 
    410 	if !ok {
    411 		if v := r.Header.Get(acceptLanguageHeaderKey); v != "" {
    412 			desired, _, err := language.ParseAcceptLanguage(v)
    413 			if err == nil {
    414 				if _, idx, conf := i.matcher.Match(desired...); conf > language.Low {
    415 					index = idx
    416 				}
    417 			}
    418 		}
    419 	}
    420 
    421 	// if index == 0 then it defaults to the first language.
    422 	locale := i.localizer.GetLocale(index)
    423 	if locale == nil {
    424 		return nil
    425 	}
    426 
    427 	return locale
    428 }
    429 
    430 // GetMessage is package-level function which calls the `Default.GetMessage` method.
    431 //
    432 // See `I18n#GetMessage` method for more.
    433 func GetMessage(r *http.Request, format string, args ...interface{}) string {
    434 	return Default.GetMessage(r, format, args...)
    435 }
    436 
    437 // GetMessage returns the localized text message for this "r" request based on the key "format".
    438 // It returns an empty string if locale or format not found.
    439 func (i *I18n) GetMessage(r *http.Request, format string, args ...interface{}) (msg string) {
    440 	loc := i.GetLocale(r)
    441 	langMatched := ""
    442 	if loc != nil {
    443 		langMatched = loc.Language()
    444 		// it's not the default/fallback language and not message found for that lang:key.
    445 		msg = loc.GetMessage(format, args...)
    446 		if msg == "" && i.DefaultMessageFunc == nil && !i.Strict && loc.Index() > 0 {
    447 			return i.localizer.GetLocale(0).GetMessage(format, args...)
    448 		}
    449 	}
    450 
    451 	if msg == "" && i.DefaultMessageFunc != nil && i.ContextKey != nil {
    452 		if v := r.Context().Value(i.ContextKey); v != nil {
    453 			if langInput, ok := v.(string); ok {
    454 				msg = i.DefaultMessageFunc(langInput, langMatched, format, args...)
    455 			}
    456 		}
    457 	}
    458 
    459 	return
    460 }
    461 
    462 // Router is package-level function which calls the `Default.Router` method.
    463 //
    464 // See `I18n#Router` method for more.
    465 func Router(next http.Handler) http.Handler {
    466 	return Default.Router(next)
    467 }
    468 
    469 func (i *I18n) setLang(w http.ResponseWriter, r *http.Request, lang string) {
    470 	if i.Cookie != "" {
    471 		http.SetCookie(w, &http.Cookie{
    472 			Name:  i.Cookie,
    473 			Value: lang,
    474 			// allow subdomain sharing.
    475 			Domain:   getDomain(getHost(r)),
    476 			SameSite: http.SameSiteLaxMode,
    477 		})
    478 	} else if i.URLParameter != "" {
    479 		q := r.URL.Query()
    480 		q.Set(i.URLParameter, lang)
    481 		r.URL.RawQuery = q.Encode()
    482 	}
    483 
    484 	r.Header.Set(acceptLanguageHeaderKey, lang)
    485 }
    486 
    487 // Router returns a new router wrapper.
    488 // It compares the path prefix for translated language and
    489 // local redirects the requested path with the selected (from the path) language to the router.
    490 func (i *I18n) Router(next http.Handler) http.Handler {
    491 	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
    492 		found := false
    493 		path := r.URL.Path[1:]
    494 
    495 		if idx := strings.IndexByte(path, '/'); idx > 0 {
    496 			path = path[:idx]
    497 		}
    498 
    499 		if path != "" {
    500 			if tag, _, ok := i.TryMatchString(path); ok {
    501 				lang := tag.String()
    502 
    503 				path = r.URL.Path[len(path)+1:]
    504 				if path == "" {
    505 					path = "/"
    506 				}
    507 
    508 				r.RequestURI = path
    509 				r.URL.Path = path
    510 				i.setLang(w, r, lang)
    511 				found = true
    512 			}
    513 		}
    514 
    515 		if !found && i.Subdomain {
    516 			host := getHost(r)
    517 			if dotIdx := strings.IndexByte(host, '.'); dotIdx > 0 {
    518 				if subdomain := host[0:dotIdx]; subdomain != "" {
    519 					if tag, _, ok := i.TryMatchString(subdomain); ok {
    520 						host = host[dotIdx+1:]
    521 						r.URL.Host = host
    522 						r.Host = host
    523 						i.setLang(w, r, tag.String())
    524 					}
    525 				}
    526 			}
    527 		}
    528 
    529 		next.ServeHTTP(w, r)
    530 	})
    531 }
    532 
    533 func getHost(r *http.Request) string {
    534 	// contains subdomain.
    535 	if host := r.URL.Host; host != "" {
    536 		return host
    537 	}
    538 	return r.Host
    539 }
    540 
    541 // GetDomain resolves and returns the server's domain.
    542 func getDomain(hostport string) string {
    543 	host := hostport
    544 	if tmp, _, err := net.SplitHostPort(hostport); err == nil {
    545 		host = tmp
    546 	}
    547 
    548 	switch host {
    549 	// We could use the netutil.LoopbackRegex but leave it as it's for now, it's faster.
    550 	case "localhost", "127.0.0.1", "0.0.0.0", "::1", "[::1]", "0:0:0:0:0:0:0:0", "0:0:0:0:0:0:0:1":
    551 		// loopback.
    552 		return "localhost"
    553 	default:
    554 		if domain, err := publicsuffix.EffectiveTLDPlusOne(host); err == nil {
    555 			host = domain
    556 		}
    557 
    558 		return host
    559 	}
    560 }
    561 
    562 func getSubdomain(r *http.Request) (subdomain, host string) {
    563 	host = getHost(r)
    564 
    565 	if index := strings.IndexByte(host, '.'); index > 0 {
    566 		if subdomain = host[0:index]; subdomain != "" {
    567 			host = host[index+1:]
    568 		}
    569 	}
    570 
    571 	return
    572 }