Browse Source

Create MO parser

Refactored a bit too, so we can use interfaces to take Mo and Po files

added fixtures

found that the parser for Po files have a bug... but it works... so not touched
tags/v1.4.0
Josef Fröhle 3 years ago
parent
commit
cd46239477
23 changed files with 1726 additions and 234 deletions
  1. +3
    -0
      .gitignore
  2. BIN
      fixtures/de/default.mo
  3. +77
    -0
      fixtures/de/default.po
  4. BIN
      fixtures/de_DE/LC_MESSAGES/default.mo
  5. +77
    -0
      fixtures/de_DE/LC_MESSAGES/default.po
  6. +68
    -0
      fixtures/en_AU/default.po
  7. BIN
      fixtures/en_GB/default.mo
  8. BIN
      fixtures/en_US/default.mo
  9. +68
    -0
      fixtures/en_US/default.po
  10. BIN
      fixtures/fr/LC_MESSAGES/default.mo
  11. +77
    -0
      fixtures/fr/LC_MESSAGES/default.po
  12. +30
    -30
      gotext.go
  13. +55
    -20
      gotext_test.go
  14. +85
    -0
      helper.go
  15. +112
    -0
      helper_test.go
  16. +88
    -42
      locale.go
  17. +148
    -19
      locale_test.go
  18. +421
    -0
      mo.go
  19. +204
    -0
      mo_test.go
  20. +59
    -92
      po.go
  21. +91
    -31
      po_test.go
  22. +48
    -0
      translation.go
  23. +15
    -0
      translator.go

+ 3
- 0
.gitignore View File

@@ -3,6 +3,9 @@
.settings
.buildpath

# golang jetbrains shit
.idea

# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a


BIN
fixtures/de/default.mo View File


+ 77
- 0
fixtures/de/default.po View File

@@ -0,0 +1,77 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: Josef Fröhle <froehle@b1-systems.de>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de\n"
"X-Generator: Poedit 2.0.6\n"
"X-Poedit-SourceCharset: UTF-8\n"

# Initial comment
# Headers below
msgid "language"
msgstr "de"

# Some comment
msgid "My text"
msgstr "Translated text"

# More comments
msgid "Another string"
msgstr ""

# Multi-line msgid
msgid ""
"multi\n"
"line\n"
"id"
msgstr "id with multiline content"

# Multi-line msgid_plural
msgid ""
"multi\n"
"line\n"
"plural\n"
"id"
msgstr "plural id with multiline content"

# Multi-line string
msgid "Multi-line"
msgstr ""
"Multi \n"
"line"

msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"

msgctxt "Ctx"
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"

msgid "Some random"
msgstr "Some random translation"

msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"

msgid "Empty translation"
msgstr ""

msgid "Empty plural form singular"
msgid_plural "Empty plural form"
msgstr[0] "Singular translated"
msgstr[1] ""

msgid "More"
msgstr "More translation"

BIN
fixtures/de_DE/LC_MESSAGES/default.mo View File


+ 77
- 0
fixtures/de_DE/LC_MESSAGES/default.po View File

@@ -0,0 +1,77 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: Josef Fröhle <froehle@b1-systems.de>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: de_DE\n"
"X-Generator: Poedit 2.0.6\n"
"X-Poedit-SourceCharset: UTF-8\n"

# Initial comment
# Headers below
msgid "language"
msgstr "de_DE"

# Some comment
msgid "My text"
msgstr "Translated text"

# More comments
msgid "Another string"
msgstr ""

# Multi-line msgid
msgid ""
"multi\n"
"line\n"
"id"
msgstr "id with multiline content"

# Multi-line msgid_plural
msgid ""
"multi\n"
"line\n"
"plural\n"
"id"
msgstr "plural id with multiline content"

# Multi-line string
msgid "Multi-line"
msgstr ""
"Multi \n"
"line"

msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"

msgctxt "Ctx"
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"

msgid "Some random"
msgstr "Some random translation"

msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"

msgid "Empty translation"
msgstr ""

msgid "Empty plural form singular"
msgid_plural "Empty plural form"
msgstr[0] "Singular translated"
msgstr[1] ""

msgid "More"
msgstr "More translation"

+ 68
- 0
fixtures/en_AU/default.po View File

@@ -0,0 +1,68 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: Josef Fröhle <froehle@b1-systems.de>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_US\n"
"X-Generator: Poedit 2.0.6\n"
"X-Poedit-SourceCharset: UTF-8\n"

# Initial comment
# Headers below
msgid "language"
msgstr "en_AU"

# Some comment
msgid "My text"
msgstr "Translated text"

# More comments
msgid "Another string"
msgstr ""

# Multi-line msgid
msgid "multilineid"
msgstr "id with multiline content"

# Multi-line msgid_plural
msgid "multilinepluralid"
msgstr "plural id with multiline content"

# Multi-line string
msgid "Multi-line"
msgstr "Multi line"

msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"

msgctxt "Ctx"
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"

msgid "Some random"
msgstr "Some random translation"

msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"

msgid "Empty translation"
msgstr ""

msgid "Empty plural form singular"
msgid_plural "Empty plural form"
msgstr[0] "Singular translated"
msgstr[1] ""

msgid "More"
msgstr "More translation"

BIN
fixtures/en_GB/default.mo View File


BIN
fixtures/en_US/default.mo View File


+ 68
- 0
fixtures/en_US/default.po View File

@@ -0,0 +1,68 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: Josef Fröhle <froehle@b1-systems.de>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: en_US\n"
"X-Generator: Poedit 2.0.6\n"
"X-Poedit-SourceCharset: UTF-8\n"

# Initial comment
# Headers below
msgid "language"
msgstr "en_US"

# Some comment
msgid "My text"
msgstr "Translated text"

# More comments
msgid "Another string"
msgstr ""

# Multi-line msgid
msgid "multilineid"
msgstr "id with multiline content"

# Multi-line msgid_plural
msgid "multilinepluralid"
msgstr "plural id with multiline content"

# Multi-line string
msgid "Multi-line"
msgstr "Multi line"

msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"

msgctxt "Ctx"
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"

msgid "Some random"
msgstr "Some random translation"

msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"

msgid "Empty translation"
msgstr ""

msgid "Empty plural form singular"
msgid_plural "Empty plural form"
msgstr[0] "Singular translated"
msgstr[1] ""

msgid "More"
msgstr "More translation"

BIN
fixtures/fr/LC_MESSAGES/default.mo View File


+ 77
- 0
fixtures/fr/LC_MESSAGES/default.po View File

@@ -0,0 +1,77 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
"Project-Id-Version: \n"
"POT-Creation-Date: \n"
"PO-Revision-Date: \n"
"Last-Translator: Josef Fröhle <froehle@b1-systems.de>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: fr\n"
"X-Generator: Poedit 2.0.6\n"
"X-Poedit-SourceCharset: UTF-8\n"

# Initial comment
# Headers below
msgid "language"
msgstr "fr"

# Some comment
msgid "My text"
msgstr "Translated text"

# More comments
msgid "Another string"
msgstr ""

# Multi-line msgid
msgid ""
"multi\n"
"line\n"
"id"
msgstr "id with multiline content"

# Multi-line msgid_plural
msgid ""
"multi\n"
"line\n"
"plural\n"
"id"
msgstr "plural id with multiline content"

# Multi-line string
msgid "Multi-line"
msgstr ""
"Multi \n"
"line"

msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"

msgctxt "Ctx"
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"

msgid "Some random"
msgstr "Some random translation"

msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"

msgid "Empty translation"
msgstr ""

msgid "Empty plural form singular"
msgid_plural "Empty plural form"
msgstr[0] "Singular translated"
msgstr[1] ""

msgid "More"
msgstr "More translation"

+ 30
- 30
gotext.go View File

@@ -23,7 +23,6 @@ For quick/simple translations you can use the package level functions directly.
package gotext

import (
"fmt"
"sync"
)

@@ -37,7 +36,7 @@ type config struct {
// Language set.
language string

// Path to library directory where all locale directories and translation files are.
// Path to library directory where all locale directories and Translation files are.
library string

// Storage for package level methods
@@ -65,7 +64,7 @@ func loadStorage(force bool) {
globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language)
}

if _, ok := globalConfig.storage.domains[globalConfig.domain]; !ok || force {
if _, ok := globalConfig.storage.Domains[globalConfig.domain]; !ok || force {
globalConfig.storage.AddDomain(globalConfig.domain)
}

@@ -74,18 +73,27 @@ func loadStorage(force bool) {

// GetDomain is the domain getter for the package configuration
func GetDomain() string {
var dom string
globalConfig.RLock()
dom := globalConfig.domain
if globalConfig.storage != nil {
dom = globalConfig.storage.GetDomain()
}
if dom == "" {
dom = globalConfig.domain
}
globalConfig.RUnlock()

return dom
}

// SetDomain sets the name for the domain to be used at package level.
// It reloads the corresponding translation file.
// It reloads the corresponding Translation file.
func SetDomain(dom string) {
globalConfig.Lock()
globalConfig.domain = dom
if globalConfig.storage != nil {
globalConfig.storage.SetDomain(dom)
}
globalConfig.Unlock()

loadStorage(true)
@@ -101,10 +109,10 @@ func GetLanguage() string {
}

// SetLanguage sets the language code to be used at package level.
// It reloads the corresponding translation file.
// It reloads the corresponding Translation file.
func SetLanguage(lang string) {
globalConfig.Lock()
globalConfig.language = lang
globalConfig.language = SimplifiedLocale(lang)
globalConfig.Unlock()

loadStorage(true)
@@ -120,7 +128,7 @@ func GetLibrary() string {
}

// SetLibrary sets the root path for the loale directories and files to be used at package level.
// It reloads the corresponding translation file.
// It reloads the corresponding Translation file.
func SetLibrary(lib string) {
globalConfig.Lock()
globalConfig.library = lib
@@ -129,47 +137,48 @@ func SetLibrary(lib string) {
loadStorage(true)
}

// Configure sets all configuration variables to be used at package level and reloads the corresponding translation file.
// Configure sets all configuration variables to be used at package level and reloads the corresponding Translation file.
// It receives the library path, language code and domain name.
// This function is recommended to be used when changing more than one setting,
// as using each setter will introduce a I/O overhead because the translation file will be loaded after each set.
// as using each setter will introduce a I/O overhead because the Translation file will be loaded after each set.
func Configure(lib, lang, dom string) {
globalConfig.Lock()

globalConfig.library = lib
globalConfig.language = lang
globalConfig.language = SimplifiedLocale(lang)
globalConfig.domain = dom
globalConfig.storage.SetDomain(dom)

globalConfig.Unlock()

loadStorage(true)
}

// Get uses the default domain globally set to return the corresponding translation of a given string.
// Get uses the default domain globally set to return the corresponding Translation of a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func Get(str string, vars ...interface{}) string {
return GetD(GetDomain(), str, vars...)
}

// GetN retrieves the (N)th plural form of translation for the given string in the default domain.
// GetN retrieves the (N)th plural form of Translation for the given string in the default domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetN(str, plural string, n int, vars ...interface{}) string {
return GetND(GetDomain(), str, plural, n, vars...)
}

// GetD returns the corresponding translation in the given domain for a given string.
// GetD returns the corresponding Translation in the given domain for a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetD(dom, str string, vars ...interface{}) string {
return GetND(dom, str, str, 1, vars...)
}

// GetND retrieves the (N)th plural form of translation in the given domain for a given string.
// GetND retrieves the (N)th plural form of Translation in the given domain for a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Try to load default package Locale storage
loadStorage(false)

// Return translation
// Return Translation
globalConfig.RLock()
tr := globalConfig.storage.GetND(dom, str, plural, n, vars...)
globalConfig.RUnlock()
@@ -177,43 +186,34 @@ func GetND(dom, str, plural string, n int, vars ...interface{}) string {
return tr
}

// GetC uses the default domain globally set to return the corresponding translation of the given string in the given context.
// GetC uses the default domain globally set to return the corresponding Translation of 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 GetC(str, ctx string, vars ...interface{}) string {
return GetDC(GetDomain(), str, ctx, vars...)
}

// GetNC retrieves the (N)th plural form of translation for the given string in the given context in the default domain.
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the default domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return GetNDC(GetDomain(), str, plural, n, ctx, vars...)
}

// GetDC returns the corresponding translation in the given domain for the given string in the given context.
// GetDC returns the corresponding Translation in the given domain 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 GetDC(dom, str, ctx string, vars ...interface{}) string {
return GetNDC(dom, str, str, 1, ctx, vars...)
}

// GetNDC retrieves the (N)th plural form of translation in the given domain for a given string.
// GetNDC retrieves the (N)th plural form of Translation in the given domain for a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Try to load default package Locale storage
loadStorage(false)

// Return translation
// Return Translation
globalConfig.RLock()
tr := globalConfig.storage.GetNDC(dom, str, plural, n, ctx, vars...)
globalConfig.RUnlock()

return tr
}

// printf applies text formatting only when needed to parse variables.
func printf(str string, vars ...interface{}) string {
if len(vars) > 0 {
return fmt.Sprintf(str, vars...)
}

return str
}

+ 55
- 20
gotext_test.go View File

@@ -3,6 +3,7 @@ package gotext
import (
"os"
"path"
"path/filepath"
"sync"
"testing"
)
@@ -65,14 +66,14 @@ msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"

msgid "Some random"
msgstr "Some random translation"
msgstr "Some random Translation"

msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgstr "Some random Translation in a context"

msgid "More"
msgstr "More translation"
msgstr "More Translation"

msgid "Untranslated"
msgid_plural "Several untranslated"
@@ -95,13 +96,15 @@ msgstr[1] ""
if err != nil {
t.Fatalf("Can't create test file: %s", err.Error())
}
defer f.Close()

_, err = f.WriteString(str)
if err != nil {
t.Fatalf("Can't write to test file: %s", err.Error())
}

// Move file close to write the file, so we can use it in the next step
f.Close()

// Set package configuration
Configure("/tmp", "en_US", "default")

@@ -125,8 +128,8 @@ msgstr[1] ""

// Test context translations
tr = GetC("Some random in a context", "Ctx")
if tr != "Some random translation in a context" {
t.Errorf("Expected 'Some random translation in a context' but got '%s'", tr)
if tr != "Some random Translation in a context" {
t.Errorf("Expected 'Some random Translation in a context' but got '%s'", tr)
}

v = "Variable"
@@ -214,6 +217,38 @@ msgstr[1] ""
}
}

func TestMoAndPoTranslator(t *testing.T) {

fixPath, _ := filepath.Abs("./fixtures/")

Configure(fixPath, "en_GB", "default")

// Check default domain Translation
SetDomain("default")
tr := Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text'. Got '%s'", tr)
}
tr = Get("language")
if tr != "en_GB" {
t.Errorf("Expected 'en_GB'. Got '%s'", tr)
}

// Change Language (locale)
SetLanguage("en_AU")

// Check default domain Translation
SetDomain("default")
tr = Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text'. Got '%s'", tr)
}
tr = Get("language")
if tr != "en_AU" {
t.Errorf("Expected 'en_AU'. Got '%s'", tr)
}
}

func TestDomains(t *testing.T) {
// Set PO content
strDefault := `
@@ -222,13 +257,13 @@ msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n"

msgid "Default text"
msgid_plural "Default texts"
msgstr[0] "Default translation"
msgstr[0] "Default Translation"
msgstr[1] "Default translations"

msgctxt "Ctx"
msgid "Default context"
msgid_plural "Default contexts"
msgstr[0] "Default ctx translation"
msgstr[0] "Default ctx Translation"
msgstr[1] "Default ctx translations"
`

@@ -238,13 +273,13 @@ msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n"

msgid "Custom text"
msgid_plural "Custom texts"
msgstr[0] "Custom translation"
msgstr[0] "Custom Translation"
msgstr[1] "Custom translations"

msgctxt "Ctx"
msgid "Custom context"
msgid_plural "Custom contexts"
msgstr[0] "Custom ctx translation"
msgstr[0] "Custom ctx Translation"
msgstr[1] "Custom ctx translations"
`

@@ -278,19 +313,19 @@ msgstr[1] "Custom ctx translations"

Configure("/tmp", "en_US", "default")

// Check default domain translation
// Check default domain Translation
SetDomain("default")
tr := Get("Default text")
if tr != "Default translation" {
t.Errorf("Expected 'Default translation'. Got '%s'", tr)
if tr != "Default Translation" {
t.Errorf("Expected 'Default Translation'. Got '%s'", tr)
}
tr = GetN("Default text", "Default texts", 23)
if tr != "Default translations" {
t.Errorf("Expected 'Default translations'. Got '%s'", tr)
}
tr = GetC("Default context", "Ctx")
if tr != "Default ctx translation" {
t.Errorf("Expected 'Default ctx translation'. Got '%s'", tr)
if tr != "Default ctx Translation" {
t.Errorf("Expected 'Default ctx Translation'. Got '%s'", tr)
}
tr = GetNC("Default context", "Default contexts", 23, "Ctx")
if tr != "Default ctx translations" {
@@ -299,16 +334,16 @@ msgstr[1] "Custom ctx translations"

SetDomain("custom")
tr = Get("Custom text")
if tr != "Custom translation" {
t.Errorf("Expected 'Custom translation'. Got '%s'", tr)
if tr != "Custom Translation" {
t.Errorf("Expected 'Custom Translation'. Got '%s'", tr)
}
tr = GetN("Custom text", "Custom texts", 23)
if tr != "Custom translations" {
t.Errorf("Expected 'Custom translations'. Got '%s'", tr)
}
tr = GetC("Custom context", "Ctx")
if tr != "Custom ctx translation" {
t.Errorf("Expected 'Custom ctx translation'. Got '%s'", tr)
if tr != "Custom ctx Translation" {
t.Errorf("Expected 'Custom ctx Translation'. Got '%s'", tr)
}
tr = GetNC("Custom context", "Custom contexts", 23, "Ctx")
if tr != "Custom ctx translations" {
@@ -334,7 +369,7 @@ msgstr[2] "And this is the second plural form: %s"

msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgstr "Some random Translation in a context"

`



+ 85
- 0
helper.go View File

@@ -0,0 +1,85 @@
/*
* 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 (
"fmt"
"regexp"
"strings"
)

var re = regexp.MustCompile(`%\(([a-zA-Z0-9_]+)\)[.0-9]*[xsvTtbcdoqXxUeEfFgGp]`)

func SimplifiedLocale(lang string) string {
// en_US/en_US.UTF-8/zh_CN/zh_TW/el_GR@euro/...
if idx := strings.Index(lang, ":"); idx != -1 {
lang = lang[:idx]
}
if idx := strings.Index(lang, "@"); idx != -1 {
lang = lang[:idx]
}
if idx := strings.Index(lang, "."); idx != -1 {
lang = lang[:idx]
}
return strings.TrimSpace(lang)
}

// printf applies text formatting only when needed to parse variables.
func Printf(str string, vars ...interface{}) string {
if len(vars) > 0 {
return fmt.Sprintf(str, vars...)
}

return str
}


// NPrintf support named format
func NPrintf(format string, params map[string]interface{}) {
f, p := parseSprintf(format, params)
fmt.Printf(f, p...)
}

// Sprintf support named format
// Sprintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
func Sprintf(format string, params map[string]interface{}) string {
f, p := parseSprintf(format, params)
return fmt.Sprintf(f, p...)
}

func parseSprintf(format string, params map[string]interface{}) (string, []interface{}) {
f, n := reformatSprintf(format)
var p []interface{}
for _, v := range n {
p = append(p, params[v])
}
return f, p
}

func reformatSprintf(f string) (string, []string) {
m := re.FindAllStringSubmatch(f, -1)
i := re.FindAllStringSubmatchIndex(f, -1)

ord := []string{}
for _, v := range m {
ord = append(ord, v[1])
}

pair := []int{0}
for _, v := range i {
pair = append(pair, v[2]-1)
pair = append(pair, v[3]+1)
}
pair = append(pair, len(f))
plen := len(pair)

out := ""
for n := 0; n < plen; n += 2 {
out += f[pair[n]:pair[n+1]]
}

return out, ord
}

+ 112
- 0
helper_test.go View File

@@ -0,0 +1,112 @@
/*
* 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 (
"reflect"
"testing"
)

func TestSimplifiedLocale(t *testing.T) {
tr :=SimplifiedLocale("de_DE@euro")
if tr != "de_DE" {
t.Errorf("Expected 'de_DE' but got '%s'", tr)
}

tr =SimplifiedLocale("de_DE.UTF-8")
if tr != "de_DE" {
t.Errorf("Expected 'de_DE' but got '%s'", tr)
}

tr =SimplifiedLocale("de_DE:latin1")
if tr != "de_DE" {
t.Errorf("Expected 'de_DE' but got '%s'", tr)
}
}

func TestReformattingSingleNamedPattern(t *testing.T) {
pat := "%(name_me)x"

f, n := reformatSprintf(pat)

if f != "%x" {
t.Errorf("pattern should be %%x but %v", f)
}

if !reflect.DeepEqual(n, []string{"name_me"}) {
t.Errorf("named var should be {name_me} but %v", n)
}
}

func TestReformattingMultipleNamedPattern(t *testing.T) {
pat := "%(name_me)x and %(another_name)v"

f, n := reformatSprintf(pat)

if f != "%x and %v" {
t.Errorf("pattern should be %%x and %%v but %v", f)
}

if !reflect.DeepEqual(n, []string{"name_me", "another_name"}) {
t.Errorf("named var should be {name_me, another_name} but %v", n)
}
}

func TestReformattingRepeatedNamedPattern(t *testing.T) {
pat := "%(name_me)x and %(another_name)v and %(name_me)v"

f, n := reformatSprintf(pat)

if f != "%x and %v and %v" {
t.Errorf("pattern should be %%x and %%v and %%v but %v", f)
}

if !reflect.DeepEqual(n, []string{"name_me", "another_name", "name_me"}) {
t.Errorf("named var should be {name_me, another_name, name_me} but %v", n)
}
}

func TestSprintf(t *testing.T) {
pat := "%(brother)s loves %(sister)s. %(sister)s also loves %(brother)s."
params := map[string]interface{}{
"sister": "Susan",
"brother": "Louis",
}

s := Sprintf(pat, params)

if s != "Louis loves Susan. Susan also loves Louis." {
t.Errorf("result should be Louis loves Susan. Susan also love Louis. but %v", s)
}
}

func TestNPrintf(t *testing.T) {
pat := "%(brother)s loves %(sister)s. %(sister)s also loves %(brother)s.\n"
params := map[string]interface{}{
"sister": "Susan",
"brother": "Louis",
}

NPrintf(pat, params)

}

func TestSprintfFloatsWithPrecision(t *testing.T) {
pat := "%(float)f / %(floatprecision).1f / %(long)g / %(longprecision).3g"
params := map[string]interface{}{
"float": 5.034560,
"floatprecision": 5.03456,
"long": 5.03456,
"longprecision": 5.03456,
}

s := Sprintf(pat, params)

expectedresult := "5.034560 / 5.0 / 5.03456 / 5.03"
if s != expectedresult {
t.Errorf("result should be (%v) but is (%v)", expectedresult, s)
}
}

+ 88
- 42
locale.go View File

@@ -1,3 +1,8 @@
/*
* 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 (
@@ -22,13 +27,13 @@ Example:
// Create Locale with library path and language code
l := gotext.NewLocale("/path/to/i18n/dir", "en_US")

// Load domain '/path/to/i18n/dir/en_US/LC_MESSAGES/default.po'
// Load domain '/path/to/i18n/dir/en_US/LC_MESSAGES/default.{po,mo}'
l.AddDomain("default")

// Translate text from default domain
fmt.Println(l.Get("Translate this"))

// Load different domain ('/path/to/i18n/dir/en_US/LC_MESSAGES/extras.po')
// Load different domain ('/path/to/i18n/dir/en_US/LC_MESSAGES/extras.{po,mo}')
l.AddDomain("extras")

// Translate text from domain
@@ -43,8 +48,11 @@ type Locale struct {
// Language for this Locale
lang string

// List of available domains for this locale.
domains map[string]*Po
// List of available Domains for this locale.
Domains map[string]Translator

// First AddDomain is default Domain
defaultDomain string

// Sync Mutex
sync.RWMutex
@@ -55,124 +63,162 @@ type Locale struct {
func NewLocale(p, l string) *Locale {
return &Locale{
path: p,
lang: l,
domains: make(map[string]*Po),
lang: SimplifiedLocale(l),
Domains: make(map[string]Translator),
}
}

func (l *Locale) findPO(dom string) string {
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+".po")
func (l *Locale) findExt(dom, ext string) string {
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
if _, err := os.Stat(filename); err == nil {
return filename
}

if len(l.lang) > 2 {
filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+".po")
filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
if _, err := os.Stat(filename); err == nil {
return filename
}
}

filename = path.Join(l.path, l.lang, dom+".po")
filename = path.Join(l.path, l.lang, dom+"."+ext)
if _, err := os.Stat(filename); err == nil {
return filename
}

if len(l.lang) > 2 {
filename = path.Join(l.path, l.lang[:2], dom+".po")
filename = path.Join(l.path, l.lang[:2], dom+"."+ext)
if _, err := os.Stat(filename); err == nil {
return filename
}
}

return filename
return ""
}

// AddDomain creates a new domain for a given locale object and initializes the Po object.
// If the domain exists, it gets reloaded.
func (l *Locale) AddDomain(dom string) {
po := new(Po)

// Parse file.
po.ParseFile(l.findPO(dom))
var poObj Translator

file := l.findExt(dom, "po")
if file != "" {
poObj = new(Po)
// Parse file.
poObj.ParseFile(file)
} else {
file = l.findExt(dom, "mo")
if file != "" {
poObj = new(Mo)
// Parse file.
poObj.ParseFile(file)
} else {
// fallback return if no file found with
return
}
}

// Save new domain
l.Lock()
defer l.Unlock()

if l.domains == nil {
l.domains = make(map[string]*Po)
if l.Domains == nil {
l.Domains = make(map[string]Translator)
}
l.domains[dom] = po
if l.defaultDomain == "" {
l.defaultDomain = dom
}
l.Domains[dom] = poObj

// Unlock "Save new domain"
l.Unlock()
}

// GetDomain is the domain getter for the package configuration
func (l *Locale) GetDomain() string {
l.RLock()
dom := l.defaultDomain
l.RUnlock()
return dom
}

// SetDomain sets the name for the domain to be used at package level.
// It reloads the corresponding Translation file.
func (l *Locale) SetDomain(dom string) {
l.Lock()
l.defaultDomain = dom
l.Unlock()
}

// Get uses a domain "default" to return the corresponding translation of a given string.
// Get uses a domain "default" to return the corresponding Translation of a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) Get(str string, vars ...interface{}) string {
return l.GetD(GetDomain(), str, vars...)
return l.GetD(l.defaultDomain, str, vars...)
}

// GetN retrieves the (N)th plural form of translation for the given string in the "default" domain.
// GetN retrieves the (N)th plural form of Translation for the given string in the "default" domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string {
return l.GetND(GetDomain(), str, plural, n, vars...)
return l.GetND(l.defaultDomain, str, plural, n, vars...)
}

// GetD returns the corresponding translation in the given domain for the given string.
// GetD returns the corresponding Translation in the given domain for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
return l.GetND(dom, str, str, 1, vars...)
}

// GetND retrieves the (N)th plural form of translation in the given domain for the given string.
// GetND retrieves the (N)th plural form of Translation in the given domain for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()

if l.domains != nil {
if _, ok := l.domains[dom]; ok {
if l.domains[dom] != nil {
return l.domains[dom].GetN(str, plural, n, vars...)
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetN(str, plural, n, vars...)
}
}
}

// Return the same we received by default
return printf(plural, vars...)
return Printf(plural, vars...)
}

// GetC uses a domain "default" to return the corresponding translation of the given string in the given context.
// GetC uses a domain "default" to return the corresponding Translation of 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 (l *Locale) GetC(str, ctx string, vars ...interface{}) string {
return l.GetDC(GetDomain(), str, ctx, vars...)
return l.GetDC(l.defaultDomain, str, ctx, vars...)
}

// GetNC retrieves the (N)th plural form of translation for the given string in the given context in the "default" domain.
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the "default" domain.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return l.GetNDC(GetDomain(), str, plural, n, ctx, vars...)
return l.GetNDC(l.defaultDomain, str, plural, n, ctx, vars...)
}

// GetDC returns the corresponding translation in the given domain for the given string in the given context.
// GetDC returns the corresponding Translation in the given domain 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 (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
return l.GetNDC(dom, str, str, 1, ctx, vars...)
}

// GetNDC retrieves the (N)th plural form of translation in the given domain for the given string in the given context.
// GetNDC retrieves the (N)th plural form of Translation in the given domain 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 (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()

if l.domains != nil {
if _, ok := l.domains[dom]; ok {
if l.domains[dom] != nil {
return l.domains[dom].GetNC(str, plural, n, ctx, vars...)
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
}
}
}

// Return the same we received by default
return printf(plural, vars...)
return Printf(plural, vars...)
}

+ 148
- 19
locale_test.go View File

@@ -1,3 +1,8 @@
/*
* 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 (
@@ -45,14 +50,14 @@ msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"

msgid "Some random"
msgstr "Some random translation"
msgstr "Some random Translation"

msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgstr "Some random Translation in a context"

msgid "More"
msgstr "More translation"
msgstr "More Translation"

`

@@ -81,14 +86,11 @@ msgstr "More translation"
l := NewLocale("/tmp", "en_US")

// Force nil domain storage
l.domains = nil
l.Domains = nil

// Add domain
l.AddDomain("my_domain")

// Set global domain
SetDomain("my_domain")

// Test translations
tr := l.GetD("my_domain", "My text")
if tr != "Translated text" {
@@ -109,8 +111,8 @@ msgstr "More translation"

// Test context translations
tr = l.GetC("Some random in a context", "Ctx")
if tr != "Some random translation in a context" {
t.Errorf("Expected 'Some random translation in a context'. Got '%s'", tr)
if tr != "Some random Translation in a context" {
t.Errorf("Expected 'Some random Translation in a context'. Got '%s'", tr)
}

v = "Test"
@@ -130,10 +132,10 @@ msgstr "More translation"
t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr)
}

// Test last translation
// Test last Translation
tr = l.GetD("my_domain", "More")
if tr != "More translation" {
t.Errorf("Expected 'More translation' but got '%s'", tr)
if tr != "More Translation" {
t.Errorf("Expected 'More Translation' but got '%s'", tr)
}
}

@@ -178,14 +180,14 @@ msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"

msgid "Some random"
msgstr "Some random translation"
msgstr "Some random Translation"

msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgstr "Some random Translation in a context"

msgid "More"
msgstr "More translation"
msgstr "More Translation"

`

@@ -214,16 +216,28 @@ msgstr "More translation"
l := NewLocale("/tmp", "en_US")

// Force nil domain storage
l.domains = nil
l.Domains = nil

// Add domain
l.AddDomain("my_domain")

// Test non-existent "default" domain responses
tr := l.GetDomain()
if tr != "my_domain" {
t.Errorf("Expected 'my_domain' but got '%s'", tr)
}

// Set default domain to make it fail
SetDomain("default")
l.SetDomain("default")

// Test non-existent "default" domain responses
tr = l.GetDomain()
if tr != "default" {
t.Errorf("Expected 'default' but got '%s'", tr)
}

// Test non-existent "deafult" domain responses
tr := l.Get("My text")
// Test non-existent "default" domain responses
tr = l.Get("My text")
if tr != "My text" {
t.Errorf("Expected 'My text' but got '%s'", tr)
}
@@ -255,6 +269,121 @@ msgstr "More translation"
if tr != "This are tests" {
t.Errorf("Expected 'Plural index' but got '%s'", tr)
}



// Create Locale with full language code
l = NewLocale("/tmp", "golem")

// Force nil domain storage
l.Domains = nil

// Add domain
l.SetDomain("my_domain")

// Test non-existent "default" domain responses
tr = l.GetDomain()
if tr != "my_domain" {
t.Errorf("Expected 'my_domain' but got '%s'", tr)
}

// Test syntax error parsed translations
tr = l.Get("This one has invalid syntax translations")
if tr != "This one has invalid syntax translations" {
t.Errorf("Expected 'This one has invalid syntax translations' but got '%s'", tr)
}

tr = l.GetN("This one has invalid syntax translations", "This are tests", 1)
if tr != "This are tests" {
t.Errorf("Expected 'Plural index' but got '%s'", tr)
}



// Create Locale with full language code
l = NewLocale("fixtures/", "fr_FR")

// Force nil domain storage
l.Domains = nil

// Add domain
l.SetDomain("default")

// Test non-existent "default" domain responses
tr = l.GetDomain()
if tr != "default" {
t.Errorf("Expected 'my_domain' but got '%s'", tr)
}

// Test syntax error parsed translations
tr = l.Get("This one has invalid syntax translations")
if tr != "This one has invalid syntax translations" {
t.Errorf("Expected 'This one has invalid syntax translations' but got '%s'", tr)
}

tr = l.GetN("This one has invalid syntax translations", "This are tests", 1)
if tr != "This are tests" {
t.Errorf("Expected 'Plural index' but got '%s'", tr)
}

// Create Locale with full language code
l = NewLocale("fixtures/", "de_DE")

// Force nil domain storage
l.Domains = nil

// Add domain
l.SetDomain("default")

// Test non-existent "default" domain responses
tr = l.GetDomain()
if tr != "default" {
t.Errorf("Expected 'my_domain' but got '%s'", tr)
}

// Test syntax error parsed translations
tr = l.Get("This one has invalid syntax translations")
if tr != "This one has invalid syntax translations" {
t.Errorf("Expected 'This one has invalid syntax translations' but got '%s'", tr)
}

tr = l.GetN("This one has invalid syntax translations", "This are tests", 1)
if tr != "This are tests" {
t.Errorf("Expected 'Plural index' but got '%s'", tr)
}


// Create Locale with full language code
l = NewLocale("fixtures/", "de_AT")

// Force nil domain storage
l.Domains = nil

// Add domain
l.SetDomain("default")

// Test non-existent "default" domain responses
tr = l.GetDomain()
if tr != "default" {
t.Errorf("Expected 'my_domain' but got '%s'", tr)
}

// Test syntax error parsed translations
tr = l.Get("This one has invalid syntax translations")
if tr != "This one has invalid syntax translations" {
t.Errorf("Expected 'This one has invalid syntax translations' but got '%s'", tr)
}

// Test syntax error parsed translations
tr = l.GetNDC("mega", "This one has invalid syntax translations","plural",2,"ctx")
if tr != "plural" {
t.Errorf("Expected 'plural' but got '%s'", tr)
}

tr = l.GetN("This one has invalid syntax translations", "This are tests", 1)
if tr != "This are tests" {
t.Errorf("Expected 'Plural index' but got '%s'", tr)
}
}

func TestLocaleRace(t *testing.T) {


+ 421
- 0
mo.go View File

@@ -0,0 +1,421 @@
/*
* 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"
"io/ioutil"
"net/textproto"
"os"
"strconv"
"strings"
"sync"

"github.com/leonelquinteros/gotext/plurals"
)

const (
MoMagicLittleEndian = 0x950412de
MoMagicBigEndian = 0xde120495

EotSeparator = "\x04" // msgctxt and msgid separator
NulSeparator = "\x00" // msgid and msgstr separator
)
/*
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
}

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
} else {
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...)
}

+ 204
- 0
mo_test.go View File

@@ -0,0 +1,204 @@
/*
* 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 (
"os"
"path"
"testing"
)

func TestMo_Get(t *testing.T) {

// Create po object
mo := new(Mo)

// Try to parse a directory
mo.ParseFile(path.Clean(os.TempDir()))

// Parse file
mo.ParseFile("fixtures/en_US/default.mo")

// Test translations
tr := mo.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
}
// Test translations
tr = mo.Get("language")
if tr != "en_US" {
t.Errorf("Expected 'en_US' but got '%s'", tr)
}
}

func TestMo(t *testing.T) {

// Create po object
mo := new(Mo)

// Try to parse a directory
mo.ParseFile(path.Clean(os.TempDir()))

// Parse file
mo.ParseFile("fixtures/en_US/default.mo")

// Test translations
tr := mo.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
}

v := "Variable"
tr = mo.Get("One with var: %s", v)
if tr != "This one is the singular: Variable" {
t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr)
}

// Test multi-line id
tr = mo.Get("multilineid")
if tr != "id with multiline content" {
t.Errorf("Expected 'id with multiline content' but got '%s'", tr)
}

// Test multi-line plural id
tr = mo.Get("multilinepluralid")
if tr != "plural id with multiline content" {
t.Errorf("Expected 'plural id with multiline content' but got '%s'", tr)
}

// Test multi-line
tr = mo.Get("Multi-line")
if tr != "Multi line" {
t.Errorf("Expected 'Multi line' but got '%s'", tr)
}

// Test plural
tr = mo.GetN("One with var: %s", "Several with vars: %s", 2, v)
if tr != "This one is the plural: Variable" {
t.Errorf("Expected 'This one is the plural: Variable' but got '%s'", tr)
}

// Test not existent translations
tr = mo.Get("This is a test")
if tr != "This is a test" {
t.Errorf("Expected 'This is a test' but got '%s'", tr)
}

tr = mo.GetN("This is a test", "This are tests", 100)
if tr != "This are tests" {
t.Errorf("Expected 'This are tests' but got '%s'", tr)
}

// Test context translations
v = "Test"
tr = mo.GetC("One with var: %s", "Ctx", v)
if tr != "This one is the singular in a Ctx context: Test" {
t.Errorf("Expected 'This one is the singular in a Ctx context: Test' but got '%s'", tr)
}

// Test plural
tr = mo.GetNC("One with var: %s", "Several with vars: %s", 17, "Ctx", v)
if tr != "This one is the plural in a Ctx context: Test" {
t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr)
}

// Test default plural vs singular return responses
tr = mo.GetN("Original", "Original plural", 4)
if tr != "Original plural" {
t.Errorf("Expected 'Original plural' but got '%s'", tr)
}
tr = mo.GetN("Original", "Original plural", 1)
if tr != "Original" {
t.Errorf("Expected 'Original' but got '%s'", tr)
}

// Test empty Translation strings
tr = mo.Get("Empty Translation")
if tr != "Empty Translation" {
t.Errorf("Expected 'Empty Translation' but got '%s'", tr)
}

tr = mo.Get("Empty plural form singular")
if tr != "Singular translated" {
t.Errorf("Expected 'Singular translated' but got '%s'", tr)
}

tr = mo.GetN("Empty plural form singular", "Empty plural form", 1)
if tr != "Singular translated" {
t.Errorf("Expected 'Singular translated' but got '%s'", tr)
}

tr = mo.GetN("Empty plural form singular", "Empty plural form", 2)
if tr != "Empty plural form" {
t.Errorf("Expected 'Empty plural form' but got '%s'", tr)
}

// Test last Translation
tr = mo.Get("More")
if tr != "More translation" {
t.Errorf("Expected 'More translation' but got '%s'", tr)
}
}

func TestMoRace(t *testing.T) {

// Create Po object
mo := new(Mo)

// Create sync channels
pc := make(chan bool)
rc := make(chan bool)

// Parse po content in a goroutine
go func(mo *Mo, done chan bool) {
// Parse file
mo.ParseFile("fixtures/en_US/default.mo")
done <- true
}(mo, pc)

// Read some Translation on a goroutine
go func(mo *Mo, done chan bool) {
mo.Get("My text")
done <- true
}(mo, rc)

// Read something at top level
mo.Get("My text")

// Wait for goroutines to finish
<-pc
<-rc
}

func TestNewMoTranslatorRace(t *testing.T) {

// Create Po object
mo := NewMoTranslator()

// Create sync channels
pc := make(chan bool)
rc := make(chan bool)

// Parse po content in a goroutine
go func(mo Translator, done chan bool) {
// Parse file
mo.ParseFile("fixtures/en_US/default.mo")
done <- true
}(mo, pc)

// Read some Translation on a goroutine
go func(mo Translator, done chan bool) {
mo.Get("My text")
done <- true
}(mo, rc)

// Read something at top level
mo.Get("My text")

// Wait for goroutines to finish
<-pc
<-rc
}

+ 59
- 92
po.go View File

@@ -1,3 +1,8 @@
/*
* 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 (
@@ -12,50 +17,8 @@ import (
"github.com/leonelquinteros/gotext/plurals"
)

type translation struct {
id string
pluralID string
trs map[int]string
}

func newTranslation() *translation {
tr := new(translation)
tr.trs = make(map[int]string)