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:
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 .settings
.buildpath .buildpath
# golang jetbrains shit
.idea
# Compiled Object files, Static and Dynamic libs (Shared Objects) # Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o *.o
*.a *.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 package gotext
import ( import (
"fmt"
"sync" "sync"
) )
@@ -37,7 +36,7 @@ type config struct {
// Language set. // Language set.
language string 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 library string
// Storage for package level methods // Storage for package level methods
@@ -65,7 +64,7 @@ func loadStorage(force bool) {
globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language) 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) globalConfig.storage.AddDomain(globalConfig.domain)
} }
@@ -74,18 +73,27 @@ func loadStorage(force bool) {
// GetDomain is the domain getter for the package configuration // GetDomain is the domain getter for the package configuration
func GetDomain() string { func GetDomain() string {
var dom string
globalConfig.RLock() globalConfig.RLock()
dom := globalConfig.domain if globalConfig.storage != nil {
dom = globalConfig.storage.GetDomain()
}
if dom == "" {
dom = globalConfig.domain
}
globalConfig.RUnlock() globalConfig.RUnlock()
return dom return dom
} }
// SetDomain sets the name for the domain to be used at package level. // 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) { func SetDomain(dom string) {
globalConfig.Lock() globalConfig.Lock()
globalConfig.domain = dom globalConfig.domain = dom
if globalConfig.storage != nil {
globalConfig.storage.SetDomain(dom)
}
globalConfig.Unlock() globalConfig.Unlock()
loadStorage(true) loadStorage(true)
@@ -101,10 +109,10 @@ func GetLanguage() string {
} }
// SetLanguage sets the language code to be used at package level. // 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) { func SetLanguage(lang string) {
globalConfig.Lock() globalConfig.Lock()
globalConfig.language = lang globalConfig.language = SimplifiedLocale(lang)
globalConfig.Unlock() globalConfig.Unlock()
loadStorage(true) 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. // 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) { func SetLibrary(lib string) {
globalConfig.Lock() globalConfig.Lock()
globalConfig.library = lib globalConfig.library = lib
@@ -129,47 +137,48 @@ func SetLibrary(lib string) {
loadStorage(true) 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. // It receives the library path, language code and domain name.
// This function is recommended to be used when changing more than one setting, // 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) { func Configure(lib, lang, dom string) {
globalConfig.Lock() globalConfig.Lock()
globalConfig.library = lib globalConfig.library = lib
globalConfig.language = lang globalConfig.language = SimplifiedLocale(lang)
globalConfig.domain = dom globalConfig.domain = dom
globalConfig.storage.SetDomain(dom)
globalConfig.Unlock() globalConfig.Unlock()
loadStorage(true) 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. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func Get(str string, vars ...interface{}) string { func Get(str string, vars ...interface{}) string {
return GetD(GetDomain(), str, vars...) 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. // 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 { func GetN(str, plural string, n int, vars ...interface{}) string {
return GetND(GetDomain(), str, plural, n, vars...) 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. // 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 { func GetD(dom, str string, vars ...interface{}) string {
return GetND(dom, str, str, 1, vars...) 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. // 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 { func GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Try to load default package Locale storage // Try to load default package Locale storage
loadStorage(false) loadStorage(false)
// Return translation // Return Translation
globalConfig.RLock() globalConfig.RLock()
tr := globalConfig.storage.GetND(dom, str, plural, n, vars...) tr := globalConfig.storage.GetND(dom, str, plural, n, vars...)
globalConfig.RUnlock() globalConfig.RUnlock()
@@ -177,43 +186,34 @@ func GetND(dom, str, plural string, n int, vars ...interface{}) string {
return tr 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. // 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 { func GetC(str, ctx string, vars ...interface{}) string {
return GetDC(GetDomain(), str, ctx, vars...) 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. // 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 { func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return GetNDC(GetDomain(), str, plural, n, ctx, vars...) 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. // 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 { func GetDC(dom, str, ctx string, vars ...interface{}) string {
return GetNDC(dom, str, str, 1, ctx, vars...) 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. // 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 { func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Try to load default package Locale storage // Try to load default package Locale storage
loadStorage(false) loadStorage(false)
// Return translation // Return Translation
globalConfig.RLock() globalConfig.RLock()
tr := globalConfig.storage.GetNDC(dom, str, plural, n, ctx, vars...) tr := globalConfig.storage.GetNDC(dom, str, plural, n, ctx, vars...)
globalConfig.RUnlock() globalConfig.RUnlock()
return tr 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 ( import (
"os" "os"
"path" "path"
"path/filepath"
"sync" "sync"
"testing" "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" msgstr[1] "This one is the plural in a Ctx context: %s"
msgid "Some random" msgid "Some random"
msgstr "Some random translation" msgstr "Some random Translation"
msgctxt "Ctx" msgctxt "Ctx"
msgid "Some random in a context" msgid "Some random in a context"
msgstr "Some random translation in a context" msgstr "Some random Translation in a context"
msgid "More" msgid "More"
msgstr "More translation" msgstr "More Translation"
msgid "Untranslated" msgid "Untranslated"
msgid_plural "Several untranslated" msgid_plural "Several untranslated"
@@ -95,13 +96,15 @@ msgstr[1] ""
if err != nil { if err != nil {
t.Fatalf("Can't create test file: %s", err.Error()) t.Fatalf("Can't create test file: %s", err.Error())
} }
defer f.Close()
_, err = f.WriteString(str) _, err = f.WriteString(str)
if err != nil { if err != nil {
t.Fatalf("Can't write to test file: %s", err.Error()) 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 // Set package configuration
Configure("/tmp", "en_US", "default") Configure("/tmp", "en_US", "default")
@@ -125,8 +128,8 @@ msgstr[1] ""
// Test context translations // Test context translations
tr = GetC("Some random in a context", "Ctx") tr = GetC("Some random in a context", "Ctx")
if tr != "Some random translation in a context" { if tr != "Some random Translation in a context" {
t.Errorf("Expected 'Some random translation in a context' but got '%s'", tr) t.Errorf("Expected 'Some random Translation in a context' but got '%s'", tr)
} }
v = "Variable" 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) { func TestDomains(t *testing.T) {
// Set PO content // Set PO content
strDefault := ` strDefault := `
@@ -222,13 +257,13 @@ msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Default text" msgid "Default text"
msgid_plural "Default texts" msgid_plural "Default texts"
msgstr[0] "Default translation" msgstr[0] "Default Translation"
msgstr[1] "Default translations" msgstr[1] "Default translations"
msgctxt "Ctx" msgctxt "Ctx"
msgid "Default context" msgid "Default context"
msgid_plural "Default contexts" msgid_plural "Default contexts"
msgstr[0] "Default ctx translation" msgstr[0] "Default ctx Translation"
msgstr[1] "Default ctx translations" msgstr[1] "Default ctx translations"
` `
@@ -238,13 +273,13 @@ msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Custom text" msgid "Custom text"
msgid_plural "Custom texts" msgid_plural "Custom texts"
msgstr[0] "Custom translation" msgstr[0] "Custom Translation"
msgstr[1] "Custom translations" msgstr[1] "Custom translations"
msgctxt "Ctx" msgctxt "Ctx"
msgid "Custom context" msgid "Custom context"
msgid_plural "Custom contexts" msgid_plural "Custom contexts"
msgstr[0] "Custom ctx translation" msgstr[0] "Custom ctx Translation"
msgstr[1] "Custom ctx translations" msgstr[1] "Custom ctx translations"
` `
@@ -278,19 +313,19 @@ msgstr[1] "Custom ctx translations"
Configure("/tmp", "en_US", "default") Configure("/tmp", "en_US", "default")
// Check default domain translation // Check default domain Translation
SetDomain("default") SetDomain("default")
tr := Get("Default text") tr := Get("Default text")
if tr != "Default translation" { if tr != "Default Translation" {
t.Errorf("Expected 'Default translation'. Got '%s'", tr) t.Errorf("Expected 'Default Translation'. Got '%s'", tr)
} }
tr = GetN("Default text", "Default texts", 23) tr = GetN("Default text", "Default texts", 23)
if tr != "Default translations" { if tr != "Default translations" {
t.Errorf("Expected 'Default translations'. Got '%s'", tr) t.Errorf("Expected 'Default translations'. Got '%s'", tr)
} }
tr = GetC("Default context", "Ctx") tr = GetC("Default context", "Ctx")
if tr != "Default ctx translation" { if tr != "Default ctx Translation" {
t.Errorf("Expected 'Default ctx translation'. Got '%s'", tr) t.Errorf("Expected 'Default ctx Translation'. Got '%s'", tr)
} }
tr = GetNC("Default context", "Default contexts", 23, "Ctx") tr = GetNC("Default context", "Default contexts", 23, "Ctx")
if tr != "Default ctx translations" { if tr != "Default ctx translations" {
@@ -299,16 +334,16 @@ msgstr[1] "Custom ctx translations"
SetDomain("custom") SetDomain("custom")
tr = Get("Custom text") tr = Get("Custom text")
if tr != "Custom translation" { if tr != "Custom Translation" {
t.Errorf("Expected 'Custom translation'. Got '%s'", tr) t.Errorf("Expected 'Custom Translation'. Got '%s'", tr)
} }
tr = GetN("Custom text", "Custom texts", 23) tr = GetN("Custom text", "Custom texts", 23)
if tr != "Custom translations" { if tr != "Custom translations" {
t.Errorf("Expected 'Custom translations'. Got '%s'", tr) t.Errorf("Expected 'Custom translations'. Got '%s'", tr)
} }
tr = GetC("Custom context", "Ctx") tr = GetC("Custom context", "Ctx")
if tr != "Custom ctx translation" { if tr != "Custom ctx Translation" {
t.Errorf("Expected 'Custom ctx translation'. Got '%s'", tr) t.Errorf("Expected 'Custom ctx Translation'. Got '%s'", tr)
} }
tr = GetNC("Custom context", "Custom contexts", 23, "Ctx") tr = GetNC("Custom context", "Custom contexts", 23, "Ctx")
if tr != "Custom ctx translations" { if tr != "Custom ctx translations" {
@@ -334,7 +369,7 @@ msgstr[2] "And this is the second plural form: %s"
msgctxt "Ctx" msgctxt "Ctx"
msgid "Some random in a context" 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)
}
}

126
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 package gotext
import ( import (
@@ -22,13 +27,13 @@ Example:
// Create Locale with library path and language code // Create Locale with library path and language code
l := gotext.NewLocale("/path/to/i18n/dir", "en_US") 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") l.AddDomain("default")
// Translate text from default domain // Translate text from default domain
fmt.Println(l.Get("Translate this")) 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") l.AddDomain("extras")
// Translate text from domain // Translate text from domain
@@ -43,8 +48,11 @@ type Locale struct {
// Language for this Locale // Language for this Locale
lang string lang string
// List of available domains for this locale. // List of available Domains for this locale.
domains map[string]*Po Domains map[string]Translator
// First AddDomain is default Domain
defaultDomain string
// Sync Mutex // Sync Mutex
sync.RWMutex sync.RWMutex
@@ -55,124 +63,162 @@ type Locale struct {
func NewLocale(p, l string) *Locale { func NewLocale(p, l string) *Locale {
return &Locale{ return &Locale{
path: p, path: p,
lang: l, lang: SimplifiedLocale(l),
domains: make(map[string]*Po), Domains: make(map[string]Translator),
} }
} }
func (l *Locale) findPO(dom string) string { func (l *Locale) findExt(dom, ext string) string {
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+".po") filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
if _, err := os.Stat(filename); err == nil { if _, err := os.Stat(filename); err == nil {
return filename return filename
} }
if len(l.lang) > 2 { 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 { if _, err := os.Stat(filename); err == nil {
return filename 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 { if _, err := os.Stat(filename); err == nil {
return filename return filename
} }
if len(l.lang) > 2 { 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. // AddDomain creates a new domain for a given locale object and initializes the Po object.
// If the domain exists, it gets reloaded. // If the domain exists, it gets reloaded.
func (l *Locale) AddDomain(dom string) { func (l *Locale) AddDomain(dom string) {
po := new(Po) var poObj Translator
file := l.findExt(dom, "po")
if file != "" {
poObj = new(Po)
// Parse file. // Parse file.
po.ParseFile(l.findPO(dom)) 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 // Save new domain
l.Lock() l.Lock()
defer l.Unlock()
if l.domains == nil { if l.Domains == nil {
l.domains = make(map[string]*Po) 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. // 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 { 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. // 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 { 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. // 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 { func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
return l.GetND(dom, str, str, 1, vars...) 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. // 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 { func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Sync read // Sync read
l.RLock() l.RLock()
defer l.RUnlock() defer l.RUnlock()
if l.domains != nil { if l.Domains != nil {
if _, ok := l.domains[dom]; ok { if _, ok := l.Domains[dom]; ok {
if l.domains[dom] != nil { if l.Domains[dom] != nil {
return l.domains[dom].GetN(str, plural, n, vars...) return l.Domains[dom].GetN(str, plural, n, vars...)
} }
} }
} }
// Return the same we received by default // 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. // 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 { 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. // 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 { 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. // 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 { func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
return l.GetNDC(dom, str, str, 1, ctx, vars...) 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. // 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 { func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Sync read // Sync read
l.RLock() l.RLock()
defer l.RUnlock() defer l.RUnlock()
if l.domains != nil { if l.Domains != nil {
if _, ok := l.domains[dom]; ok { if _, ok := l.Domains[dom]; ok {
if l.domains[dom] != nil { if l.Domains[dom] != nil {
return l.domains[dom].GetNC(str, plural, n, ctx, vars...) return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
} }
} }
} }
// Return the same we received by default // 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 package gotext
import ( 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" msgstr[1] "This one is the plural in a Ctx context: %s"
msgid "Some random" msgid "Some random"
msgstr "Some random translation" msgstr "Some random Translation"
msgctxt "Ctx" msgctxt "Ctx"
msgid "Some random in a context" msgid "Some random in a context"
msgstr "Some random translation in a context" msgstr "Some random Translation in a context"
msgid "More" msgid "More"
msgstr "More translation" msgstr "More Translation"
` `
@@ -81,14 +86,11 @@ msgstr "More translation"
l := NewLocale("/tmp", "en_US") l := NewLocale("/tmp", "en_US")
// Force nil domain storage // Force nil domain storage
l.domains = nil l.Domains = nil
// Add domain // Add domain
l.AddDomain("my_domain") l.AddDomain("my_domain")
// Set global domain
SetDomain("my_domain")
// Test translations // Test translations
tr := l.GetD("my_domain", "My text") tr := l.GetD("my_domain", "My text")
if tr != "Translated text" { if tr != "Translated text" {
@@ -109,8 +111,8 @@ msgstr "More translation"
// Test context translations // Test context translations
tr = l.GetC("Some random in a context", "Ctx") tr = l.GetC("Some random in a context", "Ctx")
if tr != "Some random translation in a context" { if tr != "Some random Translation in a context" {
t.Errorf("Expected 'Some random translation in a context'. Got '%s'", tr) t.Errorf("Expected 'Some random Translation in a context'. Got '%s'", tr)
} }
v = "Test" 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) 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") tr = l.GetD("my_domain", "More")
if tr != "More translation" { if tr != "More Translation" {
t.Errorf("Expected 'More translation' but got '%s'", tr) 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" msgstr[1] "This one is the plural in a Ctx context: %s"
msgid "Some random" msgid "Some random"
msgstr "Some random translation" msgstr "Some random Translation"
msgctxt "Ctx" msgctxt "Ctx"
msgid "Some random in a context" msgid "Some random in a context"
msgstr "Some random translation in a context" msgstr "Some random Translation in a context"
msgid "More" msgid "More"
msgstr "More translation" msgstr "More Translation"
` `
@@ -214,16 +216,28 @@ msgstr "More translation"
l := NewLocale("/tmp", "en_US") l := NewLocale("/tmp", "en_US")
// Force nil domain storage // Force nil domain storage
l.domains = nil l.Domains = nil
// Add domain // Add domain
l.AddDomain("my_domain") l.AddDomain("my_domain")
// Set default domain to make it fail // Test non-existent "default" domain responses
SetDomain("default") tr := l.GetDomain()
if tr != "my_domain" {
t.Errorf("Expected 'my_domain' but got '%s'", tr)
}
// Test non-existent "deafult" domain responses // Set default domain to make it fail
tr := l.Get("My text") 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" { if tr != "My text" {
t.Errorf("Expected 'My text' but got '%s'", tr) t.Errorf("Expected 'My text' but got '%s'", tr)
} }
@@ -255,6 +269,121 @@ msgstr "More translation"
if tr != "This are tests" { if tr != "This are tests" {
t.Errorf("Expected 'Plural index' but got '%s'", tr) 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) { 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
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
mo_test.go Normal file
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
}

151
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 package gotext
import ( import (
@@ -12,50 +17,8 @@ import (
"github.com/leonelquinteros/gotext/plurals" "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)
return tr
}
func (t *translation) get() string {
// Look for translation index 0
if _, ok := t.trs[0]; ok {
if t.trs[0] != "" {
return t.trs[0]
}
}
// Return untranslated id by default
return t.id
}
func (t *translation) getN(n int) string {
// Look for translation index
if _, ok := t.trs[n]; ok {
if t.trs[n] != "" {
return t.trs[n]
}
}
// Return untranslated singular if corresponding
if n == 0 {
return t.id
}
// Return untranslated plural by default
return t.pluralID
}
/* /*
Po parses the content of any PO file and provides all the translation functions needed. Po parses the content of any PO file and provides all the Translation functions needed.
It's the base object used by all package methods. 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. And it's safe for concurrent use by multiple goroutines by using the sync package for locking.
@@ -68,12 +31,12 @@ Example:
func main() { func main() {
// Create po object // Create po object
po := new(gotext.Po) po := gotext.NewPoTranslator()
// Parse .po file // Parse .po file
po.ParseFile("/path/to/po/file/translations.po") po.ParseFile("/path/to/po/file/translations.po")
// Get translation // Get Translation
fmt.Println(po.Get("Translate this")) fmt.Println(po.Get("Translate this"))
} }
@@ -94,14 +57,14 @@ type Po struct {
pluralforms plurals.Expression pluralforms plurals.Expression
// Storage // Storage
translations map[string]*translation translations map[string]*Translation
contexts map[string]map[string]*translation contexts map[string]map[string]*Translation
// Sync Mutex // Sync Mutex
sync.RWMutex sync.RWMutex
// Parsing buffers // Parsing buffers
trBuffer *translation trBuffer *Translation
ctxBuffer string ctxBuffer string
} }
@@ -115,6 +78,10 @@ const (
msgStr msgStr
) )
func NewPoTranslator() Translator {
return new(Po)
}
// ParseFile tries to read the file by its provided path (f) and parse its content as a .po file. // ParseFile tries to read the file by its provided path (f) and parse its content as a .po file.
func (po *Po) ParseFile(f string) { func (po *Po) ParseFile(f string) {
// Check if file exists // Check if file exists
@@ -134,25 +101,25 @@ func (po *Po) ParseFile(f string) {
return return
} }
po.Parse(string(data)) po.Parse(data)
} }
// Parse loads the translations specified in the provided string (str) // Parse loads the translations specified in the provided string (str)
func (po *Po) Parse(str string) { func (po *Po) Parse(buf []byte) {
// Lock while parsing // Lock while parsing
po.Lock() po.Lock()
// Init storage // Init storage
if po.translations == nil { if po.translations == nil {
po.translations = make(map[string]*translation) po.translations = make(map[string]*Translation)
po.contexts = make(map[string]map[string]*translation) po.contexts = make(map[string]map[string]*Translation)
} }
// Get lines // Get lines
lines := strings.Split(str, "\n") lines := strings.Split(string(buf), "\n")
// Init buffer // Init buffer
po.trBuffer = newTranslation() po.trBuffer = NewTranslation()
po.ctxBuffer = "" po.ctxBuffer = ""
state := head state := head
@@ -186,7 +153,7 @@ func (po *Po) Parse(str string) {
continue continue
} }
// Save translation // Save Translation
if strings.HasPrefix(l, "msgstr") { if strings.HasPrefix(l, "msgstr") {
po.parseMessage(l) po.parseMessage(l)
state = msgStr state = msgStr
@@ -200,7 +167,7 @@ func (po *Po) Parse(str string) {
} }
} }
// Save last translation buffer. // Save last Translation buffer.
po.saveBuffer() po.saveBuffer()
// Unlock to parse headers // Unlock to parse headers
@@ -210,33 +177,33 @@ func (po *Po) Parse(str string) {
po.parseHeaders() po.parseHeaders()
} }
// saveBuffer takes the context and translation buffers // saveBuffer takes the context and Translation buffers
// and saves it on the translations collection // and saves it on the translations collection
func (po *Po) saveBuffer() { func (po *Po) saveBuffer() {
// With no context... // With no context...
if po.ctxBuffer == "" { if po.ctxBuffer == "" {
po.translations[po.trBuffer.id] = po.trBuffer po.translations[po.trBuffer.ID] = po.trBuffer
} else { } else {
// With context... // With context...
if _, ok := po.contexts[po.ctxBuffer]; !ok { if _, ok := po.contexts[po.ctxBuffer]; !ok {
po.contexts[po.ctxBuffer] = make(map[string]*translation) po.contexts[po.ctxBuffer] = make(map[string]*Translation)
} }
po.contexts[po.ctxBuffer][po.trBuffer.id] = po.trBuffer po.contexts[po.ctxBuffer][po.trBuffer.ID] = po.trBuffer
// Cleanup current context buffer if needed // Cleanup current context buffer if needed
if po.trBuffer.id != "" { if po.trBuffer.ID != "" {
po.ctxBuffer = "" po.ctxBuffer = ""
} }
} }
// Flush translation buffer // Flush Translation buffer
po.trBuffer = newTranslation() po.trBuffer = NewTranslation()
} }
// parseContext takes a line starting with "msgctxt", // parseContext takes a line starting with "msgctxt",
// saves the current translation buffer and creates a new context. // saves the current Translation buffer and creates a new context.
func (po *Po) parseContext(l string) { func (po *Po) parseContext(l string) {
// Save current translation buffer. // Save current Translation buffer.
po.saveBuffer() po.saveBuffer()
// Buffer context // Buffer context
@@ -244,25 +211,25 @@ func (po *Po) parseContext(l string) {
} }
// parseID takes a line starting with "msgid", // parseID takes a line starting with "msgid",
// saves the current translation and creates a new msgid buffer. // saves the current Translation and creates a new msgid buffer.
func (po *Po) parseID(l string) { func (po *Po) parseID(l string) {
// Save current translation buffer. // Save current Translation buffer.
po.saveBuffer() po.saveBuffer()
// Set id // Set id
po.trBuffer.id, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid"))) po.trBuffer.ID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid")))
} }
// parsePluralID saves the plural id buffer from a line starting with "msgid_plural" // parsePluralID saves the plural id buffer from a line starting with "msgid_plural"
func (po *Po) parsePluralID(l string) { func (po *Po) parsePluralID(l string) {
po.trBuffer.pluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural"))) po.trBuffer.PluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural")))
} }
// parseMessage takes a line starting with "msgstr" and saves it into the current buffer. // parseMessage takes a line starting with "msgstr" and saves it into the current buffer.
func (po *Po) parseMessage(l string) { func (po *Po) parseMessage(l string) {
l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr")) l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr"))
// Check for indexed translation forms // Check for indexed Translation forms
if strings.HasPrefix(l, "[") { if strings.HasPrefix(l, "[") {
idx := strings.Index(l, "]") idx := strings.Index(l, "]")
if idx == -1 { if idx == -1 {
@@ -277,15 +244,15 @@ func (po *Po) parseMessage(l string) {
return return
} }
// Parse translation string // Parse Translation string
po.trBuffer.trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:])) po.trBuffer.Trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:]))
// Loop // Loop
return return
} }
// Save single translation form under 0 index // Save single Translation form under 0 index
po.trBuffer.trs[0], _ = strconv.Unquote(l) po.trBuffer.Trs[0], _ = strconv.Unquote(l)
} }
// parseString takes a well formatted string without prefix // parseString takes a well formatted string without prefix
@@ -295,16 +262,16 @@ func (po *Po) parseString(l string, state parseState) {
switch state { switch state {
case msgStr: case msgStr:
// Append to last translation found // Append to last Translation found
po.trBuffer.trs[len(po.trBuffer.trs)-1] += clean po.trBuffer.Trs[len(po.trBuffer.Trs)-1] += clean
case msgID: case msgID:
// Multiline msgid - Append to current id // Multiline msgid - Append to current id
po.trBuffer.id += clean po.trBuffer.ID += clean
case msgIDPlural: case msgIDPlural:
// Multiline msgid - Append to current id // Multiline msgid - Append to current id
po.trBuffer.pluralID += clean po.trBuffer.PluralID += clean
case msgCtxt: case msgCtxt:
// Multiline context - Append to current context // Multiline context - Append to current context
@@ -405,7 +372,7 @@ func (po *Po) pluralForm(n int) int {
return po.pluralforms.Eval(uint32(n)) return po.pluralforms.Eval(uint32(n))
} }
// Get retrieves the corresponding translation for the given string. // 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. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) Get(str string, vars ...interface{}) string { func (po *Po) Get(str string, vars ...interface{}) string {
// Sync read // Sync read
@@ -414,15 +381,15 @@ func (po *Po) Get(str string, vars ...interface{}) string {
if po.translations != nil { if po.translations != nil {
if _, ok := po.translations[str]; ok { if _, ok := po.translations[str]; ok {
return printf(po.translations[str].get(), vars...) return Printf(po.translations[str].Get(), vars...)
} }
} }
// Return the same we received by default // Return the same we received by default
return printf(str, vars...) return Printf(str, vars...)
} }
// GetN retrieves the (N)th plural form of translation for the given string. // 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. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string { func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
// Sync read // Sync read
@@ -431,17 +398,17 @@ func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
if po.translations != nil { if po.translations != nil {
if _, ok := po.translations[str]; ok { if _, ok := po.translations[str]; ok {
return printf(po.translations[str].getN(po.pluralForm(n)), vars...) return Printf(po.translations[str].GetN(po.pluralForm(n)), vars...)
} }
} }
if n == 1 { if n == 1 {
return printf(str, vars...) return Printf(str, vars...)
} }
return printf(plural, vars...) return Printf(plural, vars...)
} }
// GetC retrieves the corresponding translation for a given string in the given context. // 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. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetC(str, ctx string, vars ...interface{}) string { func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
// Sync read // Sync read
@@ -452,17 +419,17 @@ func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
if _, ok := po.contexts[ctx]; ok { if _, ok := po.contexts[ctx]; ok {
if po.contexts[ctx] != nil { if po.contexts[ctx] != nil {
if _, ok := po.contexts[ctx][str]; ok { if _, ok := po.contexts[ctx][str]; ok {
return printf(po.contexts[ctx][str].get(), vars...) return Printf(po.contexts[ctx][str].Get(), vars...)
} }
} }
} }
} }
// Return the string we received by default // Return the string we received by default
return printf(str, vars...) return Printf(str, vars...)
} }
// GetNC retrieves the (N)th plural form of translation for the given string in the given context. // 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. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string { func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
// Sync read // Sync read
@@ -473,14 +440,14 @@ func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{})
if _, ok := po.contexts[ctx]; ok { if _, ok := po.contexts[ctx]; ok {
if po.contexts[ctx] != nil { if po.contexts[ctx] != nil {
if _, ok := po.contexts[ctx][str]; ok { if _, ok := po.contexts[ctx][str]; ok {
return printf(po.contexts[ctx][str].getN(po.pluralForm(n)), vars...) return Printf(po.contexts[ctx][str].GetN(po.pluralForm(n)), vars...)
} }
} }
} }
} }
if n == 1 { if n == 1 {
return printf(str, vars...) return Printf(str, vars...)
} }
return printf(plural, vars...) return Printf(plural, vars...)
} }

View File

@@ -1,11 +1,40 @@
/*
* 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 package gotext
import ( import (
"os" "os"
"path" "path"
"testing" "testing"
) )
func TestPo_Get(t *testing.T) {
// Create po object
po := new(Po)
// Try to parse a directory
po.ParseFile(path.Clean(os.TempDir()))
// Parse file
po.ParseFile("fixtures/en_US/default.po")
// Test translations
tr := po.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
}
// Test translations
tr = po.Get("language")
if tr != "en_US" {
t.Errorf("Expected 'en_US' but got '%s'", tr)
}
}
func TestPo(t *testing.T) { func TestPo(t *testing.T) {
// Set PO content // Set PO content
str := ` str := `
@@ -28,13 +57,15 @@ msgid "Another string"
msgstr "" msgstr ""
# Multi-line msgid # Multi-line msgid
msgid "multi" msgid ""
"multi"
"line" "line"
"id" "id"
msgstr "id with multiline content" msgstr "id with multiline content"
# Multi-line msgid_plural # Multi-line msgid_plural
msgid "multi" msgid ""
"multi"
"line" "line"
"plural" "plural"
"id" "id"
@@ -42,7 +73,8 @@ msgstr "plural id with multiline content"
#Multi-line string #Multi-line string
msgid "Multi-line" msgid "Multi-line"
msgstr "Multi " msgstr ""
"Multi "
"line" "line"
msgid "One with var: %s" msgid "One with var: %s"
@@ -58,13 +90,13 @@ msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s" msgstr[1] "This one is the plural in a Ctx context: %s"
msgid "Some random" msgid "Some random"
msgstr "Some random translation" msgstr "Some random Translation"
msgctxt "Ctx" msgctxt "Ctx"
msgid "Some random in a context" msgid "Some random in a context"
msgstr "Some random translation in a context" msgstr "Some random Translation in a context"
msgid "Empty translation" msgid "Empty Translation"
msgstr "" msgstr ""
msgid "Empty plural form singular" msgid "Empty plural form singular"
@@ -73,7 +105,7 @@ msgstr[0] "Singular translated"
msgstr[1] "" msgstr[1] ""
msgid "More" msgid "More"
msgstr "More translation" msgstr "More Translation"
` `
@@ -170,10 +202,10 @@ msgstr "More translation"
t.Errorf("Expected 'Original' but got '%s'", tr) t.Errorf("Expected 'Original' but got '%s'", tr)
} }
// Test empty translation strings // Test empty Translation strings
tr = po.Get("Empty translation") tr = po.Get("Empty Translation")
if tr != "Empty translation" { if tr != "Empty Translation" {
t.Errorf("Expected 'Empty translation' but got '%s'", tr) t.Errorf("Expected 'Empty Translation' but got '%s'", tr)
} }
tr = po.Get("Empty plural form singular") tr = po.Get("Empty plural form singular")
@@ -191,10 +223,10 @@ msgstr "More translation"
t.Errorf("Expected 'Empty plural form' but got '%s'", tr) t.Errorf("Expected 'Empty plural form' but got '%s'", tr)
} }
// Test last translation // Test last Translation
tr = po.Get("More") tr = po.Get("More")
if tr != "More translation" { if tr != "More Translation" {
t.Errorf("Expected 'More translation' but got '%s'", tr) t.Errorf("Expected 'More Translation' but got '%s'", tr)
} }
} }
@@ -215,7 +247,7 @@ msgstr[2] "TR Plural 2: %s"
` `
// Create po object // Create po object
po := new(Po) po := new(Po)
po.Parse(str) po.Parse([]byte(str))
v := "Var" v := "Var"
tr := po.GetN("Singular: %s", "Plural: %s", 2, v) tr := po.GetN("Singular: %s", "Plural: %s", 2, v)
@@ -229,7 +261,6 @@ msgstr[2] "TR Plural 2: %s"
} }
} }
func TestPluralNoHeaderInformation(t *testing.T) { func TestPluralNoHeaderInformation(t *testing.T) {
// Set PO content // Set PO content
str := ` str := `
@@ -246,7 +277,7 @@ msgstr[2] "TR Plural 2: %s"
` `
// Create po object // Create po object
po := new(Po) po := new(Po)
po.Parse(str) po.Parse([]byte(str))
v := "Var" v := "Var"
tr := po.GetN("Singular: %s", "Plural: %s", 2, v) tr := po.GetN("Singular: %s", "Plural: %s", 2, v)
@@ -281,7 +312,7 @@ msgstr "Translated example"
po := new(Po) po := new(Po)
// Parse // Parse
po.Parse(str) po.Parse([]byte(str))
// Check headers expected // Check headers expected
if po.Language != "en" { if po.Language != "en" {
@@ -305,9 +336,9 @@ msgstr "Translated example"
po := new(Po) po := new(Po)
// Parse // Parse
po.Parse(str) po.Parse([]byte(str))
// Check translation expected // Check Translation expected
if po.Get("Example") != "Translated example" { if po.Get("Example") != "Translated example" {
t.Errorf("Expected 'Translated example' but got '%s'", po.Get("Example")) t.Errorf("Expected 'Translated example' but got '%s'", po.Get("Example"))
} }
@@ -333,7 +364,7 @@ msgstr[3] "Plural form 3"
po := new(Po) po := new(Po)
// Parse // Parse
po.Parse(str) po.Parse([]byte(str))
// Check plural form // Check plural form
n := po.pluralForm(0) n := po.pluralForm(0)
@@ -378,7 +409,7 @@ msgstr[3] "Plural form 3"
po := new(Po) po := new(Po)
// Parse // Parse
po.Parse(str) po.Parse([]byte(str))
// Check plural form // Check plural form
n := po.pluralForm(0) n := po.pluralForm(0)
@@ -419,7 +450,7 @@ msgstr[3] "Plural form 3"
po := new(Po) po := new(Po)
// Parse // Parse
po.Parse(str) po.Parse([]byte(str))
// Check plural form // Check plural form
n := po.pluralForm(0) n := po.pluralForm(0)
@@ -469,7 +500,7 @@ msgstr[3] "Plural form 3"
po := new(Po) po := new(Po)
// Parse // Parse
po.Parse(str) po.Parse([]byte(str))
// Check plural form // Check plural form
n := po.pluralForm(1) n := po.pluralForm(1)
@@ -495,19 +526,18 @@ msgstr[3] "Plural form 3"
} }
func TestTranslationObject(t *testing.T) { func TestTranslationObject(t *testing.T) {
tr := newTranslation() tr := NewTranslation()
str := tr.get() str := tr.Get()
if str != "" { if str != "" {
t.Errorf("Expected '' but got '%s'", str) t.Errorf("Expected '' but got '%s'", str)
} }
// Set id // Set id
tr.id = "Text" tr.ID = "Text"
str = tr.Get()
// Get again // Get again
str = tr.get()
if str != "Text" { if str != "Text" {
t.Errorf("Expected 'Text' but got '%s'", str) t.Errorf("Expected 'Text' but got '%s'", str)
} }
@@ -540,11 +570,11 @@ msgstr[2] "And this is the second plural form: %s"
// Parse po content in a goroutine // Parse po content in a goroutine
go func(po *Po, done chan bool) { go func(po *Po, done chan bool) {
po.Parse(str) po.Parse([]byte(str))
done <- true done <- true
}(po, pc) }(po, pc)
// Read some translation on a goroutine // Read some Translation on a goroutine
go func(po *Po, done chan bool) { go func(po *Po, done chan bool) {
po.Get("My text") po.Get("My text")
done <- true done <- true
@@ -557,3 +587,33 @@ msgstr[2] "And this is the second plural form: %s"
<-pc <-pc
<-rc <-rc
} }
func TestNewPoTranslatorRace(t *testing.T) {
// Create Po object
mo := NewPoTranslator()
// 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.po")
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
}

48
translation.go Normal file
View File

@@ -0,0 +1,48 @@
/*
* 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
type Translation struct {
ID string
PluralID string
Trs map[int]string
}
func NewTranslation() *Translation {
tr := new(Translation)
tr.Trs = make(map[int]string)
return tr
}
func (t *Translation) Get() string {
// Look for Translation index 0
if _, ok := t.Trs[0]; ok {
if t.Trs[0] != "" {
return t.Trs[0]
}
}
// Return untranslated id by default
return t.ID
}
func (t *Translation) GetN(n int) string {
// Look for Translation index
if _, ok := t.Trs[n]; ok {
if t.Trs[n] != "" {
return t.Trs[n]
}
}
// Return untranslated singular if corresponding
if n == 0 {
return t.ID
}
// Return untranslated plural by default
return t.PluralID
}

15
translator.go Normal file
View File

@@ -0,0 +1,15 @@
/*
* 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
type Translator interface {
ParseFile(f string)
Parse(buf []byte)
Get(str string, vars ...interface{}) string
GetN(str, plural string, n int, vars ...interface{}) string
GetC(str, ctx string, vars ...interface{}) string
GetNC(str, plural string, n int, ctx string, vars ...interface{}) string
}