Make Locale and Po objects serializable. Closes #23

This commit is contained in:
Leonel Quinteros 2018-09-07 18:14:38 -03:00
parent 302c88af99
commit 4cbf30d337
8 changed files with 303 additions and 4 deletions

View file

@ -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.

View file

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

View file

@ -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"))
}
}

45
mo.go
View file

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

View file

@ -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)
}
}

46
po.go
View file

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

View file

@ -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)
}
}

View file

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