diff --git a/gotext.go b/gotext.go index b25413c..4cb0df5 100644 --- a/gotext.go +++ b/gotext.go @@ -23,6 +23,7 @@ For quick/simple translations you can use the package level functions directly. package gotext import ( + "encoding/gob" "sync" ) @@ -45,14 +46,17 @@ type config struct { var globalConfig *config -// Init default configuration func init() { + // Init default configuration globalConfig = &config{ domain: "default", language: "en_US", library: "/usr/local/share/locale", storage: nil, } + + // Register Translator types for gob encoding + gob.Register(TranslatorEncoding{}) } // loadStorage creates a new Locale object at package level based on the Global variables settings. diff --git a/locale.go b/locale.go index 4d47f4e..195b7d5 100644 --- a/locale.go +++ b/locale.go @@ -6,6 +6,8 @@ package gotext import ( + "bytes" + "encoding/gob" "os" "path" "sync" @@ -19,6 +21,8 @@ multiple languages at the same time by working with this object. Example: import ( + "encoding/gob" + "bytes" "fmt" "github.com/leonelquinteros/gotext" ) @@ -236,3 +240,65 @@ func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...inte // Return the same we received by default return Printf(plural, vars...) } + +// LocaleEncoding is used as intermediary storage to encode Locale objects to Gob. +type LocaleEncoding struct { + Path string + Lang string + Domains map[string][]byte + DefaultDomain string +} + +// MarshalBinary implements encoding BinaryMarshaler interface +func (l *Locale) MarshalBinary() ([]byte, error) { + obj := new(LocaleEncoding) + obj.DefaultDomain = l.defaultDomain + obj.Domains = make(map[string][]byte) + for k, v := range l.Domains { + var err error + obj.Domains[k], err = v.MarshalBinary() + if err != nil { + return nil, err + } + } + obj.Lang = l.lang + obj.Path = l.path + + var buff bytes.Buffer + encoder := gob.NewEncoder(&buff) + err := encoder.Encode(obj) + + return buff.Bytes(), err +} + +// UnmarshalBinary implements encoding BinaryUnmarshaler interface +func (l *Locale) UnmarshalBinary(data []byte) error { + buff := bytes.NewBuffer(data) + obj := new(LocaleEncoding) + + decoder := gob.NewDecoder(buff) + err := decoder.Decode(obj) + if err != nil { + return err + } + + l.defaultDomain = obj.DefaultDomain + l.lang = obj.Lang + l.path = obj.Path + + // Decode Domains + l.Domains = make(map[string]Translator) + for k, v := range obj.Domains { + var tr TranslatorEncoding + buff := bytes.NewBuffer(v) + trDecoder := gob.NewDecoder(buff) + err := trDecoder.Decode(&tr) + if err != nil { + return err + } + + l.Domains[k] = tr.GetTranslator() + } + + return nil +} diff --git a/locale_test.go b/locale_test.go index 10cd1c1..11b6350 100644 --- a/locale_test.go +++ b/locale_test.go @@ -485,3 +485,42 @@ func TestArabicTranslation(t *testing.T) { t.Errorf("Expected to get 'الكحول والتبغ', but got '%s'", tr) } } + +func TestLocaleBinaryEncoding(t *testing.T) { + // Create Locale + l := NewLocale("fixtures/", "en_US") + l.AddDomain("default") + + buff, err := l.MarshalBinary() + if err != nil { + t.Fatal(err) + } + + l2 := new(Locale) + err = l2.UnmarshalBinary(buff) + if err != nil { + t.Fatal(err) + } + + // Check object properties + if l.path != l2.path { + t.Fatalf("path doesn't match: '%s' vs '%s'", l.path, l2.path) + } + if l.lang != l2.lang { + t.Fatalf("lang doesn't match: '%s' vs '%s'", l.lang, l2.lang) + } + if l.defaultDomain != l2.defaultDomain { + t.Fatalf("defaultDomain doesn't match: '%s' vs '%s'", l.defaultDomain, l2.defaultDomain) + } + + // Check translations + if l.Get("My text") != l2.Get("My text") { + t.Errorf("'%s' is different from '%s", l.Get("My text"), l2.Get("My text")) + } + if l.Get("More") != l2.Get("More") { + t.Errorf("'%s' is different from '%s", l.Get("More"), l2.Get("More")) + } + if l.GetN("One with var: %s", "Several with vars: %s", 3, "VALUE") != l2.GetN("One with var: %s", "Several with vars: %s", 3, "VALUE") { + t.Errorf("'%s' is different from '%s", l.GetN("One with var: %s", "Several with vars: %s", 3, "VALUE"), l2.GetN("One with var: %s", "Several with vars: %s", 3, "VALUE")) + } +} diff --git a/mo.go b/mo.go index f871f8a..deb21ab 100644 --- a/mo.go +++ b/mo.go @@ -9,6 +9,7 @@ import ( "bufio" "bytes" "encoding/binary" + "encoding/gob" "io/ioutil" "net/textproto" "os" @@ -425,3 +426,47 @@ func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) } return Printf(plural, vars...) } + +// MarshalBinary implements encoding.BinaryMarshaler interface +func (mo *Mo) MarshalBinary() ([]byte, error) { + obj := new(TranslatorEncoding) + obj.Headers = mo.Headers + obj.Language = mo.Language + obj.PluralForms = mo.PluralForms + obj.Nplurals = mo.nplurals + obj.Plural = mo.plural + obj.Translations = mo.translations + obj.Contexts = mo.contexts + + var buff bytes.Buffer + encoder := gob.NewEncoder(&buff) + err := encoder.Encode(obj) + + return buff.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler interface +func (mo *Mo) UnmarshalBinary(data []byte) error { + buff := bytes.NewBuffer(data) + obj := new(TranslatorEncoding) + + decoder := gob.NewDecoder(buff) + err := decoder.Decode(obj) + if err != nil { + return err + } + + mo.Headers = obj.Headers + mo.Language = obj.Language + mo.PluralForms = obj.PluralForms + mo.nplurals = obj.Nplurals + mo.plural = obj.Plural + mo.translations = obj.Translations + mo.contexts = obj.Contexts + + if expr, err := plurals.Compile(mo.plural); err == nil { + mo.pluralforms = expr + } + + return nil +} diff --git a/mo_test.go b/mo_test.go index a3939c3..5a9f627 100644 --- a/mo_test.go +++ b/mo_test.go @@ -202,3 +202,33 @@ func TestNewMoTranslatorRace(t *testing.T) { <-pc <-rc } + +func TestMoBinaryEncoding(t *testing.T) { + // Create mo objects + mo := new(Mo) + mo2 := new(Mo) + + // Parse file + mo.ParseFile("fixtures/en_US/default.mo") + + buff, err := mo.MarshalBinary() + if err != nil { + t.Fatal(err) + } + + err = mo2.UnmarshalBinary(buff) + if err != nil { + t.Fatal(err) + } + + // Test translations + tr := mo2.Get("My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text' but got '%s'", tr) + } + // Test translations + tr = mo2.Get("language") + if tr != "en_US" { + t.Errorf("Expected 'en_US' but got '%s'", tr) + } +} diff --git a/po.go b/po.go index 53caa37..4754924 100644 --- a/po.go +++ b/po.go @@ -7,6 +7,8 @@ package gotext import ( "bufio" + "bytes" + "encoding/gob" "io/ioutil" "net/textproto" "os" @@ -451,3 +453,47 @@ func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) } return Printf(plural, vars...) } + +// MarshalBinary implements encoding.BinaryMarshaler interface +func (po *Po) MarshalBinary() ([]byte, error) { + obj := new(TranslatorEncoding) + obj.Headers = po.Headers + obj.Language = po.Language + obj.PluralForms = po.PluralForms + obj.Nplurals = po.nplurals + obj.Plural = po.plural + obj.Translations = po.translations + obj.Contexts = po.contexts + + var buff bytes.Buffer + encoder := gob.NewEncoder(&buff) + err := encoder.Encode(obj) + + return buff.Bytes(), err +} + +// UnmarshalBinary implements encoding.BinaryUnmarshaler interface +func (po *Po) UnmarshalBinary(data []byte) error { + buff := bytes.NewBuffer(data) + obj := new(TranslatorEncoding) + + decoder := gob.NewDecoder(buff) + err := decoder.Decode(obj) + if err != nil { + return err + } + + po.Headers = obj.Headers + po.Language = obj.Language + po.PluralForms = obj.PluralForms + po.nplurals = obj.Nplurals + po.plural = obj.Plural + po.translations = obj.Translations + po.contexts = obj.Contexts + + if expr, err := plurals.Compile(po.plural); err == nil { + po.pluralforms = expr + } + + return nil +} diff --git a/po_test.go b/po_test.go index 51b3cdf..24830f4 100644 --- a/po_test.go +++ b/po_test.go @@ -9,11 +9,9 @@ import ( "os" "path" "testing" - ) func TestPo_Get(t *testing.T) { - // Create po object po := new(Po) @@ -589,7 +587,6 @@ msgstr[2] "And this is the second plural form: %s" } func TestNewPoTranslatorRace(t *testing.T) { - // Create Po object mo := NewPoTranslator() @@ -617,3 +614,33 @@ func TestNewPoTranslatorRace(t *testing.T) { <-pc <-rc } + +func TestPoBinaryEncoding(t *testing.T) { + // Create po objects + po := new(Po) + po2 := new(Po) + + // Parse file + po.ParseFile("fixtures/en_US/default.po") + + buff, err := po.MarshalBinary() + if err != nil { + t.Fatal(err) + } + + err = po2.UnmarshalBinary(buff) + if err != nil { + t.Fatal(err) + } + + // Test translations + tr := po2.Get("My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text' but got '%s'", tr) + } + // Test translations + tr = po2.Get("language") + if tr != "en_US" { + t.Errorf("Expected 'en_US' but got '%s'", tr) + } +} diff --git a/translator.go b/translator.go index aa8687b..982a600 100644 --- a/translator.go +++ b/translator.go @@ -5,8 +5,11 @@ package gotext +import "net/textproto" + // Translator interface is used by Locale and Po objects.Translator // It contains all methods needed to parse translation sources and obtain corresponding translations. +// Also implements gob.GobEncoder/gob.DobDecoder interfaces to allow serialization of Locale objects. type Translator interface { ParseFile(f string) Parse(buf []byte) @@ -14,4 +17,43 @@ type Translator interface { GetN(str, plural string, n int, vars ...interface{}) string GetC(str, ctx string, vars ...interface{}) string GetNC(str, plural string, n int, ctx string, vars ...interface{}) string + + MarshalBinary() ([]byte, error) + UnmarshalBinary([]byte) error +} + +// TranslatorEncoding is used as intermediary storage to encode Translator objects to Gob. +type TranslatorEncoding struct { + // Headers storage + Headers textproto.MIMEHeader + + // Language header + Language string + + // Plural-Forms header + PluralForms string + + // Parsed Plural-Forms header values + Nplurals int + Plural string + + // Storage + Translations map[string]*Translation + Contexts map[string]map[string]*Translation +} + +// GetTranslator is used to recover a Translator object after unmarshaling the TranslatorEncoding object. +// Internally uses a Po object as it should be switcheable with Mo objects without problem. +// External Translator implementations should be able to serialize into a TranslatorEncoding object in order to unserialize into a Po-compatible object. +func (te *TranslatorEncoding) GetTranslator() Translator { + po := new(Po) + po.Headers = te.Headers + po.Language = te.Language + po.PluralForms = te.PluralForms + po.nplurals = te.Nplurals + po.plural = te.Plural + po.translations = te.Translations + po.contexts = te.Contexts + + return po }