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
 | 
						|
}
 |