Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes #54 - make bundle safe for concurrent map reads and writes #59

Merged
merged 1 commit into from
Dec 5, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 37 additions & 13 deletions i18n/bundle/bundle.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ package bundle
import (
"encoding/json"
"fmt"
"gopkg.in/yaml.v2"
"io/ioutil"
"reflect"

"path/filepath"
"reflect"
"sync"

"github.com/nicksnyder/go-i18n/i18n/language"
"github.com/nicksnyder/go-i18n/i18n/translation"
"gopkg.in/yaml.v2"
)

// TranslateFunc is a copy of i18n.TranslateFunc to avoid a circular dependency.
Expand All @@ -24,6 +24,8 @@ type Bundle struct {

// Translations that can be used when an exact language match is not possible.
fallbackTranslations map[string]map[string]translation.Translation

sync.RWMutex
}

// New returns an empty bundle.
Expand Down Expand Up @@ -108,6 +110,8 @@ func parseTranslations(filename string, buf []byte) ([]translation.Translation,
//
// It is useful if your translations are in a format not supported by LoadTranslationFile.
func (b *Bundle) AddTranslation(lang *language.Language, translations ...translation.Translation) {
b.Lock()
defer b.Unlock()
if b.translations[lang.Tag] == nil {
b.translations[lang.Tag] = make(map[string]translation.Translation, len(translations))
}
Expand All @@ -128,24 +132,37 @@ func (b *Bundle) AddTranslation(lang *language.Language, translations ...transla

// Translations returns all translations in the bundle.
func (b *Bundle) Translations() map[string]map[string]translation.Translation {
return b.translations
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
}

// LanguageTags returns the tags of all languages that that have been added.
func (b *Bundle) LanguageTags() []string {
var tags []string
b.RLock()
for k := range b.translations {
tags = append(tags, k)
}
b.RUnlock()
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
b.RLock()
for id := range b.translations[languageTag] {
ids = append(ids, id)
}
b.RUnlock()
return ids
}

Expand Down Expand Up @@ -212,6 +229,8 @@ func (b *Bundle) supportedLanguage(pref string, prefs ...string) *language.Langu

func (b *Bundle) translatedLanguage(src string) *language.Language {
langs := language.Parse(src)
b.RLock()
defer b.RUnlock()
for _, lang := range langs {
if len(b.translations[lang.Tag]) > 0 ||
len(b.fallbackTranslations[lang.Tag]) > 0 {
Expand All @@ -226,15 +245,7 @@ func (b *Bundle) translate(lang *language.Language, translationID string, args .
return translationID
}

translations := b.translations[lang.Tag]
if translations == nil {
translations = b.fallbackTranslations[lang.Tag]
if translations == nil {
return translationID
}
}

translation := translations[translationID]
translation := b.translation(lang, translationID)
if translation == nil {
return translationID
}
Expand Down Expand Up @@ -280,6 +291,19 @@ func (b *Bundle) translate(lang *language.Language, translationID string, args .
return s
}

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

func isNumber(n interface{}) bool {
switch n.(type) {
case int, int8, int16, int32, int64, string:
Expand Down
55 changes: 55 additions & 0 deletions i18n/bundle/bundle_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package bundle

import (
"fmt"
"strconv"
"sync"
"testing"

"reflect"
Expand Down Expand Up @@ -160,6 +162,59 @@ func TestTfuncAndLanguage(t *testing.T) {
}
}

func TestConcurrent(t *testing.T) {
b := New()
// bootstrap bundle
translationID := "translation_id" // +1
englishLanguage := languageWithTag("en-US")
addFakeTranslation(t, b, englishLanguage, translationID)

tf, err := b.Tfunc(englishLanguage.Tag)
if err != nil {
t.Errorf("Tfunc(%v) = error{%q}; expected no error", []string{englishLanguage.Tag}, err)
}

const iterations = 1000
var wg sync.WaitGroup
wg.Add(iterations)

// Using go routines insert 1000 ints into our map.
go func() {
for i := 0; i < iterations/2; i++ {
// Add item to map.
translationID := strconv.FormatInt(int64(i), 10)
addFakeTranslation(t, b, englishLanguage, translationID)

// Retrieve item from map.
tf(translationID)

wg.Done()
} // Call go routine with current index.
}()

go func() {
for i := iterations / 2; i < iterations; i++ {
// Add item to map.
translationID := strconv.FormatInt(int64(i), 10)
addFakeTranslation(t, b, englishLanguage, translationID)

// Retrieve item from map.
tf(translationID)

wg.Done()
} // Call go routine with current index.
}()

// Wait for all go routines to finish.
wg.Wait()

// Make sure map contains 1000+1 elements.
count := len(b.Translations()[englishLanguage.Tag])
if count != iterations+1 {
t.Error("Expecting 1001 elements, got", count)
}
}

func addFakeTranslation(t *testing.T, b *Bundle, lang *language.Language, translationID string) string {
translation := fakeTranslation(lang, translationID)
b.AddTranslation(lang, testNewTranslation(t, map[string]interface{}{
Expand Down