matterbridge/vendor/github.com/mattermost/go-i18n/i18n/bundle/bundle.go

454 lines
13 KiB
Go
Raw Permalink Normal View History

2016-04-10 14:39:38 -07:00
// Package bundle manages translations for multiple languages.
package bundle
import (
2020-08-09 15:29:54 -07:00
"bytes"
2016-04-10 14:39:38 -07:00
"encoding/json"
"fmt"
"io/ioutil"
"path/filepath"
2020-08-09 15:29:54 -07:00
"reflect"
"sync"
"unicode"
2016-04-10 14:39:38 -07:00
2020-08-09 15:29:54 -07:00
"github.com/mattermost/go-i18n/i18n/language"
"github.com/mattermost/go-i18n/i18n/translation"
toml "github.com/pelletier/go-toml"
"gopkg.in/yaml.v2"
2016-04-10 14:39:38 -07:00
)
// TranslateFunc is a copy of i18n.TranslateFunc to avoid a circular dependency.
type TranslateFunc func(translationID string, args ...interface{}) string
// Bundle stores the translations for multiple languages.
type Bundle struct {
// The primary translations for a language tag and translation id.
translations map[string]map[string]translation.Translation
// Translations that can be used when an exact language match is not possible.
fallbackTranslations map[string]map[string]translation.Translation
2020-08-09 15:29:54 -07:00
sync.RWMutex
2016-04-10 14:39:38 -07:00
}
// New returns an empty bundle.
func New() *Bundle {
return &Bundle{
translations: make(map[string]map[string]translation.Translation),
fallbackTranslations: make(map[string]map[string]translation.Translation),
}
}
// MustLoadTranslationFile is similar to LoadTranslationFile
// except it panics if an error happens.
func (b *Bundle) MustLoadTranslationFile(filename string) {
if err := b.LoadTranslationFile(filename); err != nil {
panic(err)
}
}
// LoadTranslationFile loads the translations from filename into memory.
//
// The language that the translations are associated with is parsed from the filename (e.g. en-US.json).
//
// Generally you should load translation files once during your program's initialization.
func (b *Bundle) LoadTranslationFile(filename string) error {
buf, err := ioutil.ReadFile(filename)
if err != nil {
return err
}
return b.ParseTranslationFileBytes(filename, buf)
}
// ParseTranslationFileBytes is similar to LoadTranslationFile except it parses the bytes in buf.
//
// It is useful for parsing translation files embedded with go-bindata.
func (b *Bundle) ParseTranslationFileBytes(filename string, buf []byte) error {
basename := filepath.Base(filename)
langs := language.Parse(basename)
switch l := len(langs); {
case l == 0:
return fmt.Errorf("no language found in %q", basename)
case l > 1:
return fmt.Errorf("multiple languages found in filename %q: %v; expected one", basename, langs)
}
translations, err := parseTranslations(filename, buf)
if err != nil {
return err
}
b.AddTranslation(langs[0], translations...)
return nil
}
func parseTranslations(filename string, buf []byte) ([]translation.Translation, error) {
2020-08-09 15:29:54 -07:00
if len(buf) == 0 {
return []translation.Translation{}, nil
2016-04-10 14:39:38 -07:00
}
2020-08-09 15:29:54 -07:00
ext := filepath.Ext(filename)
// `github.com/pelletier/go-toml` lacks an Unmarshal function,
// so we should parse TOML separately.
if ext == ".toml" {
tree, err := toml.LoadReader(bytes.NewReader(buf))
if err != nil {
2016-04-10 14:39:38 -07:00
return nil, err
}
2020-08-09 15:29:54 -07:00
m := make(map[string]map[string]interface{})
for k, v := range tree.ToMap() {
m[k] = v.(map[string]interface{})
}
return parseFlatFormat(m)
}
// Then parse other formats.
if isStandardFormat(ext, buf) {
var standardFormat []map[string]interface{}
if err := unmarshal(ext, buf, &standardFormat); err != nil {
return nil, fmt.Errorf("failed to unmarshal %v: %v", filename, err)
}
return parseStandardFormat(standardFormat)
}
var flatFormat map[string]map[string]interface{}
if err := unmarshal(ext, buf, &flatFormat); err != nil {
return nil, fmt.Errorf("failed to unmarshal %v: %v", filename, err)
}
return parseFlatFormat(flatFormat)
}
func isStandardFormat(ext string, buf []byte) bool {
buf = deleteLeadingComments(ext, buf)
firstRune := rune(buf[0])
return (ext == ".json" && firstRune == '[') || (ext == ".yaml" && firstRune == '-')
}
// deleteLeadingComments deletes leading newlines and comments in buf.
// It only works for ext == ".yaml".
func deleteLeadingComments(ext string, buf []byte) []byte {
if ext != ".yaml" {
return buf
}
for {
buf = bytes.TrimLeftFunc(buf, unicode.IsSpace)
if buf[0] == '#' {
buf = deleteLine(buf)
} else {
break
}
2016-04-10 14:39:38 -07:00
}
2020-08-09 15:29:54 -07:00
return buf
}
func deleteLine(buf []byte) []byte {
index := bytes.IndexRune(buf, '\n')
if index == -1 { // If there is only one line without newline ...
return nil // ... delete it and return nothing.
}
if index == len(buf)-1 { // If there is only one line with newline ...
return nil // ... do the same as above.
}
return buf[index+1:]
}
// unmarshal finds an appropriate unmarshal function for ext
// (extension of filename) and unmarshals buf to out. out must be a pointer.
func unmarshal(ext string, buf []byte, out interface{}) error {
switch ext {
case ".json":
return json.Unmarshal(buf, out)
case ".yaml":
return yaml.Unmarshal(buf, out)
}
return fmt.Errorf("unsupported file extension %v", ext)
}
func parseStandardFormat(data []map[string]interface{}) ([]translation.Translation, error) {
translations := make([]translation.Translation, 0, len(data))
for i, translationData := range data {
2016-04-10 14:39:38 -07:00
t, err := translation.NewTranslation(translationData)
if err != nil {
2020-08-09 15:29:54 -07:00
return nil, fmt.Errorf("unable to parse translation #%d because %s\n%v", i, err, translationData)
2016-04-10 14:39:38 -07:00
}
translations = append(translations, t)
}
return translations, nil
}
2020-08-09 15:29:54 -07:00
// parseFlatFormat just converts data from flat format to standard format
// and passes it to parseStandardFormat.
//
// Flat format logic:
// key of data must be a string and data[key] must be always map[string]interface{},
// but if there is only "other" key in it then it is non-plural, else plural.
func parseFlatFormat(data map[string]map[string]interface{}) ([]translation.Translation, error) {
var standardFormatData []map[string]interface{}
for id, translationData := range data {
dataObject := make(map[string]interface{})
dataObject["id"] = id
if len(translationData) == 1 { // non-plural form
_, otherExists := translationData["other"]
if otherExists {
dataObject["translation"] = translationData["other"]
}
} else { // plural form
dataObject["translation"] = translationData
}
standardFormatData = append(standardFormatData, dataObject)
}
return parseStandardFormat(standardFormatData)
}
2016-04-10 14:39:38 -07:00
// AddTranslation adds translations for a language.
//
// It is useful if your translations are in a format not supported by LoadTranslationFile.
func (b *Bundle) AddTranslation(lang *language.Language, translations ...translation.Translation) {
2020-08-09 15:29:54 -07:00
b.Lock()
defer b.Unlock()
2016-04-10 14:39:38 -07:00
if b.translations[lang.Tag] == nil {
b.translations[lang.Tag] = make(map[string]translation.Translation, len(translations))
}
currentTranslations := b.translations[lang.Tag]
for _, newTranslation := range translations {
if currentTranslation := currentTranslations[newTranslation.ID()]; currentTranslation != nil {
currentTranslations[newTranslation.ID()] = currentTranslation.Merge(newTranslation)
} else {
currentTranslations[newTranslation.ID()] = newTranslation
}
}
// lang can provide translations for less specific language tags.
for _, tag := range lang.MatchingTags() {
b.fallbackTranslations[tag] = currentTranslations
}
}
// Translations returns all translations in the bundle.
func (b *Bundle) Translations() map[string]map[string]translation.Translation {
2020-08-09 15:29:54 -07:00
t := make(map[string]map[string]translation.Translation)
b.RLock()
for tag, translations := range b.translations {
t[tag] = make(map[string]translation.Translation)
for id, translation := range translations {
t[tag][id] = translation
}
}
b.RUnlock()
return t
2016-04-10 14:39:38 -07:00
}
// LanguageTags returns the tags of all languages that that have been added.
func (b *Bundle) LanguageTags() []string {
var tags []string
2020-08-09 15:29:54 -07:00
b.RLock()
2016-04-10 14:39:38 -07:00
for k := range b.translations {
tags = append(tags, k)
}
2020-08-09 15:29:54 -07:00
b.RUnlock()
2016-04-10 14:39:38 -07:00
return tags
}
// LanguageTranslationIDs returns the ids of all translations that have been added for a given language.
func (b *Bundle) LanguageTranslationIDs(languageTag string) []string {
var ids []string
2020-08-09 15:29:54 -07:00
b.RLock()
2016-04-10 14:39:38 -07:00
for id := range b.translations[languageTag] {
ids = append(ids, id)
}
2020-08-09 15:29:54 -07:00
b.RUnlock()
2016-04-10 14:39:38 -07:00
return ids
}
// MustTfunc is similar to Tfunc except it panics if an error happens.
func (b *Bundle) MustTfunc(pref string, prefs ...string) TranslateFunc {
tfunc, err := b.Tfunc(pref, prefs...)
if err != nil {
panic(err)
}
return tfunc
}
// MustTfuncAndLanguage is similar to TfuncAndLanguage except it panics if an error happens.
func (b *Bundle) MustTfuncAndLanguage(pref string, prefs ...string) (TranslateFunc, *language.Language) {
tfunc, language, err := b.TfuncAndLanguage(pref, prefs...)
if err != nil {
panic(err)
}
return tfunc, language
}
// Tfunc is similar to TfuncAndLanguage except is doesn't return the Language.
func (b *Bundle) Tfunc(pref string, prefs ...string) (TranslateFunc, error) {
tfunc, _, err := b.TfuncAndLanguage(pref, prefs...)
return tfunc, err
}
// TfuncAndLanguage returns a TranslateFunc for the first Language that
// has a non-zero number of translations in the bundle.
//
// The returned Language matches the the first language preference that could be satisfied,
// but this may not strictly match the language of the translations used to satisfy that preference.
//
// For example, the user may request "zh". If there are no translations for "zh" but there are translations
// for "zh-cn", then the translations for "zh-cn" will be used but the returned Language will be "zh".
//
// It can parse languages from Accept-Language headers (RFC 2616),
// but it assumes weights are monotonically decreasing.
func (b *Bundle) TfuncAndLanguage(pref string, prefs ...string) (TranslateFunc, *language.Language, error) {
lang := b.supportedLanguage(pref, prefs...)
var err error
if lang == nil {
err = fmt.Errorf("no supported languages found %#v", append(prefs, pref))
}
return func(translationID string, args ...interface{}) string {
return b.translate(lang, translationID, args...)
}, lang, err
}
// supportedLanguage returns the first language which
// has a non-zero number of translations in the bundle.
func (b *Bundle) supportedLanguage(pref string, prefs ...string) *language.Language {
lang := b.translatedLanguage(pref)
if lang == nil {
for _, pref := range prefs {
lang = b.translatedLanguage(pref)
if lang != nil {
break
}
}
}
return lang
}
func (b *Bundle) translatedLanguage(src string) *language.Language {
langs := language.Parse(src)
2020-08-09 15:29:54 -07:00
b.RLock()
defer b.RUnlock()
2016-04-10 14:39:38 -07:00
for _, lang := range langs {
if len(b.translations[lang.Tag]) > 0 ||
len(b.fallbackTranslations[lang.Tag]) > 0 {
return lang
}
}
return nil
}
func (b *Bundle) translate(lang *language.Language, translationID string, args ...interface{}) string {
if lang == nil {
return translationID
}
2020-08-09 15:29:54 -07:00
translation := b.translation(lang, translationID)
2016-04-10 14:39:38 -07:00
if translation == nil {
return translationID
}
var data interface{}
var count interface{}
if argc := len(args); argc > 0 {
if isNumber(args[0]) {
count = args[0]
if argc > 1 {
data = args[1]
}
} else {
data = args[0]
}
}
if count != nil {
if data == nil {
data = map[string]interface{}{"Count": count}
} else {
dataMap := toMap(data)
dataMap["Count"] = count
data = dataMap
}
2020-08-09 15:29:54 -07:00
} else {
dataMap := toMap(data)
if c, ok := dataMap["Count"]; ok {
count = c
}
2016-04-10 14:39:38 -07:00
}
p, _ := lang.Plural(count)
template := translation.Template(p)
if template == nil {
2020-08-09 15:29:54 -07:00
if p == language.Other {
return translationID
}
countInt, ok := count.(int)
if ok && countInt > 1 {
template = translation.Template(language.Other)
}
2016-04-10 14:39:38 -07:00
}
2021-10-30 06:17:50 -07:00
if template == nil {
return translationID
}
2016-04-10 14:39:38 -07:00
s := template.Execute(data)
if s == "" {
return translationID
}
return s
}
2020-08-09 15:29:54 -07:00
func (b *Bundle) translation(lang *language.Language, translationID string) translation.Translation {
b.RLock()
defer b.RUnlock()
translations := b.translations[lang.Tag]
if translations == nil {
translations = b.fallbackTranslations[lang.Tag]
if translations == nil {
return nil
}
}
return translations[translationID]
}
2016-04-10 14:39:38 -07:00
func isNumber(n interface{}) bool {
switch n.(type) {
case int, int8, int16, int32, int64, string:
return true
}
return false
}
func toMap(input interface{}) map[string]interface{} {
if data, ok := input.(map[string]interface{}); ok {
return data
}
v := reflect.ValueOf(input)
switch v.Kind() {
case reflect.Ptr:
return toMap(v.Elem().Interface())
case reflect.Struct:
return structToMap(v)
default:
return nil
}
}
// Converts the top level of a struct to a map[string]interface{}.
// Code inspired by github.com/fatih/structs.
func structToMap(v reflect.Value) map[string]interface{} {
out := make(map[string]interface{})
t := v.Type()
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
if field.PkgPath != "" {
// unexported field. skip.
continue
}
out[field.Name] = v.FieldByName(field.Name).Interface()
}
return out
}