move common po.go/mo.go code to domain.go

adjust tests to reduce duplication
This commit is contained in:
Jon Snyder 2020-09-10 13:02:24 -07:00
parent 62ce4e85a3
commit bb276626f3
13 changed files with 565 additions and 720 deletions

248
domain.go Normal file
View file

@ -0,0 +1,248 @@
package gotext
import (
"bufio"
"bytes"
"encoding/gob"
"net/textproto"
"strconv"
"strings"
"sync"
"golang.org/x/text/language"
"github.com/leonelquinteros/gotext/plurals"
)
// Domain has all the common functions for dealing with a gettext domain
// it's initialized with a GettextFile (which represents either a Po or Mo file)
type Domain struct {
Headers textproto.MIMEHeader
// Language header
Language string
tag language.Tag
// Plural-Forms header
PluralForms string
// Parsed Plural-Forms header values
nplurals int
plural string
pluralforms plurals.Expression
// Storage
translations map[string]*Translation
contexts map[string]map[string]*Translation
pluralTranslations map[string]*Translation
// Sync Mutex
trMutex sync.RWMutex
pluralMutex sync.RWMutex
// Parsing buffers
trBuffer *Translation
ctxBuffer string
}
func NewDomain() *Domain {
domain := new(Domain)
domain.translations = make(map[string]*Translation)
domain.contexts = make(map[string]map[string]*Translation)
domain.pluralTranslations = make(map[string]*Translation)
return domain
}
func (do *Domain) pluralForm(n int) int {
// do we really need locking here? not sure how this plurals.Expression works, so sticking with it for now
do.pluralMutex.RLock()
defer do.pluralMutex.RUnlock()
// Failure fallback
if do.pluralforms == nil {
/* Use the Germanic plural rule. */
if n == 1 {
return 0
}
return 1
}
return do.pluralforms.Eval(uint32(n))
}
// parseHeaders retrieves data from previously parsed headers. it's called by both Mo and Po when parsing
func (do *Domain) parseHeaders() {
// Make sure we end with 2 carriage returns.
empty := ""
if _, ok := do.translations[empty]; ok {
empty = do.translations[empty].Get()
}
raw := empty + "\n\n"
// Read
reader := bufio.NewReader(strings.NewReader(raw))
tp := textproto.NewReader(reader)
var err error
do.Headers, err = tp.ReadMIMEHeader()
if err != nil {
return
}
// Get/save needed headers
do.Language = do.Headers.Get("Language")
do.tag = language.Make(do.Language)
do.PluralForms = do.Headers.Get("Plural-Forms")
// Parse Plural-Forms formula
if do.PluralForms == "" {
return
}
// Split plural form header value
pfs := strings.Split(do.PluralForms, ";")
// Parse values
for _, i := range pfs {
vs := strings.SplitN(i, "=", 2)
if len(vs) != 2 {
continue
}
switch strings.TrimSpace(vs[0]) {
case "nplurals":
do.nplurals, _ = strconv.Atoi(vs[1])
case "plural":
do.plural = vs[1]
if expr, err := plurals.Compile(do.plural); err == nil {
do.pluralforms = expr
}
}
}
}
func (do *Domain) Get(str string, vars ...interface{}) string {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations != nil {
if _, ok := do.translations[str]; ok {
return Printf(do.translations[str].Get(), vars...)
}
}
// Return the same we received by default
return Printf(str, vars...)
}
// GetN retrieves the (N)th plural form of Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetN(str, plural string, n int, vars ...interface{}) string {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations != nil {
if _, ok := do.translations[str]; ok {
return Printf(do.translations[str].GetN(do.pluralForm(n)), vars...)
}
}
// Parse plural forms to distinguish between plural and singular
if do.pluralForm(n) == 0 {
return Printf(str, vars...)
}
return Printf(plural, vars...)
}
// GetC retrieves the corresponding Translation for a given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetC(str, ctx string, vars ...interface{}) string {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.contexts != nil {
if _, ok := do.contexts[ctx]; ok {
if do.contexts[ctx] != nil {
if _, ok := do.contexts[ctx][str]; ok {
return Printf(do.contexts[ctx][str].Get(), vars...)
}
}
}
}
// Return the string we received by default
return Printf(str, vars...)
}
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.contexts != nil {
if _, ok := do.contexts[ctx]; ok {
if do.contexts[ctx] != nil {
if _, ok := do.contexts[ctx][str]; ok {
return Printf(do.contexts[ctx][str].GetN(do.pluralForm(n)), vars...)
}
}
}
}
if n == 1 {
return Printf(str, vars...)
}
return Printf(plural, vars...)
}
// MarshalBinary implements encoding.BinaryMarshaler interface
func (do *Domain) MarshalBinary() ([]byte, error) {
obj := new(TranslatorEncoding)
obj.Headers = do.Headers
obj.Language = do.Language
obj.PluralForms = do.PluralForms
obj.Nplurals = do.nplurals
obj.Plural = do.plural
obj.Translations = do.translations
obj.Contexts = do.contexts
var buff bytes.Buffer
encoder := gob.NewEncoder(&buff)
err := encoder.Encode(obj)
return buff.Bytes(), err
}
// UnmarshalBinary implements encoding.BinaryUnmarshaler interface
func (do *Domain) UnmarshalBinary(data []byte) error {
buff := bytes.NewBuffer(data)
obj := new(TranslatorEncoding)
decoder := gob.NewDecoder(buff)
err := decoder.Decode(obj)
if err != nil {
return err
}
do.Headers = obj.Headers
do.Language = obj.Language
do.PluralForms = obj.PluralForms
do.nplurals = obj.Nplurals
do.plural = obj.Plural
do.translations = obj.Translations
do.contexts = obj.Contexts
if expr, err := plurals.Compile(do.plural); err == nil {
do.pluralforms = expr
}
return nil
}

34
domain_test.go Normal file
View file

@ -0,0 +1,34 @@
package gotext
import "testing"
//since both Po and Mo just pass-through to Domain for MarshalBinary and UnmarshalBinary, test it here
func TestBinaryEncoding(t *testing.T) {
// Create po objects
po := NewPo()
po2 := NewPo()
// Parse file
po.ParseFile("fixtures/en_US/default.po")
buff, err := po.GetDomain().MarshalBinary()
if err != nil {
t.Fatal(err)
}
err = po2.GetDomain().UnmarshalBinary(buff)
if err != nil {
t.Fatal(err)
}
// Test translations
tr := po2.Get("My text")
if tr != translatedText {
t.Errorf("Expected '%s' but got '%s'", translatedText, tr)
}
// Test translations
tr = po2.Get("language")
if tr != "en_US" {
t.Errorf("Expected 'en_US' but got '%s'", tr)
}
}

5
go.mod
View file

@ -2,6 +2,9 @@ module github.com/leonelquinteros/gotext
// go: no requirements found in Gopkg.lock
require golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd
require (
golang.org/x/text v0.3.0
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd
)
go 1.13

1
go.sum
View file

@ -7,6 +7,7 @@ golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd h1:hHkvGJK23seRCflePJnVa9IMv8fsuavSCWKd11kDQFs=
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=

View file

@ -139,8 +139,8 @@ msgstr "Another text on another domain"
// Test translations
tr := Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
if tr != translatedText {
t.Errorf("Expected '%s' but got '%s'", translatedText, tr)
}
v := "Variable"
@ -252,7 +252,6 @@ msgstr[1] ""
}
func TestMoAndPoTranslator(t *testing.T) {
fixPath, _ := filepath.Abs("./fixtures/")
Configure(fixPath, "en_GB", "default")
@ -260,8 +259,8 @@ func TestMoAndPoTranslator(t *testing.T) {
// Check default domain Translation
SetDomain("default")
tr := Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text'. Got '%s'", tr)
if tr != translatedText {
t.Errorf("Expected '%s'. Got '%s'", translatedText, tr)
}
tr = Get("language")
if tr != "en_GB" {
@ -274,8 +273,8 @@ func TestMoAndPoTranslator(t *testing.T) {
// Check default domain Translation
SetDomain("default")
tr = Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text'. Got '%s'", tr)
if tr != translatedText {
t.Errorf("Expected '%s'. Got '%s'", translatedText, tr)
}
tr = Get("language")
if tr != "en_AU" {

View file

@ -11,17 +11,17 @@ import (
)
func TestSimplifiedLocale(t *testing.T) {
tr :=SimplifiedLocale("de_DE@euro")
tr := SimplifiedLocale("de_DE@euro")
if tr != "de_DE" {
t.Errorf("Expected 'de_DE' but got '%s'", tr)
}
tr =SimplifiedLocale("de_DE.UTF-8")
tr = SimplifiedLocale("de_DE.UTF-8")
if tr != "de_DE" {
t.Errorf("Expected 'de_DE' but got '%s'", tr)
}
tr =SimplifiedLocale("de_DE:latin1")
tr = SimplifiedLocale("de_DE:latin1")
if tr != "de_DE" {
t.Errorf("Expected 'de_DE' but got '%s'", tr)
}
@ -97,10 +97,10 @@ func TestNPrintf(t *testing.T) {
func TestSprintfFloatsWithPrecision(t *testing.T) {
pat := "%(float)f / %(floatprecision).1f / %(long)g / %(longprecision).3g"
params := map[string]interface{}{
"float": 5.034560,
"float": 5.034560,
"floatprecision": 5.03456,
"long": 5.03456,
"longprecision": 5.03456,
"long": 5.03456,
"longprecision": 5.03456,
}
s := Sprintf(pat, params)
@ -109,4 +109,4 @@ func TestSprintfFloatsWithPrecision(t *testing.T) {
if s != expectedresult {
t.Errorf("result should be (%v) but is (%v)", expectedresult, s)
}
}
}

View file

@ -107,13 +107,13 @@ func (l *Locale) AddDomain(dom string) {
file := l.findExt(dom, "po")
if file != "" {
poObj = new(Po)
poObj = NewPo()
// Parse file.
poObj.ParseFile(file)
} else {
file = l.findExt(dom, "mo")
if file != "" {
poObj = new(Mo)
poObj = NewMo()
// Parse file.
poObj.ParseFile(file)
} else {

View file

@ -93,8 +93,8 @@ msgstr "More Translation"
// Test translations
tr := l.GetD("my_domain", "My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
if tr != translatedText {
t.Errorf("Expected '%s' but got '%s'", translatedText, tr)
}
v := "Variable"
@ -474,7 +474,7 @@ msgstr[2] "And this is the second plural form: %s"
func TestAddTranslator(t *testing.T) {
// Create po object
po := new(Po)
po := NewPo()
// Parse file
po.ParseFile("fixtures/en_US/default.po")
@ -487,8 +487,8 @@ func TestAddTranslator(t *testing.T) {
// Test translations
tr := l.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
if tr != translatedText {
t.Errorf("Expected '%s' but got '%s'", translatedText, tr)
}
// Test translations
tr = l.Get("language")

319
mo.go
View file

@ -6,18 +6,9 @@
package gotext
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/gob"
"io/ioutil"
"net/textproto"
"os"
"strconv"
"strings"
"sync"
"github.com/leonelquinteros/gotext/plurals"
)
const (
@ -57,52 +48,52 @@ Example:
*/
type Mo struct {
// Headers storage
Headers textproto.MIMEHeader
// Language header
Language string
// Plural-Forms header
//these three public members are for backwards compatibility. they are just set to the value in the domain
Headers textproto.MIMEHeader
Language string
PluralForms string
// Parsed Plural-Forms header values
nplurals int
plural string
pluralforms plurals.Expression
// Storage
translations map[string]*Translation
contexts map[string]map[string]*Translation
// Sync Mutex
sync.RWMutex
// Parsing buffers
trBuffer *Translation
ctxBuffer string
domain *Domain
}
// NewMoTranslator creates a new Mo object with the Translator interface
func NewMoTranslator() Translator {
return new(Mo)
//NewMo should always be used to instantiate a new Mo object
func NewMo() *Mo {
mo := new(Mo)
mo.domain = NewDomain()
return mo
}
func (mo *Mo) GetDomain() *Domain {
return mo.domain
}
//all of the Get functions are for convenience and aid in backwards compatibility
func (mo *Mo) Get(str string, vars ...interface{}) string {
return mo.domain.Get(str, vars...)
}
func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string {
return mo.domain.GetN(str, plural, n, vars...)
}
func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string {
return mo.domain.GetC(str, ctx, vars...)
}
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return mo.domain.GetNC(str, plural, n, ctx, vars...)
}
func (mo *Mo) MarshalBinary() ([]byte, error) {
return mo.domain.MarshalBinary()
}
func (mo *Mo) UnmarshalBinary(data []byte) error {
return mo.domain.UnmarshalBinary(data)
}
// ParseFile tries to read the file by its provided path (f) and parse its content as a .po file.
func (mo *Mo) ParseFile(f string) {
// Check if file exists
info, err := os.Stat(f)
if err != nil {
return
}
// Check that isn't a directory
if info.IsDir() {
return
}
// Parse file content
data, err := ioutil.ReadFile(f)
data, err := getFileData(f)
if err != nil {
return
}
@ -110,16 +101,13 @@ func (mo *Mo) ParseFile(f string) {
mo.Parse(data)
}
// Parse loads the translations specified in the provided string (str)
// Parse loads the translations specified in the provided byte slice, in the GNU gettext .mo format
func (mo *Mo) Parse(buf []byte) {
// Lock while parsing
mo.Lock()
// Init storage
if mo.translations == nil {
mo.translations = make(map[string]*Translation)
mo.contexts = make(map[string]map[string]*Translation)
}
mo.domain.trMutex.Lock()
mo.domain.pluralMutex.Lock()
defer mo.domain.trMutex.Unlock()
defer mo.domain.pluralMutex.Unlock()
r := bytes.NewReader(buf)
@ -223,13 +211,14 @@ func (mo *Mo) Parse(buf []byte) {
}
}
// Unlock to parse headers
mo.Unlock()
// Parse headers
mo.parseHeaders()
return
// return nil
mo.domain.parseHeaders()
// set values on this struct
// this is for backwards compatibility
mo.Language = mo.domain.Language
mo.PluralForms = mo.domain.PluralForms
mo.Headers = mo.domain.Headers
}
func (mo *Mo) addTranslation(msgid, msgstr []byte) {
@ -266,207 +255,11 @@ func (mo *Mo) addTranslation(msgid, msgstr []byte) {
if len(msgctxt) > 0 {
// With context...
if _, ok := mo.contexts[string(msgctxt)]; !ok {
mo.contexts[string(msgctxt)] = make(map[string]*Translation)
if _, ok := mo.domain.contexts[string(msgctxt)]; !ok {
mo.domain.contexts[string(msgctxt)] = make(map[string]*Translation)
}
mo.contexts[string(msgctxt)][translation.ID] = translation
mo.domain.contexts[string(msgctxt)][translation.ID] = translation
} else {
mo.translations[translation.ID] = translation
mo.domain.translations[translation.ID] = translation
}
}
// parseHeaders retrieves data from previously parsed headers
func (mo *Mo) parseHeaders() {
// Make sure we end with 2 carriage returns.
raw := mo.Get("") + "\n\n"
// Read
reader := bufio.NewReader(strings.NewReader(raw))
tp := textproto.NewReader(reader)
var err error
// Sync Headers write.
mo.Lock()
defer mo.Unlock()
mo.Headers, err = tp.ReadMIMEHeader()
if err != nil {
return
}
// Get/save needed headers
mo.Language = mo.Headers.Get("Language")
mo.PluralForms = mo.Headers.Get("Plural-Forms")
// Parse Plural-Forms formula
if mo.PluralForms == "" {
return
}
// Split plural form header value
pfs := strings.Split(mo.PluralForms, ";")
// Parse values
for _, i := range pfs {
vs := strings.SplitN(i, "=", 2)
if len(vs) != 2 {
continue
}
switch strings.TrimSpace(vs[0]) {
case "nplurals":
mo.nplurals, _ = strconv.Atoi(vs[1])
case "plural":
mo.plural = vs[1]
if expr, err := plurals.Compile(mo.plural); err == nil {
mo.pluralforms = expr
}
}
}
}
// pluralForm calculates the plural form index corresponding to n.
// Returns 0 on error
func (mo *Mo) pluralForm(n int) int {
mo.RLock()
defer mo.RUnlock()
// Failure fallback
if mo.pluralforms == nil {
/* Use the Germanic plural rule. */
if n == 1 {
return 0
}
return 1
}
return mo.pluralforms.Eval(uint32(n))
}
// Get retrieves the corresponding Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (mo *Mo) Get(str string, vars ...interface{}) string {
// Sync read
mo.RLock()
defer mo.RUnlock()
if mo.translations != nil {
if _, ok := mo.translations[str]; ok {
return Printf(mo.translations[str].Get(), vars...)
}
}
// Return the same we received by default
return Printf(str, vars...)
}
// GetN retrieves the (N)th plural form of Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string {
// Sync read
mo.RLock()
defer mo.RUnlock()
if mo.translations != nil {
if _, ok := mo.translations[str]; ok {
return Printf(mo.translations[str].GetN(mo.pluralForm(n)), vars...)
}
}
if n == 1 {
return Printf(str, vars...)
}
return Printf(plural, vars...)
}
// GetC retrieves the corresponding Translation for a given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string {
// Sync read
mo.RLock()
defer mo.RUnlock()
if mo.contexts != nil {
if _, ok := mo.contexts[ctx]; ok {
if mo.contexts[ctx] != nil {
if _, ok := mo.contexts[ctx][str]; ok {
return Printf(mo.contexts[ctx][str].Get(), vars...)
}
}
}
}
// Return the string we received by default
return Printf(str, vars...)
}
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
// Sync read
mo.RLock()
defer mo.RUnlock()
if mo.contexts != nil {
if _, ok := mo.contexts[ctx]; ok {
if mo.contexts[ctx] != nil {
if _, ok := mo.contexts[ctx][str]; ok {
return Printf(mo.contexts[ctx][str].GetN(mo.pluralForm(n)), vars...)
}
}
}
}
if n == 1 {
return Printf(str, vars...)
}
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

@ -12,9 +12,8 @@ import (
)
func TestMo_Get(t *testing.T) {
// Create po object
mo := new(Mo)
// Create mo object
mo := NewMo()
// Try to parse a directory
mo.ParseFile(path.Clean(os.TempDir()))
@ -24,8 +23,8 @@ func TestMo_Get(t *testing.T) {
// Test translations
tr := mo.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
if tr != translatedText {
t.Errorf("Expected '%s' but got '%s'", translatedText, tr)
}
// Test translations
tr = mo.Get("language")
@ -35,9 +34,8 @@ func TestMo_Get(t *testing.T) {
}
func TestMo(t *testing.T) {
// Create po object
mo := new(Mo)
// Create mo object
mo := NewMo()
// Try to parse a directory
mo.ParseFile(path.Clean(os.TempDir()))
@ -47,8 +45,8 @@ func TestMo(t *testing.T) {
// Test translations
tr := mo.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
if tr != translatedText {
t.Errorf("Expected '%s' but got '%s'", translatedText, tr)
}
v := "Variable"
@ -144,9 +142,8 @@ func TestMo(t *testing.T) {
}
func TestMoRace(t *testing.T) {
// Create Po object
mo := new(Mo)
// Create mo object
mo := NewMo()
// Create sync channels
pc := make(chan bool)
@ -176,7 +173,7 @@ func TestMoRace(t *testing.T) {
func TestNewMoTranslatorRace(t *testing.T) {
// Create Po object
mo := NewMoTranslator()
mo := NewMo()
// Create sync channels
pc := make(chan bool)
@ -205,8 +202,8 @@ func TestNewMoTranslatorRace(t *testing.T) {
func TestMoBinaryEncoding(t *testing.T) {
// Create mo objects
mo := new(Mo)
mo2 := new(Mo)
mo := NewMo()
mo2 := NewMo()
// Parse file
mo.ParseFile("fixtures/en_US/default.mo")
@ -223,8 +220,8 @@ func TestMoBinaryEncoding(t *testing.T) {
// Test translations
tr := mo2.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
if tr != translatedText {
t.Errorf("Expected '%s' but got '%s'", translatedText, tr)
}
// Test translations
tr = mo2.Get("language")

349
po.go
View file

@ -6,17 +6,9 @@
package gotext
import (
"bufio"
"bytes"
"encoding/gob"
"io/ioutil"
"net/textproto"
"os"
"strconv"
"strings"
"sync"
"github.com/leonelquinteros/gotext/plurals"
)
/*
@ -44,30 +36,12 @@ Example:
*/
type Po struct {
// Headers storage
Headers textproto.MIMEHeader
// Language header
Language string
// Plural-Forms header
//these three public members are for backwards compatibility. they are just set to the value in the domain
Headers textproto.MIMEHeader
Language string
PluralForms string
// Parsed Plural-Forms header values
nplurals int
plural string
pluralforms plurals.Expression
// Storage
translations map[string]*Translation
contexts map[string]map[string]*Translation
// Sync Mutex
sync.RWMutex
// Parsing buffers
trBuffer *Translation
ctxBuffer string
domain *Domain
}
type parseState int
@ -80,26 +54,45 @@ const (
msgStr
)
// NewPoTranslator creates a new Po object with the Translator interface
func NewPoTranslator() Translator {
return new(Po)
//NewPo should always be used to instantiate a new Po object
func NewPo() *Po {
po := new(Po)
po.domain = NewDomain()
return po
}
func (po *Po) GetDomain() *Domain {
return po.domain
}
//all of these functions are for convenience and aid in backwards compatibility
func (po *Po) Get(str string, vars ...interface{}) string {
return po.domain.Get(str, vars...)
}
func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
return po.domain.GetN(str, plural, n, vars...)
}
func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
return po.domain.GetC(str, ctx, vars...)
}
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return po.domain.GetNC(str, plural, n, ctx, vars...)
}
func (po *Po) MarshalBinary() ([]byte, error) {
return po.domain.MarshalBinary()
}
func (po *Po) UnmarshalBinary(data []byte) error {
return po.domain.UnmarshalBinary(data)
}
// ParseFile tries to read the file by its provided path (f) and parse its content as a .po file.
func (po *Po) ParseFile(f string) {
// Check if file exists
info, err := os.Stat(f)
if err != nil {
return
}
// Check that isn't a directory
if info.IsDir() {
return
}
// Parse file content
data, err := ioutil.ReadFile(f)
data, err := getFileData(f)
if err != nil {
return
}
@ -109,21 +102,22 @@ func (po *Po) ParseFile(f string) {
// Parse loads the translations specified in the provided string (str)
func (po *Po) Parse(buf []byte) {
// Lock while parsing
po.Lock()
// Init storage
if po.translations == nil {
po.translations = make(map[string]*Translation)
po.contexts = make(map[string]map[string]*Translation)
if po.domain == nil {
panic("po.domain must be set when calling Parse")
}
// Lock while parsing
po.domain.trMutex.Lock()
po.domain.pluralMutex.Lock()
defer po.domain.trMutex.Unlock()
defer po.domain.pluralMutex.Unlock()
// Get lines
lines := strings.Split(string(buf), "\n")
// Init buffer
po.trBuffer = NewTranslation()
po.ctxBuffer = ""
po.domain.trBuffer = NewTranslation()
po.domain.ctxBuffer = ""
state := head
for _, l := range lines {
@ -152,6 +146,7 @@ func (po *Po) Parse(buf []byte) {
// Check for plural form
if strings.HasPrefix(l, "msgid_plural") {
po.parsePluralID(l)
po.domain.pluralTranslations[po.domain.trBuffer.PluralID] = po.domain.trBuffer
state = msgIDPlural
continue
}
@ -173,34 +168,37 @@ func (po *Po) Parse(buf []byte) {
// Save last Translation buffer.
po.saveBuffer()
// Unlock to parse headers
po.Unlock()
// Parse headers
po.parseHeaders()
po.domain.parseHeaders()
// set values on this struct
// this is for backwards compatibility
po.Language = po.domain.Language
po.PluralForms = po.domain.PluralForms
po.Headers = po.domain.Headers
}
// saveBuffer takes the context and Translation buffers
// and saves it on the translations collection
func (po *Po) saveBuffer() {
// With no context...
if po.ctxBuffer == "" {
po.translations[po.trBuffer.ID] = po.trBuffer
if po.domain.ctxBuffer == "" {
po.domain.translations[po.domain.trBuffer.ID] = po.domain.trBuffer
} else {
// With context...
if _, ok := po.contexts[po.ctxBuffer]; !ok {
po.contexts[po.ctxBuffer] = make(map[string]*Translation)
if _, ok := po.domain.contexts[po.domain.ctxBuffer]; !ok {
po.domain.contexts[po.domain.ctxBuffer] = make(map[string]*Translation)
}
po.contexts[po.ctxBuffer][po.trBuffer.ID] = po.trBuffer
po.domain.contexts[po.domain.ctxBuffer][po.domain.trBuffer.ID] = po.domain.trBuffer
// Cleanup current context buffer if needed
if po.trBuffer.ID != "" {
po.ctxBuffer = ""
if po.domain.trBuffer.ID != "" {
po.domain.ctxBuffer = ""
}
}
// Flush Translation buffer
po.trBuffer = NewTranslation()
po.domain.trBuffer = NewTranslation()
}
// parseContext takes a line starting with "msgctxt",
@ -210,7 +208,7 @@ func (po *Po) parseContext(l string) {
po.saveBuffer()
// Buffer context
po.ctxBuffer, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt")))
po.domain.ctxBuffer, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt")))
}
// parseID takes a line starting with "msgid",
@ -220,12 +218,12 @@ func (po *Po) parseID(l string) {
po.saveBuffer()
// Set id
po.trBuffer.ID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid")))
po.domain.trBuffer.ID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid")))
}
// parsePluralID saves the plural id buffer from a line starting with "msgid_plural"
func (po *Po) parsePluralID(l string) {
po.trBuffer.PluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural")))
po.domain.trBuffer.PluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural")))
}
// parseMessage takes a line starting with "msgstr" and saves it into the current buffer.
@ -248,14 +246,14 @@ func (po *Po) parseMessage(l string) {
}
// Parse Translation string
po.trBuffer.Trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:]))
po.domain.trBuffer.Trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:]))
// Loop
return
}
// Save single Translation form under 0 index
po.trBuffer.Trs[0], _ = strconv.Unquote(l)
po.domain.trBuffer.Trs[0], _ = strconv.Unquote(l)
}
// parseString takes a well formatted string without prefix
@ -266,19 +264,19 @@ func (po *Po) parseString(l string, state parseState) {
switch state {
case msgStr:
// Append to last Translation found
po.trBuffer.Trs[len(po.trBuffer.Trs)-1] += clean
po.domain.trBuffer.Trs[len(po.domain.trBuffer.Trs)-1] += clean
case msgID:
// Multiline msgid - Append to current id
po.trBuffer.ID += clean
po.domain.trBuffer.ID += clean
case msgIDPlural:
// Multiline msgid - Append to current id
po.trBuffer.PluralID += clean
po.domain.trBuffer.PluralID += clean
case msgCtxt:
// Multiline context - Append to current context
po.ctxBuffer += clean
po.domain.ctxBuffer += clean
}
}
@ -302,200 +300,3 @@ func (po *Po) isValidLine(l string) bool {
return false
}
// parseHeaders retrieves data from previously parsed headers
func (po *Po) parseHeaders() {
// Make sure we end with 2 carriage returns.
raw := po.Get("") + "\n\n"
// Read
reader := bufio.NewReader(strings.NewReader(raw))
tp := textproto.NewReader(reader)
var err error
// Sync Headers write.
po.Lock()
defer po.Unlock()
po.Headers, err = tp.ReadMIMEHeader()
if err != nil {
return
}
// Get/save needed headers
po.Language = po.Headers.Get("Language")
po.PluralForms = po.Headers.Get("Plural-Forms")
// Parse Plural-Forms formula
if po.PluralForms == "" {
return
}
// Split plural form header value
pfs := strings.Split(po.PluralForms, ";")
// Parse values
for _, i := range pfs {
vs := strings.SplitN(i, "=", 2)
if len(vs) != 2 {
continue
}
switch strings.TrimSpace(vs[0]) {
case "nplurals":
po.nplurals, _ = strconv.Atoi(vs[1])
case "plural":
po.plural = vs[1]
if expr, err := plurals.Compile(po.plural); err == nil {
po.pluralforms = expr
}
}
}
}
// pluralForm calculates the plural form index corresponding to n.
// Returns 0 on error
func (po *Po) pluralForm(n int) int {
po.RLock()
defer po.RUnlock()
// Failure fallback
if po.pluralforms == nil {
/* Use Western plural rule. */
if n == 1 {
return 0
}
return 1
}
return po.pluralforms.Eval(uint32(n))
}
// Get retrieves the corresponding Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) Get(str string, vars ...interface{}) string {
// Sync read
po.RLock()
defer po.RUnlock()
if po.translations != nil {
if _, ok := po.translations[str]; ok {
return Printf(po.translations[str].Get(), vars...)
}
}
// Return the same we received by default
return Printf(str, vars...)
}
// GetN retrieves the (N)th plural form of Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
// Sync read
po.RLock()
defer po.RUnlock()
if po.translations != nil {
if _, ok := po.translations[str]; ok {
return Printf(po.translations[str].GetN(po.pluralForm(n)), vars...)
}
}
// Parse plural forms to distinguish between plural and singular
if po.pluralForm(n) == 0 {
return Printf(str, vars...)
}
return Printf(plural, vars...)
}
// GetC retrieves the corresponding Translation for a given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
// Sync read
po.RLock()
defer po.RUnlock()
if po.contexts != nil {
if _, ok := po.contexts[ctx]; ok {
if po.contexts[ctx] != nil {
if _, ok := po.contexts[ctx][str]; ok {
return Printf(po.contexts[ctx][str].Get(), vars...)
}
}
}
}
// Return the string we received by default
return Printf(str, vars...)
}
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
// Sync read
po.RLock()
defer po.RUnlock()
if po.contexts != nil {
if _, ok := po.contexts[ctx]; ok {
if po.contexts[ctx] != nil {
if _, ok := po.contexts[ctx][str]; ok {
return Printf(po.contexts[ctx][str].GetN(po.pluralForm(n)), vars...)
}
}
}
}
// Parse plural forms to distinguish between plural and singular
if po.pluralForm(n) == 0 {
return Printf(str, vars...)
}
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

@ -6,14 +6,19 @@
package gotext
import (
"fmt"
"os"
"path"
"testing"
)
const (
translatedText = "Translated text"
)
func TestPo_Get(t *testing.T) {
// Create po object
po := new(Po)
po := NewPo()
// Try to parse a directory
po.ParseFile(path.Clean(os.TempDir()))
@ -23,8 +28,8 @@ func TestPo_Get(t *testing.T) {
// Test translations
tr := po.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
if tr != translatedText {
t.Errorf("Expected '%s' but got '%s'", translatedText, tr)
}
// Test translations
tr = po.Get("language")
@ -122,7 +127,7 @@ msgstr "More Translation"
}
// Create po object
po := new(Po)
po := NewPo()
// Try to parse a directory
po.ParseFile(path.Clean(os.TempDir()))
@ -132,8 +137,8 @@ msgstr "More Translation"
// Test translations
tr := po.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
if tr != translatedText {
t.Errorf(