Fix GetN and GetNC to honor package domain. Refactor global package functions to make them all concurrent safe. Fixes #14

This commit is contained in:
Leonel Quinteros
2018-02-13 17:35:07 -03:00
parent 4d0fbfd720
commit c583d0991b
3 changed files with 209 additions and 45 deletions

View File

@@ -1,9 +1,15 @@
# CONTRIBUTING # CONTRIBUTING
This open source project welcomes everybody that wants to contribute to it by implementing new features, fixing bugs, testing, creating documentation or simply talk about it.
Most contributions will start by creating a new Issue to discuss what is the contribution about and to agree on the steps to move forward.
## Issues ## Issues
All issues reports are welcome. Open a new Issue whenever you want to report a bug, request a change or make a proposal. All issues reports are welcome. Open a new Issue whenever you want to report a bug, request a change or make a proposal.
This should be your start point of contribution.
## Pull Requests ## Pull Requests

108
gotext.go
View File

@@ -22,68 +22,110 @@ For quick/simple translations you can use the package level functions directly.
*/ */
package gotext package gotext
import "fmt" import (
"fmt"
"sync"
)
// Global environment variables // Global environment variables
var ( type config struct {
sync.RWMutex
// Default domain to look at when no domain is specified. Used by package level functions. // Default domain to look at when no domain is specified. Used by package level functions.
domain = "default" domain string
// Language set. // Language set.
language = "en_US" 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 = "/usr/local/share/locale" library string
// Storage for package level methods // Storage for package level methods
storage *Locale storage *Locale
) }
var globalConfig *config
// Init default configuration
func init() {
globalConfig = &config{
domain: "default",
language: "en_US",
library: "/usr/local/share/locale",
storage: nil,
}
}
// loadStorage creates a new Locale object at package level based on the Global variables settings. // loadStorage creates a new Locale object at package level based on the Global variables settings.
// It's called automatically when trying to use Get or GetD methods. // It's called automatically when trying to use Get or GetD methods.
func loadStorage(force bool) { func loadStorage(force bool) {
if storage == nil || force { globalConfig.Lock()
storage = NewLocale(library, language)
if globalConfig.storage == nil || force {
globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language)
} }
if _, ok := storage.domains[domain]; !ok || force { if _, ok := globalConfig.storage.domains[globalConfig.domain]; !ok || force {
storage.AddDomain(domain) globalConfig.storage.AddDomain(globalConfig.domain)
} }
globalConfig.Unlock()
} }
// GetDomain is the domain getter for the package configuration // GetDomain is the domain getter for the package configuration
func GetDomain() string { func GetDomain() string {
return domain globalConfig.RLock()
dom := globalConfig.domain
globalConfig.RUnlock()
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) {
domain = dom globalConfig.Lock()
globalConfig.domain = dom
globalConfig.Unlock()
loadStorage(true) loadStorage(true)
} }
// GetLanguage is the language getter for the package configuration // GetLanguage is the language getter for the package configuration
func GetLanguage() string { func GetLanguage() string {
return language globalConfig.RLock()
lang := globalConfig.language
globalConfig.RUnlock()
return lang
} }
// 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) {
language = lang globalConfig.Lock()
globalConfig.language = lang
globalConfig.Unlock()
loadStorage(true) loadStorage(true)
} }
// GetLibrary is the library getter for the package configuration // GetLibrary is the library getter for the package configuration
func GetLibrary() string { func GetLibrary() string {
return library globalConfig.RLock()
lib := globalConfig.library
globalConfig.RUnlock()
return lib
} }
// 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) {
library = lib globalConfig.Lock()
globalConfig.library = lib
globalConfig.Unlock()
loadStorage(true) loadStorage(true)
} }
@@ -92,9 +134,13 @@ func SetLibrary(lib string) {
// 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) {
library = lib globalConfig.Lock()
language = lang
domain = dom globalConfig.library = lib
globalConfig.language = lang
globalConfig.domain = dom
globalConfig.Unlock()
loadStorage(true) loadStorage(true)
} }
@@ -102,13 +148,13 @@ func Configure(lib, lang, dom string) {
// 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(domain, 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("default", 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.
@@ -124,19 +170,23 @@ func GetND(dom, str, plural string, n int, vars ...interface{}) string {
loadStorage(false) loadStorage(false)
// Return translation // Return translation
return storage.GetND(dom, str, plural, n, vars...) globalConfig.RLock()
tr := globalConfig.storage.GetND(dom, str, plural, n, vars...)
globalConfig.RUnlock()
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(domain, 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("default", 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.
@@ -152,7 +202,11 @@ func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) str
loadStorage(false) loadStorage(false)
// Return translation // Return translation
return storage.GetNDC(dom, str, plural, n, ctx, vars...) 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. // printf applies text formatting only when needed to parse variables.

View File

@@ -82,14 +82,14 @@ msgstr[1] ""
` `
// Create Locales directory on default location // Create Locales directory on default location
dirname := path.Clean("/tmp" + string(os.PathSeparator) + "en_US") dirname := path.Join("/tmp", "en_US")
err := os.MkdirAll(dirname, os.ModePerm) err := os.MkdirAll(dirname, os.ModePerm)
if err != nil { if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error()) t.Fatalf("Can't create test directory: %s", err.Error())
} }
// Write PO content to default domain file // Write PO content to default domain file
filename := path.Clean(dirname + string(os.PathSeparator) + "default.po") filename := path.Join(dirname, "default.po")
f, err := os.Create(filename) f, err := os.Create(filename)
if err != nil { if err != nil {
@@ -161,14 +161,14 @@ msgstr[1] ""
` `
// Create Locales directory on default location // Create Locales directory on default location
dirname := path.Clean("/tmp" + string(os.PathSeparator) + "en_US") dirname := path.Join("/tmp", "en_US")
err := os.MkdirAll(dirname, os.ModePerm) err := os.MkdirAll(dirname, os.ModePerm)
if err != nil { if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error()) t.Fatalf("Can't create test directory: %s", err.Error())
} }
// Write PO content to default domain file // Write PO content to default domain file
filename := path.Clean(dirname + string(os.PathSeparator) + "default.po") filename := path.Join(dirname, "default.po")
f, err := os.Create(filename) f, err := os.Create(filename)
if err != nil { if err != nil {
@@ -214,6 +214,108 @@ msgstr[1] ""
} }
} }
func TestDomains(t *testing.T) {
// Set PO content
strDefault := `
msgid ""
msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Default text"
msgid_plural "Default texts"
msgstr[0] "Default translation"
msgstr[1] "Default translations"
msgctxt "Ctx"
msgid "Default context"
msgid_plural "Default contexts"
msgstr[0] "Default ctx translation"
msgstr[1] "Default ctx translations"
`
strCustom := `
msgid ""
msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Custom text"
msgid_plural "Custom texts"
msgstr[0] "Custom translation"
msgstr[1] "Custom translations"
msgctxt "Ctx"
msgid "Custom context"
msgid_plural "Custom contexts"
msgstr[0] "Custom ctx translation"
msgstr[1] "Custom ctx translations"
`
// Create Locales directory and files on temp location
dirname := path.Join("/tmp", "en_US")
err := os.MkdirAll(dirname, os.ModePerm)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
fDefault, err := os.Create(path.Join(dirname, "default.po"))
if err != nil {
t.Fatalf("Can't create test file: %s", err.Error())
}
defer fDefault.Close()
fCustom, err := os.Create(path.Join(dirname, "custom.po"))
if err != nil {
t.Fatalf("Can't create test file: %s", err.Error())
}
defer fCustom.Close()
_, err = fDefault.WriteString(strDefault)
if err != nil {
t.Fatalf("Can't write to test file: %s", err.Error())
}
_, err = fCustom.WriteString(strCustom)
if err != nil {
t.Fatalf("Can't write to test file: %s", err.Error())
}
Configure("/tmp", "en_US", "default")
// Check default domain translation
SetDomain("default")
tr := Get("Default text")
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)
}
tr = GetNC("Default context", "Default contexts", 23, "Ctx")
if tr != "Default ctx translations" {
t.Errorf("Expected 'Default ctx translations'. Got '%s'", tr)
}
SetDomain("custom")
tr = Get("Custom text")
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)
}
tr = GetNC("Custom context", "Custom contexts", 23, "Ctx")
if tr != "Custom ctx translations" {
t.Errorf("Expected 'Custom ctx translations'. Got '%s'", tr)
}
}
func TestPackageRace(t *testing.T) { func TestPackageRace(t *testing.T) {
// Set PO content // Set PO content
str := `# Some comment str := `# Some comment
@@ -230,17 +332,21 @@ msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s" msgstr[1] "This one is the plural: %s"
msgstr[2] "And this is the second plural form: %s" 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"
` `
// Create Locales directory on default location // Create Locales directory on default location
dirname := path.Clean(library + string(os.PathSeparator) + "en_US") dirname := path.Join("/tmp", "en_US")
err := os.MkdirAll(dirname, os.ModePerm) err := os.MkdirAll(dirname, os.ModePerm)
if err != nil { if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error()) t.Fatalf("Can't create test directory: %s", err.Error())
} }
// Write PO content to default domain file // Write PO content to default domain file
filename := path.Clean(dirname + string(os.PathSeparator) + domain + ".po") filename := path.Join("/tmp", GetDomain()+".po")
f, err := os.Create(filename) f, err := os.Create(filename)
if err != nil { if err != nil {
@@ -255,26 +361,24 @@ msgstr[2] "And this is the second plural form: %s"
var wg sync.WaitGroup var wg sync.WaitGroup
for i := 0; i < 100; i++ { for i := 0; i < 1000; i++ {
wg.Add(1) wg.Add(1)
// Test translations // Test translations
go func() { go func() {
defer wg.Done() defer wg.Done()
GetLibrary()
SetLibrary(path.Join("/tmp", "gotextlib"))
GetDomain()
SetDomain("default")
GetLanguage()
SetLanguage("en_US")
Configure("/tmp", "en_US", "default")
Get("My text") Get("My text")
GetN("One with var: %s", "Several with vars: %s", 0, "test") GetN("One with var: %s", "Several with vars: %s", 0, "test")
GetC("Some random in a context", "Ctx")
}() }()
wg.Add(1)
go func() {
defer wg.Done()
Get("My text")
GetN("One with var: %s", "Several with vars: %s", 1, "test")
}()
Get("My text")
GetN("One with var: %s", "Several with vars: %s", 2, "test")
} }
wg.Wait() wg.Wait()