diff options
Diffstat (limited to 'vendor/github.com/nicksnyder/go-i18n/i18n/bundle/bundle.go')
-rw-r--r-- | vendor/github.com/nicksnyder/go-i18n/i18n/bundle/bundle.go | 315 |
1 files changed, 315 insertions, 0 deletions
diff --git a/vendor/github.com/nicksnyder/go-i18n/i18n/bundle/bundle.go b/vendor/github.com/nicksnyder/go-i18n/i18n/bundle/bundle.go new file mode 100644 index 00000000..e93db95d --- /dev/null +++ b/vendor/github.com/nicksnyder/go-i18n/i18n/bundle/bundle.go @@ -0,0 +1,315 @@ +// 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 +} |