diff --git a/gotext.go b/gotext.go index a999261..e56be51 100644 --- a/gotext.go +++ b/gotext.go @@ -24,6 +24,7 @@ package gotext import ( "encoding/gob" + "strings" "sync" ) @@ -31,47 +32,56 @@ import ( type config struct { sync.RWMutex + // Path to library directory where all locale directories and Translation files are. + library string + // Default domain to look at when no domain is specified. Used by package level functions. domain string // Language set. - language string - - // Path to library directory where all locale directories and Translation files are. - library string + languages []string // Storage for package level methods - storage *Locale + locales []*Locale } var globalConfig *config +var FallbackLocale = "en_US" + func init() { // Init default configuration globalConfig = &config{ - domain: "default", - language: "en_US", - library: "/usr/local/share/locale", - storage: nil, + domain: "default", + languages: []string{FallbackLocale}, + library: "/usr/local/share/locale", + locales: nil, } // Register Translator types for gob encoding gob.Register(TranslatorEncoding{}) } -// 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) { +// loadLocales creates a new Locale object for every language (specified using Configure) +// at package level based on the configuration of global configuration . +// It is called when trying to use Get or GetD methods. +func loadLocales(rebuildCache bool) { globalConfig.Lock() - if globalConfig.storage == nil || force { - globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language) + if globalConfig.locales == nil || rebuildCache { + var locales []*Locale + for _, language := range globalConfig.languages { + locales = append(locales, NewLocale(globalConfig.library, language)) + } + globalConfig.locales = locales } - if _, ok := globalConfig.storage.Domains[globalConfig.domain]; !ok || force { - globalConfig.storage.AddDomain(globalConfig.domain) + for _, locale := range globalConfig.locales { + if _, ok := locale.Domains[globalConfig.domain]; !ok || rebuildCache { + locale.AddDomain(globalConfig.domain) + } + locale.SetDomain(globalConfig.domain) } - globalConfig.storage.SetDomain(globalConfig.domain) globalConfig.Unlock() } @@ -80,8 +90,9 @@ func loadStorage(force bool) { func GetDomain() string { var dom string globalConfig.RLock() - if globalConfig.storage != nil { - dom = globalConfig.storage.GetDomain() + if globalConfig.locales != nil { + // All locales have the same domain + dom = globalConfig.locales[0].GetDomain() } if dom == "" { dom = globalConfig.domain @@ -96,31 +107,46 @@ func GetDomain() string { func SetDomain(dom string) { globalConfig.Lock() globalConfig.domain = dom - if globalConfig.storage != nil { - globalConfig.storage.SetDomain(dom) + if globalConfig.locales != nil { + for _, locale := range globalConfig.locales { + locale.SetDomain(dom) + } } globalConfig.Unlock() - loadStorage(true) + loadLocales(true) } -// GetLanguage is the language getter for the package configuration +// GetLanguage returns the language gotext will translate into. +// If multiple languages have been supplied, the first one will be returned. +// If no language has been supplied, the fallback will be returned. func GetLanguage() string { - globalConfig.RLock() - lang := globalConfig.language - globalConfig.RUnlock() + languages := GetLanguages() + if len(languages) == 0 { + return FallbackLocale + } + return languages[0] +} - return lang +// GetLanguages returns all languages that have been supplied. +func GetLanguages() []string { + globalConfig.RLock() + defer globalConfig.RUnlock() + return globalConfig.languages } -// SetLanguage sets the language code to be used at package level. +// SetLanguage sets the language code (or colon separated language codes) to be used at package level. // It reloads the corresponding Translation file. func SetLanguage(lang string) { globalConfig.Lock() - globalConfig.language = SimplifiedLocale(lang) + var languages []string + for _, language := range strings.Split(lang, ":") { + languages = append(languages, SimplifiedLocale(language)) + } + globalConfig.languages = languages globalConfig.Unlock() - loadStorage(true) + loadLocales(true) } // GetLibrary is the library getter for the package configuration @@ -132,38 +158,56 @@ func GetLibrary() string { 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 locale directories and files to be used at package level. // It reloads the corresponding Translation file. func SetLibrary(lib string) { globalConfig.Lock() globalConfig.library = lib globalConfig.Unlock() - loadStorage(true) + loadLocales(true) } -// GetStorage is the locale storage getter for the package configuration. -func GetStorage() *Locale { +func GetLocales() []*Locale { globalConfig.RLock() - storage := globalConfig.storage - globalConfig.RUnlock() + defer globalConfig.RUnlock() + return globalConfig.locales +} - return storage +// GetStorage is the locale storage getter for the package configuration. +// +// Deprecated: Storage has been renamed to Locale for consistency, use GetLocales instead. +func GetStorage() *Locale { + locales := GetLocales() + if len(locales) > 0 { + return locales[0] + } + return nil } -// SetStorage allows overridding the global Locale object with one built manually with NewLocale(). -// This allows then to attach to the locale Domains object in memory po or mo files (embedded or in any directory), -// for each domain. -// Locale library, language and domain properties will apply on default global configuration. -// Any domain not loaded yet will use to the just in time domain loading process. -// Note that any call to gotext.Set* or Configure will invalidate this override. -func SetStorage(storage *Locale) { +// SetLocales allows for overriding the global Locale objects with ones built manually with +// NewLocale(). This makes it possible to attach custom Domain objects from in-memory po/mo. +// The library, language and domain of the first Locale will set the default global configuration. +func SetLocales(locales []*Locale) { globalConfig.Lock() - globalConfig.storage = storage - globalConfig.library = storage.path - globalConfig.language = storage.lang - globalConfig.domain = storage.defaultDomain - globalConfig.Unlock() + defer globalConfig.Unlock() + + globalConfig.locales = locales + globalConfig.library = locales[0].path + globalConfig.domain = locales[0].defaultDomain + + var languages []string + for _, locale := range locales { + languages = append(languages, locale.lang) + } + globalConfig.languages = languages +} + +// SetStorage allows overriding the global Locale object with one built manually with NewLocale(). +// +// Deprecated: Storage has been renamed to Locale for consistency, use SetLocales instead. +func SetStorage(locale *Locale) { + SetLocales([]*Locale{locale}) } // Configure sets all configuration variables to be used at package level and reloads the corresponding Translation file. @@ -173,11 +217,15 @@ func SetStorage(storage *Locale) { func Configure(lib, lang, dom string) { globalConfig.Lock() globalConfig.library = lib - globalConfig.language = SimplifiedLocale(lang) + var languages []string + for _, language := range strings.Split(lang, ":") { + languages = append(languages, SimplifiedLocale(language)) + } + globalConfig.languages = languages globalConfig.domain = dom globalConfig.Unlock() - loadStorage(true) + loadLocales(true) } // Get uses the default domain globally set to return the corresponding Translation of a given string. @@ -195,38 +243,46 @@ func GetN(str, plural string, n int, vars ...interface{}) string { // GetD returns the corresponding Translation in the given domain for a given string. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func GetD(dom, str string, vars ...interface{}) string { - // Try to load default package Locale storage - loadStorage(false) + // Try to load default package Locales + loadLocales(false) - // Return Translation globalConfig.RLock() + defer globalConfig.RUnlock() - if _, ok := globalConfig.storage.Domains[dom]; !ok { - globalConfig.storage.AddDomain(dom) + var tr string + for i, locale := range globalConfig.locales { + if _, ok := locale.Domains[dom]; !ok { + locale.AddDomain(dom) + } + if !locale.IsTranslatedD(dom, str) && i < (len(globalConfig.locales)-1) { + continue + } + tr = locale.GetD(dom, str, vars...) + break } - - tr := globalConfig.storage.GetD(dom, str, vars...) - globalConfig.RUnlock() - return tr } // GetND retrieves the (N)th plural form of Translation in the given domain for a given string. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func GetND(dom, str, plural string, n int, vars ...interface{}) string { - // Try to load default package Locale storage - loadStorage(false) + // Try to load default package Locales + loadLocales(false) - // Return Translation globalConfig.RLock() + defer globalConfig.RUnlock() - if _, ok := globalConfig.storage.Domains[dom]; !ok { - globalConfig.storage.AddDomain(dom) + var tr string + for i, locale := range globalConfig.locales { + if _, ok := locale.Domains[dom]; !ok { + locale.AddDomain(dom) + } + if !locale.IsTranslatedND(dom, str, n) && i < (len(globalConfig.locales)-1) { + continue + } + tr = locale.GetND(dom, str, plural, n, vars...) + break } - - tr := globalConfig.storage.GetND(dom, str, plural, n, vars...) - globalConfig.RUnlock() - return tr } @@ -245,81 +301,126 @@ func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string { // GetDC returns the corresponding Translation in the given domain for the given string in the given context. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func GetDC(dom, str, ctx string, vars ...interface{}) string { - // Try to load default package Locale storage - loadStorage(false) + // Try to load default package Locales + loadLocales(false) - // Return Translation globalConfig.RLock() - tr := globalConfig.storage.GetDC(dom, str, ctx, vars...) - globalConfig.RUnlock() + defer globalConfig.RUnlock() + var tr string + for i, locale := range globalConfig.locales { + if !locale.IsTranslatedDC(dom, str, ctx) && i < (len(globalConfig.locales)-1) { + continue + } + tr = locale.GetDC(dom, str, ctx, vars...) + break + } return tr } // GetNDC retrieves the (N)th plural form of Translation in the given domain for a given string. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string { - // Try to load default package Locale storage - loadStorage(false) + // Try to load default package Locales + loadLocales(false) // Return Translation globalConfig.RLock() - tr := globalConfig.storage.GetNDC(dom, str, plural, n, ctx, vars...) - globalConfig.RUnlock() + defer globalConfig.RUnlock() + var tr string + for i, locale := range globalConfig.locales { + if !locale.IsTranslatedNDC(dom, str, n, ctx) && i < (len(globalConfig.locales)-1) { + continue + } + tr = locale.GetNDC(dom, str, plural, n, ctx, vars...) + break + } return tr } -// IsTranslated reports whether a string is translated -func IsTranslated(str string) bool { - return IsTranslatedND(GetDomain(), str, 0) +// IsTranslated reports whether a string is translated in given languages. +// When the langs argument is omitted, the output of GetLanguages is used. +func IsTranslated(str string, langs ...string) bool { + return IsTranslatedND(GetDomain(), str, 0, langs...) } -// IsTranslatedN reports whether a plural string is translated -func IsTranslatedN(str string, n int) bool { - return IsTranslatedND(GetDomain(), str, n) +// IsTranslatedN reports whether a plural string is translated in given languages. +// When the langs argument is omitted, the output of GetLanguages is used. +func IsTranslatedN(str string, n int, langs ...string) bool { + return IsTranslatedND(GetDomain(), str, n, langs...) } -// IsTranslatedD reports whether a domain string is translated -func IsTranslatedD(dom, str string) bool { - return IsTranslatedND(dom, str, 0) +// IsTranslatedD reports whether a domain string is translated in given languages. +// When the langs argument is omitted, the output of GetLanguages is used. +func IsTranslatedD(dom, str string, langs ...string) bool { + return IsTranslatedND(dom, str, 0, langs...) } -// IsTranslatedND reports whether a plural domain string is translated -func IsTranslatedND(dom, str string, n int) bool { - loadStorage(false) +// IsTranslatedND reports whether a plural domain string is translated in any of given languages. +// When the langs argument is omitted, the output of GetLanguages is used. +func IsTranslatedND(dom, str string, n int, langs ...string) bool { + if len(langs) == 0 { + langs = GetLanguages() + } + + loadLocales(false) globalConfig.RLock() defer globalConfig.RUnlock() - if _, ok := globalConfig.storage.Domains[dom]; !ok { - globalConfig.storage.AddDomain(dom) - } + for _, lang := range langs { + lang = SimplifiedLocale(lang) - return globalConfig.storage.IsTranslatedND(dom, str, n) + for _, supportedLocale := range globalConfig.locales { + if lang != supportedLocale.GetActualLanguage(dom) { + continue + } + return supportedLocale.IsTranslatedND(dom, str, n) + } + } + return false } -// IsTranslatedC reports whether a context string is translated -func IsTranslatedC(str, ctx string) bool { - return IsTranslatedNDC(GetDomain(), str, 0, ctx) +// IsTranslatedC reports whether a context string is translated in given languages. +// When the langs argument is omitted, the output of GetLanguages is used. +func IsTranslatedC(str, ctx string, langs ...string) bool { + return IsTranslatedNDC(GetDomain(), str, 0, ctx, langs...) } -// IsTranslatedNC reports whether a plural context string is translated -func IsTranslatedNC(str string, n int, ctx string) bool { - return IsTranslatedNDC(GetDomain(), str, n, ctx) +// IsTranslatedNC reports whether a plural context string is translated in given languages. +// When the langs argument is omitted, the output of GetLanguages is used. +func IsTranslatedNC(str string, n int, ctx string, langs ...string) bool { + return IsTranslatedNDC(GetDomain(), str, n, ctx, langs...) } -// IsTranslatedDC reports whether a domain context string is translated -func IsTranslatedDC(dom, str, ctx string) bool { - return IsTranslatedNDC(dom, str, 0, ctx) +// IsTranslatedDC reports whether a domain context string is translated in given languages. +// When the langs argument is omitted, the output of GetLanguages is used. +func IsTranslatedDC(dom, str, ctx string, langs ...string) bool { + return IsTranslatedNDC(dom, str, 0, ctx, langs...) } -// IsTranslatedNDC reports whether a plural domain context string is translated -func IsTranslatedNDC(dom, str string, n int, ctx string) bool { - loadStorage(false) +// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages. +// When the langs argument is omitted, the output of GetLanguages is used. +func IsTranslatedNDC(dom, str string, n int, ctx string, langs ...string) bool { + if len(langs) == 0 { + langs = GetLanguages() + } + + loadLocales(false) globalConfig.RLock() defer globalConfig.RUnlock() - return globalConfig.storage.IsTranslatedNDC(dom, str, n, ctx) + for _, lang := range langs { + lang = SimplifiedLocale(lang) + + for _, locale := range globalConfig.locales { + if lang != locale.GetActualLanguage(dom) { + continue + } + return locale.IsTranslatedNDC(dom, str, n, ctx) + } + } + return false } diff --git a/gotext_test.go b/gotext_test.go index 561ea69..5f30c6b 100644 --- a/gotext_test.go +++ b/gotext_test.go @@ -32,11 +32,11 @@ func TestGettersSetters(t *testing.T) { // Create Locale with full language code l := NewLocale("fixtures/", "fr_FR") - SetStorage(l) - storage := GetStorage() + SetLocales([]*Locale{l}) + locale := GetLocales()[0] - if storage != l { - t.Errorf("Expected GetStorage to return provided locale storage %v, but got '%v'", storage, l) + if locale != l { + t.Errorf("Expected GetLocale to return provided locale locale %v, but got '%v'", locale, l) } } @@ -186,13 +186,29 @@ msgstr "Another text on another domain" t.Errorf("Expected 'Another text on another domain' but got '%s'", tr) } - // Test IsTranslation functions + // IsTranslated tests for when the string is translated in English. if !IsTranslated("My text") { - t.Error("'My text' should be reported as translated.") + t.Error("'My text' should be reported as translated when 'langs' is omitted.") } + if !IsTranslated("My text", "en_US") { + t.Error("'My text' should be reported as translated when 'langs' is 'en_US'.") + } + if IsTranslated("My text", "cs_CZ") { + t.Error("'My text' should be reported as not translated when 'langs' is 'cs_CZ'.") + } + if !IsTranslated("My text", "en_US", "cs_CZ") { + t.Error("'My text' should be reported as translated when 'langs' is 'en_US, cs_CZ'.") + } + + // IsTranslated tests for when the string is not translated in English if IsTranslated("Another string") { t.Error("'Another string' should be reported as not translated.") } + if IsTranslated("String not in .po") { + t.Error("'String not in .po' should be reported as not translated.") + } + + // IsTranslated tests for plurals and contexts plural := "One with var: %s" if !IsTranslated(plural) { t.Errorf("'%s' should be reported as translated for singular.", plural) @@ -420,7 +436,7 @@ msgstr[1] "Custom ctx translations" } } -func TestOverrideStorage(t *testing.T) { +func TestOverrideLocale(t *testing.T) { // Configure global translation Configure("fixtures/", "de_DE", "default") @@ -432,14 +448,14 @@ func TestOverrideStorage(t *testing.T) { // Create and override with our new locale. l := NewLocale("fixtures/", "fr") l.SetDomain("default") - SetStorage(l) + SetLocales([]*Locale{l}) language = Get("language") if language != "fr" { t.Errorf("Expected default configuration to be overriden by locale 'fr' but got '%s'", language) } - // Ensure properties were applied on globale configuration when Set. + // Ensure properties were applied on global configuration when Set. dom := GetDomain() if dom != "default" { t.Errorf("Expected GetDomain to return 'default', but got '%s'", dom) diff --git a/locale.go b/locale.go index c39d730..ac204dd 100644 --- a/locale.go +++ b/locale.go @@ -111,6 +111,40 @@ func (l *Locale) findExt(dom, ext string) string { return "" } +// GetActualLanguage inspects the filesystem and decides whether to strip +// a CC part of the ll_CC locale string. +func (l *Locale) GetActualLanguage(dom string) string { + extensions := []string{"mo", "po"} + var fp string + for _, ext := range extensions { + // 'll' (or 'll_CC') exists, and it was specified as-is + fp = path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext) + if l.fileExists(fp) { + return l.lang + } + // 'll' exists, but 'll_CC' was specified + if len(l.lang) > 2 { + fp = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext) + if l.fileExists(fp) { + return l.lang[:2] + } + } + // 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is + fp = path.Join(l.path, l.lang, dom+"."+ext) + if l.fileExists(fp) { + return l.lang + } + // 'll' exists outside of LC_category, but 'll_CC' was specified + if len(l.lang) > 2 { + fp = path.Join(l.path, l.lang[:2], dom+"."+ext) + if l.fileExists(fp) { + return l.lang[:2] + } + } + } + return "" +} + func (l *Locale) fileExists(filename string) bool { if l.fs != nil { _, err := fs.Stat(l.fs, filename)