473 lines
11 KiB
Go
473 lines
11 KiB
Go
/*
|
|
* Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved.
|
|
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
|
|
*/
|
|
|
|
package gotext
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/binary"
|
|
"encoding/gob"
|
|
"io/ioutil"
|
|
"net/textproto"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/leonelquinteros/gotext/plurals"
|
|
)
|
|
|
|
const (
|
|
// MoMagicLittleEndian encoding
|
|
MoMagicLittleEndian = 0x950412de
|
|
// MoMagicBigEndian encoding
|
|
MoMagicBigEndian = 0xde120495
|
|
|
|
// EotSeparator msgctxt and msgid separator
|
|
EotSeparator = "\x04"
|
|
// NulSeparator msgid and msgstr separator
|
|
NulSeparator = "\x00"
|
|
)
|
|
|
|
/*
|
|
Mo parses the content of any MO file and provides all the Translation functions needed.
|
|
It's the base object used by all package methods.
|
|
And it's safe for concurrent use by multiple goroutines by using the sync package for locking.
|
|
|
|
Example:
|
|
|
|
import (
|
|
"fmt"
|
|
"github.com/leonelquinteros/gotext"
|
|
)
|
|
|
|
func main() {
|
|
// Create po object
|
|
po := gotext.NewMoTranslator()
|
|
|
|
// Parse .po file
|
|
po.ParseFile("/path/to/po/file/translations.mo")
|
|
|
|
// Get Translation
|
|
fmt.Println(po.Get("Translate this"))
|
|
}
|
|
|
|
*/
|
|
type Mo struct {
|
|
// Headers storage
|
|
Headers textproto.MIMEHeader
|
|
|
|
// Language header
|
|
Language string
|
|
|
|
// 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
|
|
|
|
// Sync Mutex
|
|
sync.RWMutex
|
|
|
|
// Parsing buffers
|
|
trBuffer *Translation
|
|
ctxBuffer string
|
|
}
|
|
|
|
// NewMoTranslator creates a new Mo object with the Translator interface
|
|
func NewMoTranslator() Translator {
|
|
return new(Mo)
|
|
}
|
|
|
|
// 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)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
mo.Parse(data)
|
|
}
|
|
|
|
// Parse loads the translations specified in the provided string (str)
|
|
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)
|
|
}
|
|
|
|
r := bytes.NewReader(buf)
|
|
|
|
var magicNumber uint32
|
|
if err := binary.Read(r, binary.LittleEndian, &magicNumber); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
var bo binary.ByteOrder
|
|
switch magicNumber {
|
|
case MoMagicLittleEndian:
|
|
bo = binary.LittleEndian
|
|
case MoMagicBigEndian:
|
|
bo = binary.BigEndian
|
|
default:
|
|
return
|
|
// return fmt.Errorf("gettext: %v", "invalid magic number")
|
|
}
|
|
|
|
var header struct {
|
|
MajorVersion uint16
|
|
MinorVersion uint16
|
|
MsgIDCount uint32
|
|
MsgIDOffset uint32
|
|
MsgStrOffset uint32
|
|
HashSize uint32
|
|
HashOffset uint32
|
|
}
|
|
if err := binary.Read(r, bo, &header); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
if v := header.MajorVersion; v != 0 && v != 1 {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", "invalid version number")
|
|
}
|
|
if v := header.MinorVersion; v != 0 && v != 1 {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", "invalid version number")
|
|
}
|
|
|
|
msgIDStart := make([]uint32, header.MsgIDCount)
|
|
msgIDLen := make([]uint32, header.MsgIDCount)
|
|
if _, err := r.Seek(int64(header.MsgIDOffset), 0); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
for i := 0; i < int(header.MsgIDCount); i++ {
|
|
if err := binary.Read(r, bo, &msgIDLen[i]); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
if err := binary.Read(r, bo, &msgIDStart[i]); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
}
|
|
|
|
msgStrStart := make([]int32, header.MsgIDCount)
|
|
msgStrLen := make([]int32, header.MsgIDCount)
|
|
if _, err := r.Seek(int64(header.MsgStrOffset), 0); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
for i := 0; i < int(header.MsgIDCount); i++ {
|
|
if err := binary.Read(r, bo, &msgStrLen[i]); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
if err := binary.Read(r, bo, &msgStrStart[i]); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
}
|
|
|
|
for i := 0; i < int(header.MsgIDCount); i++ {
|
|
if _, err := r.Seek(int64(msgIDStart[i]), 0); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
msgIDData := make([]byte, msgIDLen[i])
|
|
if _, err := r.Read(msgIDData); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
|
|
if _, err := r.Seek(int64(msgStrStart[i]), 0); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
msgStrData := make([]byte, msgStrLen[i])
|
|
if _, err := r.Read(msgStrData); err != nil {
|
|
return
|
|
// return fmt.Errorf("gettext: %v", err)
|
|
}
|
|
|
|
if len(msgIDData) == 0 {
|
|
mo.addTranslation(msgIDData, msgStrData)
|
|
} else {
|
|
mo.addTranslation(msgIDData, msgStrData)
|
|
}
|
|
}
|
|
|
|
// Unlock to parse headers
|
|
mo.Unlock()
|
|
|
|
// Parse headers
|
|
mo.parseHeaders()
|
|
return
|
|
// return nil
|
|
}
|
|
|
|
func (mo *Mo) addTranslation(msgid, msgstr []byte) {
|
|
translation := NewTranslation()
|
|
var msgctxt []byte
|
|
var msgidPlural []byte
|
|
|
|
d := bytes.Split(msgid, []byte(EotSeparator))
|
|
if len(d) == 1 {
|
|
msgid = d[0]
|
|
} else {
|
|
msgid, msgctxt = d[1], d[0]
|
|
}
|
|
|
|
dd := bytes.Split(msgid, []byte(NulSeparator))
|
|
if len(dd) > 1 {
|
|
msgid = dd[0]
|
|
dd = dd[1:]
|
|
}
|
|
|
|
translation.ID = string(msgid)
|
|
|
|
msgidPlural = bytes.Join(dd, []byte(NulSeparator))
|
|
if len(msgidPlural) > 0 {
|
|
translation.PluralID = string(msgidPlural)
|
|
}
|
|
|
|
ddd := bytes.Split(msgstr, []byte(NulSeparator))
|
|
if len(ddd) > 0 {
|
|
for i, s := range ddd {
|
|
translation.Trs[i] = string(s)
|
|
}
|
|
}
|
|
|
|
if len(msgctxt) > 0 {
|
|
// With context...
|
|
if _, ok := mo.contexts[string(msgctxt)]; !ok {
|
|
mo.contexts[string(msgctxt)] = make(map[string]*Translation)
|
|
}
|
|
mo.contexts[string(msgctxt)][translation.ID] = translation
|
|
} else {
|
|
mo.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
|
|
}
|