photoprism-client-go/vendor/github.com/leonelquinteros/gotext/po.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
}