forked from toolshed/abra
		
	
		
			
				
	
	
		
			479 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			479 lines
		
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
|  * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved.
 | |
|  * Licensed under the MIT License. See LICENSE file in the project root for full license information.
 | |
|  */
 | |
| 
 | |
| package gotext
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"encoding/gob"
 | |
| 	"io/fs"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"sync"
 | |
| )
 | |
| 
 | |
| /*
 | |
| Locale wraps the entire i18n collection for a single language (locale)
 | |
| It's used by the package functions, but it can also be used independently to handle
 | |
| multiple languages at the same time by working with this object.
 | |
| 
 | |
| Example:
 | |
| 
 | |
| 	    import (
 | |
| 		"encoding/gob"
 | |
| 		"bytes"
 | |
| 		    "fmt"
 | |
| 		    "github.com/leonelquinteros/gotext"
 | |
| 	    )
 | |
| 
 | |
| 	    func main() {
 | |
| 	        // Create Locale with library path and language code
 | |
| 	        l := gotext.NewLocale("/path/to/i18n/dir", "en_US")
 | |
| 
 | |
| 	        // Load domain '/path/to/i18n/dir/en_US/LC_MESSAGES/default.{po,mo}'
 | |
| 	        l.AddDomain("default")
 | |
| 
 | |
| 	        // Translate text from default domain
 | |
| 	        fmt.Println(l.Get("Translate this"))
 | |
| 
 | |
| 	        // Load different domain ('/path/to/i18n/dir/en_US/LC_MESSAGES/extras.{po,mo}')
 | |
| 	        l.AddDomain("extras")
 | |
| 
 | |
| 	        // Translate text from domain
 | |
| 	        fmt.Println(l.GetD("extras", "Translate this"))
 | |
| 	    }
 | |
| */
 | |
| type Locale struct {
 | |
| 	// Path to locale files.
 | |
| 	path string
 | |
| 
 | |
| 	// Language for this Locale
 | |
| 	lang string
 | |
| 
 | |
| 	// List of available Domains for this locale.
 | |
| 	Domains map[string]Translator
 | |
| 
 | |
| 	// First AddDomain is default Domain
 | |
| 	defaultDomain string
 | |
| 
 | |
| 	// Sync Mutex
 | |
| 	sync.RWMutex
 | |
| 
 | |
| 	// optional fs to use
 | |
| 	fs fs.FS
 | |
| }
 | |
| 
 | |
| // NewLocale creates and initializes a new Locale object for a given language.
 | |
| // It receives a path for the i18n .po/.mo files directory (p) and a language code to use (l).
 | |
| func NewLocale(p, l string) *Locale {
 | |
| 	return &Locale{
 | |
| 		path:    p,
 | |
| 		lang:    SimplifiedLocale(l),
 | |
| 		Domains: make(map[string]Translator),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // NewLocaleFS returns a Locale working with a fs.FS
 | |
| func NewLocaleFS(l string, filesystem fs.FS) *Locale {
 | |
| 	loc := NewLocale("", l)
 | |
| 	loc.fs = filesystem
 | |
| 	return loc
 | |
| }
 | |
| 
 | |
| // NewLocaleFSWithPath returns a Locale working with a fs.FS on a p path folder.
 | |
| func NewLocaleFSWithPath(l string, filesystem fs.FS, p string) *Locale {
 | |
| 	loc := NewLocale("", l)
 | |
| 	loc.fs = filesystem
 | |
| 	loc.path = p
 | |
| 	return loc
 | |
| }
 | |
| 
 | |
| func (l *Locale) findExt(dom, ext string) string {
 | |
| 	filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
 | |
| 	if l.fileExists(filename) {
 | |
| 		return filename
 | |
| 	}
 | |
| 
 | |
| 	if len(l.lang) > 2 {
 | |
| 		filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
 | |
| 		if l.fileExists(filename) {
 | |
| 			return filename
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	filename = path.Join(l.path, l.lang, dom+"."+ext)
 | |
| 	if l.fileExists(filename) {
 | |
| 		return filename
 | |
| 	}
 | |
| 
 | |
| 	if len(l.lang) > 2 {
 | |
| 		filename = path.Join(l.path, l.lang[:2], dom+"."+ext)
 | |
| 		if l.fileExists(filename) {
 | |
| 			return filename
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| // GetActualLanguage inspects the filesystem and decides whether to strip
 | |
| // a CC part of the ll_CC locale string.
 | |
| func (l *Locale) GetActualLanguage(dom string) string {
 | |
| 	extensions := []string{"mo", "po"}
 | |
| 	var fp string
 | |
| 	for _, ext := range extensions {
 | |
| 		// 'll' (or 'll_CC') exists, and it was specified as-is
 | |
| 		fp = path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
 | |
| 		if l.fileExists(fp) {
 | |
| 			return l.lang
 | |
| 		}
 | |
| 		// 'll' exists, but 'll_CC' was specified
 | |
| 		if len(l.lang) > 2 {
 | |
| 			fp = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
 | |
| 			if l.fileExists(fp) {
 | |
| 				return l.lang[:2]
 | |
| 			}
 | |
| 		}
 | |
| 		// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
 | |
| 		fp = path.Join(l.path, l.lang, dom+"."+ext)
 | |
| 		if l.fileExists(fp) {
 | |
| 			return l.lang
 | |
| 		}
 | |
| 		// 'll' exists outside of LC_category, but 'll_CC' was specified
 | |
| 		if len(l.lang) > 2 {
 | |
| 			fp = path.Join(l.path, l.lang[:2], dom+"."+ext)
 | |
| 			if l.fileExists(fp) {
 | |
| 				return l.lang[:2]
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return ""
 | |
| }
 | |
| 
 | |
| func (l *Locale) fileExists(filename string) bool {
 | |
| 	if l.fs != nil {
 | |
| 		_, err := fs.Stat(l.fs, filename)
 | |
| 		return err == nil
 | |
| 	}
 | |
| 	_, err := os.Stat(filename)
 | |
| 	return err == nil
 | |
| }
 | |
| 
 | |
| // AddDomain creates a new domain for a given locale object and initializes the Po object.
 | |
| // If the domain exists, it gets reloaded.
 | |
| func (l *Locale) AddDomain(dom string) {
 | |
| 	var poObj Translator
 | |
| 
 | |
| 	file := l.findExt(dom, "po")
 | |
| 	if file != "" {
 | |
| 		poObj = NewPoFS(l.fs)
 | |
| 		// Parse file.
 | |
| 		poObj.ParseFile(file)
 | |
| 	} else {
 | |
| 		file = l.findExt(dom, "mo")
 | |
| 		if file != "" {
 | |
| 			poObj = NewMoFS(l.fs)
 | |
| 			// Parse file.
 | |
| 			poObj.ParseFile(file)
 | |
| 		} else {
 | |
| 			// fallback return if no file found with
 | |
| 			return
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Save new domain
 | |
| 	l.Lock()
 | |
| 
 | |
| 	if l.Domains == nil {
 | |
| 		l.Domains = make(map[string]Translator)
 | |
| 	}
 | |
| 	if l.defaultDomain == "" {
 | |
| 		l.defaultDomain = dom
 | |
| 	}
 | |
| 	l.Domains[dom] = poObj
 | |
| 
 | |
| 	// Unlock "Save new domain"
 | |
| 	l.Unlock()
 | |
| }
 | |
| 
 | |
| // AddTranslator takes a domain name and a Translator object to make it available in the Locale object.
 | |
| func (l *Locale) AddTranslator(dom string, tr Translator) {
 | |
| 	l.Lock()
 | |
| 
 | |
| 	if l.Domains == nil {
 | |
| 		l.Domains = make(map[string]Translator)
 | |
| 	}
 | |
| 	if l.defaultDomain == "" {
 | |
| 		l.defaultDomain = dom
 | |
| 	}
 | |
| 	l.Domains[dom] = tr
 | |
| 
 | |
| 	l.Unlock()
 | |
| }
 | |
| 
 | |
| // GetDomain is the domain getter for Locale configuration
 | |
| func (l *Locale) GetDomain() string {
 | |
| 	l.RLock()
 | |
| 	dom := l.defaultDomain
 | |
| 	l.RUnlock()
 | |
| 	return dom
 | |
| }
 | |
| 
 | |
| // SetDomain sets the name for the domain to be used.
 | |
| func (l *Locale) SetDomain(dom string) {
 | |
| 	l.Lock()
 | |
| 	l.defaultDomain = dom
 | |
| 	l.Unlock()
 | |
| }
 | |
| 
 | |
| // GetLanguage is the lang getter for Locale configuration
 | |
| func (l *Locale) GetLanguage() string {
 | |
| 	l.RLock()
 | |
| 	lang := l.lang
 | |
| 	l.RUnlock()
 | |
| 	return lang
 | |
| }
 | |
| 
 | |
| // Get uses a domain "default" to return the corresponding Translation of a given string.
 | |
| // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
 | |
| func (l *Locale) Get(str string, vars ...interface{}) string {
 | |
| 	return l.GetD(l.GetDomain(), str, vars...)
 | |
| }
 | |
| 
 | |
| // GetN retrieves the (N)th plural form of Translation for the given string in the "default" domain.
 | |
| // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
 | |
| func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string {
 | |
| 	return l.GetND(l.GetDomain(), str, plural, n, vars...)
 | |
| }
 | |
| 
 | |
| // GetD returns the corresponding Translation in the given domain for the given string.
 | |
| // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
 | |
| func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
 | |
| 	// Sync read
 | |
| 	l.RLock()
 | |
| 	defer l.RUnlock()
 | |
| 
 | |
| 	if l.Domains != nil {
 | |
| 		if _, ok := l.Domains[dom]; ok {
 | |
| 			if l.Domains[dom] != nil {
 | |
| 				return l.Domains[dom].Get(str, vars...)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return FormatString(str, vars...)
 | |
| }
 | |
| 
 | |
| // GetND retrieves the (N)th plural form of Translation in the given domain for the given string.
 | |
| // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
 | |
| func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
 | |
| 	// Sync read
 | |
| 	l.RLock()
 | |
| 	defer l.RUnlock()
 | |
| 
 | |
| 	if l.Domains != nil {
 | |
| 		if _, ok := l.Domains[dom]; ok {
 | |
| 			if l.Domains[dom] != nil {
 | |
| 				return l.Domains[dom].GetN(str, plural, n, vars...)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Use western default rule (plural > 1) to handle missing domain default result.
 | |
| 	if n == 1 {
 | |
| 		return FormatString(str, vars...)
 | |
| 	}
 | |
| 	return FormatString(plural, vars...)
 | |
| }
 | |
| 
 | |
| // GetC uses a domain "default" to return the corresponding Translation of the given string in the given context.
 | |
| // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
 | |
| func (l *Locale) GetC(str, ctx string, vars ...interface{}) string {
 | |
| 	return l.GetDC(l.GetDomain(), str, ctx, vars...)
 | |
| }
 | |
| 
 | |
| // GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the "default" domain.
 | |
| // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
 | |
| func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
 | |
| 	return l.GetNDC(l.GetDomain(), str, plural, n, ctx, vars...)
 | |
| }
 | |
| 
 | |
| // GetDC returns the corresponding Translation in the given domain for the given string in the given context.
 | |
| // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
 | |
| func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
 | |
| 	// Sync read
 | |
| 	l.RLock()
 | |
| 	defer l.RUnlock()
 | |
| 
 | |
| 	if l.Domains != nil {
 | |
| 		if _, ok := l.Domains[dom]; ok {
 | |
| 			if l.Domains[dom] != nil {
 | |
| 				return l.Domains[dom].GetC(str, ctx, vars...)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return FormatString(str, vars...)
 | |
| }
 | |
| 
 | |
| // GetNDC retrieves the (N)th plural form of Translation in the given domain for the given string in the given context.
 | |
| // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
 | |
| func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
 | |
| 	// Sync read
 | |
| 	l.RLock()
 | |
| 	defer l.RUnlock()
 | |
| 
 | |
| 	if l.Domains != nil {
 | |
| 		if _, ok := l.Domains[dom]; ok {
 | |
| 			if l.Domains[dom] != nil {
 | |
| 				return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Use western default rule (plural > 1) to handle missing domain default result.
 | |
| 	if n == 1 {
 | |
| 		return FormatString(str, vars...)
 | |
| 	}
 | |
| 	return FormatString(plural, vars...)
 | |
| }
 | |
| 
 | |
| // GetTranslations returns a copy of all translations in all domains of this locale. It does not support contexts.
 | |
| func (l *Locale) GetTranslations() map[string]*Translation {
 | |
| 	all := make(map[string]*Translation)
 | |
| 
 | |
| 	l.RLock()
 | |
| 	defer l.RUnlock()
 | |
| 	for _, translator := range l.Domains {
 | |
| 		for msgID, trans := range translator.GetDomain().GetTranslations() {
 | |
| 			all[msgID] = trans
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return all
 | |
| }
 | |
| 
 | |
| // IsTranslated reports whether a string is translated
 | |
| func (l *Locale) IsTranslated(str string) bool {
 | |
| 	return l.IsTranslatedND(l.GetDomain(), str, 0)
 | |
| }
 | |
| 
 | |
| // IsTranslatedN reports whether a plural string is translated
 | |
| func (l *Locale) IsTranslatedN(str string, n int) bool {
 | |
| 	return l.IsTranslatedND(l.GetDomain(), str, n)
 | |
| }
 | |
| 
 | |
| // IsTranslatedD reports whether a domain string is translated
 | |
| func (l *Locale) IsTranslatedD(dom, str string) bool {
 | |
| 	return l.IsTranslatedND(dom, str, 0)
 | |
| }
 | |
| 
 | |
| // IsTranslatedND reports whether a plural domain string is translated
 | |
| func (l *Locale) IsTranslatedND(dom, str string, n int) bool {
 | |
| 	l.RLock()
 | |
| 	defer l.RUnlock()
 | |
| 
 | |
| 	if l.Domains == nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	translator, ok := l.Domains[dom]
 | |
| 	if !ok {
 | |
| 		return false
 | |
| 	}
 | |
| 	return translator.GetDomain().IsTranslatedN(str, n)
 | |
| }
 | |
| 
 | |
| // IsTranslatedC reports whether a context string is translated
 | |
| func (l *Locale) IsTranslatedC(str, ctx string) bool {
 | |
| 	return l.IsTranslatedNDC(l.GetDomain(), str, 0, ctx)
 | |
| }
 | |
| 
 | |
| // IsTranslatedNC reports whether a plural context string is translated
 | |
| func (l *Locale) IsTranslatedNC(str string, n int, ctx string) bool {
 | |
| 	return l.IsTranslatedNDC(l.GetDomain(), str, n, ctx)
 | |
| }
 | |
| 
 | |
| // IsTranslatedDC reports whether a domain context string is translated
 | |
| func (l *Locale) IsTranslatedDC(dom, str, ctx string) bool {
 | |
| 	return l.IsTranslatedNDC(dom, str, 0, ctx)
 | |
| }
 | |
| 
 | |
| // IsTranslatedNDC reports whether a plural domain context string is translated
 | |
| func (l *Locale) IsTranslatedNDC(dom string, str string, n int, ctx string) bool {
 | |
| 	l.RLock()
 | |
| 	defer l.RUnlock()
 | |
| 
 | |
| 	if l.Domains == nil {
 | |
| 		return false
 | |
| 	}
 | |
| 	translator, ok := l.Domains[dom]
 | |
| 	if !ok {
 | |
| 		return false
 | |
| 	}
 | |
| 	return translator.GetDomain().IsTranslatedNC(str, n, ctx)
 | |
| }
 | |
| 
 | |
| // LocaleEncoding is used as intermediary storage to encode Locale objects to Gob.
 | |
| type LocaleEncoding struct {
 | |
| 	Path          string
 | |
| 	Lang          string
 | |
| 	Domains       map[string][]byte
 | |
| 	DefaultDomain string
 | |
| }
 | |
| 
 | |
| // MarshalBinary implements encoding BinaryMarshaler interface
 | |
| func (l *Locale) MarshalBinary() ([]byte, error) {
 | |
| 	obj := new(LocaleEncoding)
 | |
| 	obj.DefaultDomain = l.defaultDomain
 | |
| 	obj.Domains = make(map[string][]byte)
 | |
| 	for k, v := range l.Domains {
 | |
| 		var err error
 | |
| 		obj.Domains[k], err = v.MarshalBinary()
 | |
| 		if err != nil {
 | |
| 			return nil, err
 | |
| 		}
 | |
| 	}
 | |
| 	obj.Lang = l.lang
 | |
| 	obj.Path = l.path
 | |
| 
 | |
| 	var buff bytes.Buffer
 | |
| 	encoder := gob.NewEncoder(&buff)
 | |
| 	err := encoder.Encode(obj)
 | |
| 
 | |
| 	return buff.Bytes(), err
 | |
| }
 | |
| 
 | |
| // UnmarshalBinary implements encoding BinaryUnmarshaler interface
 | |
| func (l *Locale) UnmarshalBinary(data []byte) error {
 | |
| 	buff := bytes.NewBuffer(data)
 | |
| 	obj := new(LocaleEncoding)
 | |
| 
 | |
| 	decoder := gob.NewDecoder(buff)
 | |
| 	err := decoder.Decode(obj)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	l.defaultDomain = obj.DefaultDomain
 | |
| 	l.lang = obj.Lang
 | |
| 	l.path = obj.Path
 | |
| 
 | |
| 	// Decode Domains
 | |
| 	l.Domains = make(map[string]Translator)
 | |
| 	for k, v := range obj.Domains {
 | |
| 		var tr TranslatorEncoding
 | |
| 		buff := bytes.NewBuffer(v)
 | |
| 		trDecoder := gob.NewDecoder(buff)
 | |
| 		err := trDecoder.Decode(&tr)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		l.Domains[k] = tr.GetTranslator()
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 |