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
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
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

108
gotext.go
View file

@ -22,68 +22,110 @@ For quick/simple translations you can use the package level functions directly.
*/
package gotext
import "fmt"
import (
"fmt"
"sync"
)
// Global environment variables
var (
type config struct {
sync.RWMutex
// Default domain to look at when no domain is specified. Used by package level functions.
domain = "default"
domain string
// Language set.
language = "en_US"
language string
// 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 *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.
// It's called automatically when trying to use Get or GetD methods.
func loadStorage(force bool) {
if storage == nil || force {
storage = NewLocale(library, language)
globalConfig.Lock()
if globalConfig.storage == nil || force {
globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language)
}
if _, ok := storage.domains[domain]; !ok || force {
storage.AddDomain(domain)
if _, ok := globalConfig.storage.domains[globalConfig.domain]; !ok || force {
globalConfig.storage.AddDomain(globalConfig.domain)
}
globalConfig.Unlock()
}
// GetDomain is the domain getter for the package configuration
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.
// It reloads the corresponding translation file.
func SetDomain(dom string) {
domain = dom
globalConfig.Lock()
globalConfig.domain = dom
globalConfig.Unlock()
loadStorage(true)
}
// GetLanguage is the language getter for the package configuration
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.
// It reloads the corresponding translation file.
func SetLanguage(lang string) {
language = lang
globalConfig.Lock()
globalConfig.language = lang
globalConfig.Unlock()
loadStorage(true)
}
// GetLibrary is the library getter for the package configuration
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.
// It reloads the corresponding translation file.
func SetLibrary(lib string) {
library = lib
globalConfig.Lock()
globalConfig.library = lib
globalConfig.Unlock()
loadStorage(true)
}
@ -92,9 +134,13 @@ func SetLibrary(lib string) {
// 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.
func Configure(lib, lang, dom string) {
library = lib
language = lang
domain = dom
globalConfig.Lock()
globalConfig.library = lib
globalConfig.language = lang
globalConfig.domain = dom
globalConfig.Unlock()
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.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func Get(str string, vars ...interface{}) string {
return GetD(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.
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.
@ -124,19 +170,23 @@ func GetND(dom, str, plural string, n int, vars ...interface{}) string {
loadStorage(false)
// 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.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetC(str, ctx string, vars ...interface{}) string {
return GetDC(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.
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.
@ -152,7 +202,11 @@ func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) str
loadStorage(false)
// 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.

View file

@ -82,14 +82,14 @@ msgstr[1] ""
`
// 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)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
// 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)
if err != nil {
@ -161,14 +161,14 @@ msgstr[1] ""
`
// 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)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
// 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)
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) {
// Set PO content
str := `# Some comment
@ -230,17 +332,21 @@ msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %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
dirname := path.Clean(library + string(os.PathSeparator) + "en_US")
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())
}
// 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)
if err != nil {
@ -255,26 +361,24 @@ msgstr[2] "And this is the second plural form: %s"
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
for i := 0; i < 1000; i++ {
wg.Add(1)
// Test translations
go func() {
defer wg.Done()
GetLibrary()
SetLibrary(path.Join("/tmp", "gotextlib"))
GetDomain()
SetDomain("default")
GetLanguage()
SetLanguage("en_US")
Configure("/tmp", "en_US", "default")
Get("My text")
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()