diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7103f49..56957d0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/gotext.go b/gotext.go index 213dba6..c5c636d 100644 --- a/gotext.go +++ b/gotext.go @@ -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. diff --git a/gotext_test.go b/gotext_test.go index a9d87a6..9e173bd 100644 --- a/gotext_test.go +++ b/gotext_test.go @@ -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() - Get("My text") - GetN("One with var: %s", "Several with vars: %s", 0, "test") - }() - - wg.Add(1) - 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", 1, "test") + GetN("One with var: %s", "Several with vars: %s", 0, "test") + GetC("Some random in a context", "Ctx") }() - - Get("My text") - GetN("One with var: %s", "Several with vars: %s", 2, "test") } wg.Wait()