500 lines
11 KiB
Go
500 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/gob"
|
||
|
"io/ioutil"
|
||
|
"net/textproto"
|
||
|
"os"
|
||
|
"strconv"
|
||
|
"strings"
|
||
|
"sync"
|
||
|
|
||
|
"github.com/leonelquinteros/gotext/plurals"
|
||
|
)
|
||
|
|
||
|
/*
|
||
|
Po parses the content of any PO 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.NewPoTranslator()
|
||
|
|
||
|
// Parse .po file
|
||
|
po.ParseFile("/path/to/po/file/translations.po")
|
||
|
|
||
|
// Get Translation
|
||
|
fmt.Println(po.Get("Translate this"))
|
||
|
}
|
||
|
|
||
|
*/
|
||
|
type Po 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
|
||
|
}
|
||
|
|
||
|
type parseState int
|
||
|
|
||
|
const (
|
||
|
head parseState = iota
|
||
|
msgCtxt
|
||
|
msgID
|
||
|
msgIDPlural
|
||
|
msgStr
|
||
|
)
|
||
|
|
||
|
// NewPoTranslator creates a new Po object with the Translator interface
|
||
|
func NewPoTranslator() Translator {
|
||
|
return new(Po)
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
if err != nil {
|
||
|
return
|
||
|
}
|
||
|
|
||
|
po.Parse(data)
|
||
|
}
|
||
|
|
||
|
// 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)
|
||
|
}
|
||
|
|
||
|
// Get lines
|
||
|
lines := strings.Split(string(buf), "\n")
|
||
|
|
||
|
// Init buffer
|
||
|
po.trBuffer = NewTranslation()
|
||
|
po.ctxBuffer = ""
|
||
|
|
||
|
state := head
|
||
|
for _, l := range lines {
|
||
|
// Trim spaces
|
||
|
l = strings.TrimSpace(l)
|
||
|
|
||
|
// Skip invalid lines
|
||
|
if !po.isValidLine(l) {
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Buffer context and continue
|
||
|
if strings.HasPrefix(l, "msgctxt") {
|
||
|
po.parseContext(l)
|
||
|
state = msgCtxt
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Buffer msgid and continue
|
||
|
if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") {
|
||
|
po.parseID(l)
|
||
|
state = msgID
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Check for plural form
|
||
|
if strings.HasPrefix(l, "msgid_plural") {
|
||
|
po.parsePluralID(l)
|
||
|
state = msgIDPlural
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Save Translation
|
||
|
if strings.HasPrefix(l, "msgstr") {
|
||
|
po.parseMessage(l)
|
||
|
state = msgStr
|
||
|
continue
|
||
|
}
|
||
|
|
||
|
// Multi line strings and headers
|
||
|
if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") {
|
||
|
po.parseString(l, state)
|
||
|
continue
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Save last Translation buffer.
|
||
|
po.saveBuffer()
|
||
|
|
||
|
// Unlock to parse headers
|
||
|
po.Unlock()
|
||
|
|
||
|
// Parse headers
|
||
|
po.parseHeaders()
|
||
|
}
|
||
|
|
||
|
// 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
|
||
|
} else {
|
||
|
// With context...
|
||
|
if _, ok := po.contexts[po.ctxBuffer]; !ok {
|
||
|
po.contexts[po.ctxBuffer] = make(map[string]*Translation)
|
||
|
}
|
||
|
po.contexts[po.ctxBuffer][po.trBuffer.ID] = po.trBuffer
|
||
|
|
||
|
// Cleanup current context buffer if needed
|
||
|
if po.trBuffer.ID != "" {
|
||
|
po.ctxBuffer = ""
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Flush Translation buffer
|
||
|
po.trBuffer = NewTranslation()
|
||
|
}
|
||
|
|
||
|
// parseContext takes a line starting with "msgctxt",
|
||
|
// saves the current Translation buffer and creates a new context.
|
||
|
func (po *Po) parseContext(l string) {
|
||
|
// Save current Translation buffer.
|
||
|
po.saveBuffer()
|
||
|
|
||
|
// Buffer context
|
||
|
po.ctxBuffer, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt")))
|
||
|
}
|
||
|
|
||
|
// parseID takes a line starting with "msgid",
|
||
|
// saves the current Translation and creates a new msgid buffer.
|
||
|
func (po *Po) parseID(l string) {
|
||
|
// Save current Translation buffer.
|
||
|
po.saveBuffer()
|
||
|
|
||
|
// Set id
|
||
|
po.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")))
|
||
|
}
|
||
|
|
||
|
// parseMessage takes a line starting with "msgstr" and saves it into the current buffer.
|
||
|
func (po *Po) parseMessage(l string) {
|
||
|
l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr"))
|
||
|
|
||
|
// Check for indexed Translation forms
|
||
|
if strings.HasPrefix(l, "[") {
|
||
|
idx := strings.Index(l, "]")
|
||
|
if idx == -1 {
|
||
|
// Skip wrong index formatting
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Parse index
|
||
|
i, err := strconv.Atoi(l[1:idx])
|
||
|
if err != nil {
|
||
|
// Skip wrong index formatting
|
||
|
return
|
||
|
}
|
||
|
|
||
|
// Parse Translation string
|
||
|
po.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)
|
||
|
}
|
||
|
|
||
|
// parseString takes a well formatted string without prefix
|
||
|
// and creates headers or attach multi-line strings when corresponding
|
||
|
func (po *Po) parseString(l string, state parseState) {
|
||
|
clean, _ := strconv.Unquote(l)
|
||
|
|
||
|
switch state {
|
||
|
case msgStr:
|
||
|
// Append to last Translation found
|
||
|
po.trBuffer.Trs[len(po.trBuffer.Trs)-1] += clean
|
||
|
|
||
|
case msgID:
|
||
|
// Multiline msgid - Append to current id
|
||
|
po.trBuffer.ID += clean
|
||
|
|
||
|
case msgIDPlural:
|
||
|
// Multiline msgid - Append to current id
|
||
|
po.trBuffer.PluralID += clean
|
||
|
|
||
|
case msgCtxt:
|
||
|
// Multiline context - Append to current context
|
||
|
po.ctxBuffer += clean
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// isValidLine checks for line prefixes to detect valid syntax.
|
||
|
func (po *Po) isValidLine(l string) bool {
|
||
|
// Check prefix
|
||
|
valid := []string{
|
||
|
"\"",
|
||
|
"msgctxt",
|
||
|
"msgid",
|
||
|
"msgid_plural",
|
||
|
"msgstr",
|
||
|
}
|
||
|
|
||
|
for _, v := range valid {
|
||
|
if strings.HasPrefix(l, v) {
|
||
|
return true
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 the Germanic 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...)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
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 (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...)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if n == 1 {
|
||
|
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
|
||
|
}
|