Skip to content

Commit

Permalink
Fixes #54 - make bundle safe for concurrent map reads and writes
Browse files Browse the repository at this point in the history
  • Loading branch information
emosbaugh authored and nicksnyder committed Dec 5, 2016
1 parent 991e81c commit f757c9f
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 13 deletions.
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

0 comments on commit f757c9f

Please sign in to comment.