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
This commit is contained in:
Josef Fröhle 2018-03-23 21:17:05 +01:00
parent 8c36835ece
commit cd46239477
23 changed files with 1726 additions and 234 deletions

3
.gitignore vendored
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 Normal file

Binary file not shown.

77
fixtures/de/default.po Normal file
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"

Binary file not shown.

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
fixtures/en_AU/default.po Normal file
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 Normal file

Binary file not shown.

BIN
fixtures/en_US/default.mo Normal file

Binary file not shown.

68
fixtures/en_US/default.po Normal file
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"

Binary file not shown.

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"

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
}

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
helper.go Normal file
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
helper_test.go Normal file
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)
}
}

128
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)
var poObj Translator
// Parse file.
po.ParseFile(l.findPO(dom))
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()
}
// Get uses a domain "default" to return the corresponding translation of a given string.
// 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.
// 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...)
}

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")
// Set default domain to make it fail
SetDomain("default")
// Test non-existent "default" domain responses
tr := l.GetDomain()
if tr != "my_domain" {
t.Errorf("Expected 'my_domain' but got '%s'", tr)
}
// Test non-existent "deafult" domain responses
tr := l.Get("My text")
// Set default domain to make it fail
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 "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
mo.go Normal file
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