Skip to content

Commit

Permalink
Moved default & supported langs into the storage
Browse files Browse the repository at this point in the history
This changes the way a router is created, but it also means that supported/default languages are synced for all routers using a storage.
  • Loading branch information
Adam Talbot committed Dec 20, 2015
1 parent 9c03164 commit f88521a
Show file tree
Hide file tree
Showing 13 changed files with 616 additions and 92 deletions.
10 changes: 1 addition & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,7 @@ func main() {
translations: translations,
}

languageRouter := &i18n.Router{
DefaultLanguage: language.English,
SupportedLanguages: []language.Tag{
language.English,
language.Spanish,
language.BritishEnglish,
},
Handler: defaultHandler,
}
languageRouter := NewRouter(defaultHandler, translations)

http.ListenAndServe(":8080", languageRouter)
}
Expand Down
101 changes: 101 additions & 0 deletions i18n.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,94 @@ type I18n struct {
storage []Storage
translations *Cache

defaultLanguage language.Tag
supportedLanguages []language.Tag

quit chan struct{}
}

// New translation manager
func New(storage ...Storage) *I18n {
if len(storage) == 0 {
storage = []Storage{NewInMemoryStorage()}
}

return &I18n{
storage: storage,
translations: new(Cache),
}
}

func (i18n *I18n) AddSupportedLanguage(tags ...language.Tag) error {
i18n.lock.Lock()
defer i18n.lock.Unlock()

TagLoop:
for _, tag := range tags {

for _, s := range i18n.storage {
err := s.StoreSupportedLanguage(tag)
if err != nil {
return err
}
}

for _, lang := range i18n.supportedLanguages {
if lang.String() == tag.String() {
continue TagLoop
}
}
i18n.supportedLanguages = append(i18n.supportedLanguages, tag)
}
return nil
}

func (i18n *I18n) GetSupportedLanguages() []language.Tag {
return i18n.supportedLanguages
}

func (i18n *I18n) GetDefaultLanguage() language.Tag {
return i18n.defaultLanguage
}

func (i18n *I18n) RemoveSupportedLanguage(tag language.Tag) error {
i18n.lock.Lock()
defer i18n.lock.Unlock()

for _, s := range i18n.storage {
err := s.DeleteSupportedLanguage(tag)
if err != nil {
return err
}
}

for i, lang := range i18n.supportedLanguages {
if lang.String() == tag.String() {
i18n.supportedLanguages = append(i18n.supportedLanguages[:i], i18n.supportedLanguages[i+1:]...)
}
}

return nil
}

func (i18n *I18n) SetDefaultLanguage(tag language.Tag) error {
i18n.AddSupportedLanguage(tag)

i18n.lock.Lock()
defer i18n.lock.Unlock()

for _, s := range i18n.storage {
err := s.SetDefaultLanguage(tag)
if err != nil {
return err
}
}

i18n.defaultLanguage = tag

return nil
}

// Sync translations with database
func (i18n *I18n) Sync() error {
i18n.lock.Lock()
Expand All @@ -51,6 +128,30 @@ func (i18n *I18n) Sync() error {
}
}

for _, s := range i18n.storage {
tags, err := s.SupportedLanguages()
if err != nil {
return err
}

TagLoop:
for _, tag := range tags {
for _, lang := range i18n.supportedLanguages {
if lang.String() == tag.String() {
continue TagLoop
}
}
i18n.supportedLanguages = append(i18n.supportedLanguages, tag)
}
}

if len(i18n.storage) > 0 {
def, err := i18n.storage[0].DefaultLanguage()
if err != nil {
return err
}
i18n.defaultLanguage = def
}
return nil
}

Expand Down
22 changes: 14 additions & 8 deletions router.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,15 @@ func GetLanguage(r *http.Request) language.Tag {
}

type Router struct {
DefaultLanguage language.Tag
SupportedLanguages []language.Tag
Handler http.Handler
i18n *I18n
handler http.Handler
}

func NewRouter(handler http.Handler, i18n *I18n) *Router {
return &Router{
handler: handler,
i18n: i18n,
}
}

func (router *Router) match(lang string) (matched language.Tag, match bool, valid bool, exact bool) {
Expand All @@ -47,7 +53,7 @@ func (router *Router) match(lang string) (matched language.Tag, match bool, vali
valid = true

for {
for _, supported := range router.SupportedLanguages {
for _, supported := range router.i18n.GetSupportedLanguages() {
if supported.String() == tag.String() {
matched = supported
match = true
Expand Down Expand Up @@ -87,7 +93,7 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {

if !match {
tags, _, _ := language.ParseAcceptLanguage(r.Header.Get("Accept-Language"))
tag = router.DefaultLanguage
tag = router.i18n.GetDefaultLanguage()

for _, t := range tags {
a, b, _, c := router.match(t.String())
Expand Down Expand Up @@ -124,9 +130,9 @@ func (router *Router) ServeHTTP(w http.ResponseWriter, r *http.Request) {
languages.Unlock()
}()

if router.Handler == nil {
router.Handler = http.NotFoundHandler()
if router.handler == nil {
router.handler = http.NotFoundHandler()
}

http.StripPrefix(strings.Join(segments[:2], "/"), router.Handler).ServeHTTP(w, r)
http.StripPrefix(strings.Join(segments[:2], "/"), router.handler).ServeHTTP(w, r)
}
19 changes: 7 additions & 12 deletions router_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,11 @@ func TestRouter(t *testing.T) {

Convey("Given a server handling language selection", t, func() {
handler := &TestHandler{}

r := &Router{
DefaultLanguage: language.English,
SupportedLanguages: []language.Tag{
language.English,
language.Spanish,
language.BritishEnglish,
},
Handler: handler,
}
i18n := New()
r := NewRouter(handler, i18n)
i18n.AddSupportedLanguage(language.English, language.Spanish, language.BritishEnglish, language.French)
i18n.RemoveSupportedLanguage(language.French)
i18n.SetDefaultLanguage(language.English)

server := httptest.NewServer(r)

Expand Down Expand Up @@ -67,7 +62,7 @@ func TestRouter(t *testing.T) {
So(err, ShouldBeNil)

Convey("Then the default lang and correct path should be forwarded to the handler", func() {
So(handler.Tag, ShouldResemble, r.DefaultLanguage)
So(handler.Tag, ShouldResemble, i18n.GetDefaultLanguage())
So(handler.Request.URL.Path, ShouldEqual, "/other")
})
})
Expand All @@ -78,7 +73,7 @@ func TestRouter(t *testing.T) {
So(err, ShouldBeNil)

Convey("Then the default lang and correct path should be forwarded to the handler", func() {
So(handler.Tag, ShouldResemble, r.DefaultLanguage)
So(handler.Tag, ShouldResemble, i18n.GetDefaultLanguage())
So(handler.Request.URL.Path, ShouldEqual, "/other")
})
})
Expand Down
73 changes: 72 additions & 1 deletion storage.go
Original file line number Diff line number Diff line change
@@ -1,24 +1,95 @@
package i18n

import "sync"
import (
"sync"

"golang.org/x/text/language"
)

// Storage interface
type Storage interface {
GetAll() ([]*Translation, error)
Store(*Translation) error
Delete(*Translation) error

DefaultLanguage() (language.Tag, error)
SupportedLanguages() ([]language.Tag, error)

SetDefaultLanguage(language.Tag) error
StoreSupportedLanguage(language.Tag) error
DeleteSupportedLanguage(language.Tag) error
}

type inMemoryStorage struct {
lock sync.RWMutex
translations []*Translation

defaultLang language.Tag
supportedLangs []language.Tag
}

// NewInMemoryStorage Creates a non persistent in memory translation store
func NewInMemoryStorage() Storage {
return new(inMemoryStorage)
}

func (storage *inMemoryStorage) SupportedLanguages() ([]language.Tag, error) {
storage.lock.RLock()
defer storage.lock.RUnlock()

return storage.supportedLangs, nil
}

func (storage *inMemoryStorage) DefaultLanguage() (language.Tag, error) {
storage.lock.RLock()
defer storage.lock.RUnlock()

return storage.defaultLang, nil
}

func (storage *inMemoryStorage) StoreSupportedLanguage(tag language.Tag) error {
storage.lock.Lock()
defer storage.lock.Unlock()

for _, l := range storage.supportedLangs {
if l.String() == tag.String() {
return nil
}
}

storage.supportedLangs = append(storage.supportedLangs, tag)

return nil
}

func (storage *inMemoryStorage) DeleteSupportedLanguage(tag language.Tag) error {
storage.lock.Lock()
defer storage.lock.Unlock()

for i, l := range storage.supportedLangs {
if l.String() == tag.String() {
storage.supportedLangs = append(storage.supportedLangs[:i], storage.supportedLangs[i+1:]...)
return nil
}
}

return nil
}

func (storage *inMemoryStorage) SetDefaultLanguage(tag language.Tag) error {
err := storage.StoreSupportedLanguage(tag)
if err != nil {
return err
}

storage.lock.Lock()
defer storage.lock.Unlock()

storage.defaultLang = tag

return nil
}

func (storage *inMemoryStorage) GetAll() ([]*Translation, error) {
storage.lock.RLock()
defer storage.lock.RUnlock()
Expand Down
33 changes: 33 additions & 0 deletions storage/redis/encode.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package redis

import (
"encoding/json"

"github.com/ThatsMrTalbot/i18n"
"golang.org/x/text/language"
)

type translationObject struct {
Lang string `json:"lang"`
Key string `json:"key"`
Value string `json:"value"`
}

func encode(t *i18n.Translation) string {
data, _ := json.Marshal(&translationObject{
Lang: t.Lang.String(),
Key: t.Key,
Value: t.Value,
})
return string(data)
}

func decode(t string) (*i18n.Translation, error) {
var obj translationObject
err := json.Unmarshal([]byte(t), &obj)
return &i18n.Translation{
Lang: language.Make(obj.Lang),
Key: obj.Key,
Value: obj.Value,
}, err
}
Loading

0 comments on commit f88521a

Please sign in to comment.