forked from jshiffer/matterbridge
316 lines
9.0 KiB
Go
316 lines
9.0 KiB
Go
|
// Package bundle manages translations for multiple languages.
|
||
|
package bundle
|
||
|
|
||
|
import (
|
||
|
"encoding/json"
|
||
|
"fmt"
|
||
|
"gopkg.in/yaml.v2"
|
||
|
"io/ioutil"
|
||
|
"reflect"
|
||
|
|
||
|
"path/filepath"
|
||
|
|
||
|
"github.com/nicksnyder/go-i18n/i18n/language"
|
||
|
"github.com/nicksnyder/go-i18n/i18n/translation"
|
||
|
)
|
||
|
|
||
|
// 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
|
||
|
}
|
||
|
|
||
|
// 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) {
|
||
|
var unmarshalFunc func([]byte, interface{}) error
|
||
|
switch format := filepath.Ext(filename); format {
|
||
|
case ".json":
|
||
|
unmarshalFunc = json.Unmarshal
|
||
|
case ".yaml":
|
||
|
unmarshalFunc = yaml.Unmarshal
|
||
|
default:
|
||
|
return nil, fmt.Errorf("unsupported file extension %s", format)
|
||
|
}
|
||
|
|
||
|
var translationsData []map[string]interface{}
|
||
|
if len(buf) > 0 {
|
||
|
if err := unmarshalFunc(buf, &translationsData); err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
}
|
||
|
|
||
|
translations := make([]translation.Translation, 0, len(translationsData))
|
||
|
for i, translationData := range translationsData {
|
||
|
t, err := translation.NewTranslation(translationData)
|
||
|
if err != nil {
|
||
|
return nil, fmt.Errorf("unable to parse translation #%d in %s because %s\n%v", i, filename, err, translationData)
|
||
|
}
|
||
|
translations = append(translations, t)
|
||
|
}
|
||
|
return translations, nil
|
||
|
}
|
||
|
|
||
|
// 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) {
|
||
|
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 {
|
||
|
return b.translations
|
||
|
}
|
||
|
|
||
|
// LanguageTags returns the tags of all languages that that have been added.
|
||
|
func (b *Bundle) LanguageTags() []string {
|
||
|
var tags []string
|
||
|
for k := range b.translations {
|
||
|
tags = append(tags, k)
|
||
|
}
|
||
|
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
|
||
|
for id := range b.translations[languageTag] {
|
||
|
ids = append(ids, id)
|
||
|
}
|
||
|
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)
|
||
|
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
|
||
|
}
|
||
|
|
||
|
translations := b.translations[lang.Tag]
|
||
|
if translations == nil {
|
||
|
translations = b.fallbackTranslations[lang.Tag]
|
||
|
if translations == nil {
|
||
|
return translationID
|
||
|
}
|
||
|
}
|
||
|
|
||
|
translation := translations[translationID]
|
||
|
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
|
||
|
}
|
||
|
}
|
||
|
|
||
|
p, _ := lang.Plural(count)
|
||
|
template := translation.Template(p)
|
||
|
if template == nil {
|
||
|
return translationID
|
||
|
}
|
||
|
|
||
|
s := template.Execute(data)
|
||
|
if s == "" {
|
||
|
return translationID
|
||
|
}
|
||
|
return s
|
||
|
}
|
||
|
|
||
|
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
|
||
|
}
|