Skip to content

Commit 7125c7d

Browse files
committed
Add support for multiple languages
gettext supports supplying multiple languages using the colon character $ LC_ALL=pt_BR:pt_PT:en_US foo bar which are used as fallback: when a translation for the first one is not available, the second language is used. If no language contains a translation for the string `msgid` is returned. This patch adds this support into gotext. 1/ config struct - 'language' was renamed to 'languages' and is a slice of strings - 'storage' was renamed to 'locales' and is a slice of Locale pointers 2/ loadStorage() - all loaded languages are iterated over 3/ GetLanguages() - new function returns the languages from the config - GetLanguage() uses the first element of it, keeping the compatibility 4/ SetLanguage(), Configure() - the language string is split at colon and iterated over 5/ Get*() - languages are iterated and the first translation for given string is returned 6/ IsTranslated*() - new optional parameter (langs) has been added 7/ Locale.GetActualLanguage() - it checks the filesystem and determines what the actual language code is: for 'cs_CZ', just 'cs' may be returned, depending on the actual name of the .mo/.po file.
1 parent 4829902 commit 7125c7d

File tree

3 files changed

+201
-75
lines changed

3 files changed

+201
-75
lines changed

gotext.go

Lines changed: 149 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -24,54 +24,62 @@ package gotext
2424

2525
import (
2626
"encoding/gob"
27+
"strings"
2728
"sync"
2829
)
2930

3031
// Global environment variables
3132
type config struct {
3233
sync.RWMutex
3334

35+
// Path to library directory where all locale directories and Translation files are.
36+
library string
37+
3438
// Default domain to look at when no domain is specified. Used by package level functions.
3539
domain string
3640

3741
// Language set.
38-
language string
39-
40-
// Path to library directory where all locale directories and Translation files are.
41-
library string
42+
languages []string
4243

4344
// Storage for package level methods
44-
storage *Locale
45+
locales []*Locale
4546
}
4647

4748
var globalConfig *config
4849

4950
func init() {
5051
// Init default configuration
5152
globalConfig = &config{
52-
domain: "default",
53-
language: "en_US",
54-
library: "/usr/local/share/locale",
55-
storage: nil,
53+
domain: "default",
54+
languages: []string{"en_US"},
55+
library: "/usr/local/share/locale",
56+
locales: nil,
5657
}
5758

5859
// Register Translator types for gob encoding
5960
gob.Register(TranslatorEncoding{})
6061
}
6162

62-
// loadStorage creates a new Locale object at package level based on the Global variables settings.
63+
// loadStorage creates a new Locale object at package level based on the Global variables settings
64+
// for every language specified using Configure.
6365
// It's called automatically when trying to use Get or GetD methods.
6466
func loadStorage(force bool) {
6567
globalConfig.Lock()
6668

67-
if globalConfig.storage == nil || force {
68-
globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language)
69+
if globalConfig.locales == nil || force {
70+
var locales []*Locale
71+
for _, language := range globalConfig.languages {
72+
locales = append(locales, NewLocale(globalConfig.library, language))
73+
}
74+
globalConfig.locales = locales
6975
}
7076

71-
if _, ok := globalConfig.storage.Domains[globalConfig.domain]; !ok || force {
72-
globalConfig.storage.AddDomain(globalConfig.domain)
77+
for _, locale := range globalConfig.locales {
78+
if _, ok := locale.Domains[globalConfig.domain]; !ok || force {
79+
locale.AddDomain(globalConfig.domain)
80+
}
81+
locale.SetDomain(globalConfig.domain)
7382
}
74-
globalConfig.storage.SetDomain(globalConfig.domain)
7583

7684
globalConfig.Unlock()
7785
}
@@ -80,8 +88,9 @@ func loadStorage(force bool) {
8088
func GetDomain() string {
8189
var dom string
8290
globalConfig.RLock()
83-
if globalConfig.storage != nil {
84-
dom = globalConfig.storage.GetDomain()
91+
if globalConfig.locales != nil {
92+
// All locales have the same domain
93+
dom = globalConfig.locales[0].GetDomain()
8594
}
8695
if dom == "" {
8796
dom = globalConfig.domain
@@ -96,28 +105,38 @@ func GetDomain() string {
96105
func SetDomain(dom string) {
97106
globalConfig.Lock()
98107
globalConfig.domain = dom
99-
if globalConfig.storage != nil {
100-
globalConfig.storage.SetDomain(dom)
108+
if globalConfig.locales != nil {
109+
for _, locale := range globalConfig.locales {
110+
locale.SetDomain(dom)
111+
}
101112
}
102113
globalConfig.Unlock()
103114

104115
loadStorage(true)
105116
}
106117

107-
// GetLanguage is the language getter for the package configuration
118+
// GetLanguage returns the language gotext will translate into.
119+
// If multiple languages have been supplied, the first one will be returned.
108120
func GetLanguage() string {
109-
globalConfig.RLock()
110-
lang := globalConfig.language
111-
globalConfig.RUnlock()
121+
return GetLanguages()[0]
122+
}
112123

113-
return lang
124+
// GetLanguages returns all languages that have been supplied.
125+
func GetLanguages() []string {
126+
globalConfig.RLock()
127+
defer globalConfig.RUnlock()
128+
return globalConfig.languages
114129
}
115130

116-
// SetLanguage sets the language code to be used at package level.
131+
// SetLanguage sets the language code (or colon separated language codes) to be used at package level.
117132
// It reloads the corresponding Translation file.
118133
func SetLanguage(lang string) {
119134
globalConfig.Lock()
120-
globalConfig.language = SimplifiedLocale(lang)
135+
var languages []string
136+
for _, language := range strings.Split(lang, ":") {
137+
languages = append(languages, SimplifiedLocale(language))
138+
}
139+
globalConfig.languages = languages
121140
globalConfig.Unlock()
122141

123142
loadStorage(true)
@@ -149,7 +168,11 @@ func SetLibrary(lib string) {
149168
func Configure(lib, lang, dom string) {
150169
globalConfig.Lock()
151170
globalConfig.library = lib
152-
globalConfig.language = SimplifiedLocale(lang)
171+
var languages []string
172+
for _, language := range strings.Split(lang, ":") {
173+
languages = append(languages, SimplifiedLocale(language))
174+
}
175+
globalConfig.languages = languages
153176
globalConfig.domain = dom
154177
globalConfig.Unlock()
155178

@@ -174,16 +197,20 @@ func GetD(dom, str string, vars ...interface{}) string {
174197
// Try to load default package Locale storage
175198
loadStorage(false)
176199

177-
// Return Translation
178200
globalConfig.RLock()
201+
defer globalConfig.RUnlock()
179202

180-
if _, ok := globalConfig.storage.Domains[dom]; !ok {
181-
globalConfig.storage.AddDomain(dom)
203+
var tr string
204+
for i, locale := range globalConfig.locales {
205+
if _, ok := locale.Domains[dom]; !ok {
206+
locale.AddDomain(dom)
207+
}
208+
if !locale.IsTranslatedD(dom, str) && i < (len(globalConfig.locales)-1) {
209+
continue
210+
}
211+
tr = locale.GetD(dom, str, vars...)
212+
break
182213
}
183-
184-
tr := globalConfig.storage.GetD(dom, str, vars...)
185-
globalConfig.RUnlock()
186-
187214
return tr
188215
}
189216

@@ -193,16 +220,20 @@ func GetND(dom, str, plural string, n int, vars ...interface{}) string {
193220
// Try to load default package Locale storage
194221
loadStorage(false)
195222

196-
// Return Translation
197223
globalConfig.RLock()
224+
defer globalConfig.RUnlock()
198225

199-
if _, ok := globalConfig.storage.Domains[dom]; !ok {
200-
globalConfig.storage.AddDomain(dom)
226+
var tr string
227+
for i, locale := range globalConfig.locales {
228+
if _, ok := locale.Domains[dom]; !ok {
229+
locale.AddDomain(dom)
230+
}
231+
if !locale.IsTranslatedND(dom, str, n) && i < (len(globalConfig.locales)-1) {
232+
continue
233+
}
234+
tr = locale.GetND(dom, str, plural, n, vars...)
235+
break
201236
}
202-
203-
tr := globalConfig.storage.GetND(dom, str, plural, n, vars...)
204-
globalConfig.RUnlock()
205-
206237
return tr
207238
}
208239

@@ -224,11 +255,17 @@ func GetDC(dom, str, ctx string, vars ...interface{}) string {
224255
// Try to load default package Locale storage
225256
loadStorage(false)
226257

227-
// Return Translation
228258
globalConfig.RLock()
229-
tr := globalConfig.storage.GetDC(dom, str, ctx, vars...)
230-
globalConfig.RUnlock()
259+
defer globalConfig.RUnlock()
231260

261+
var tr string
262+
for i, locale := range globalConfig.locales {
263+
if !locale.IsTranslatedDC(dom, str, ctx) && i < (len(globalConfig.locales)-1) {
264+
continue
265+
}
266+
tr = locale.GetDC(dom, str, ctx, vars...)
267+
break
268+
}
232269
return tr
233270
}
234271

@@ -240,62 +277,101 @@ func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) str
240277

241278
// Return Translation
242279
globalConfig.RLock()
243-
tr := globalConfig.storage.GetNDC(dom, str, plural, n, ctx, vars...)
244-
globalConfig.RUnlock()
280+
defer globalConfig.RUnlock()
245281

282+
var tr string
283+
for i, locale := range globalConfig.locales {
284+
if !locale.IsTranslatedNDC(dom, str, n, ctx) && i < (len(globalConfig.locales)-1) {
285+
continue
286+
}
287+
tr = locale.GetNDC(dom, str, plural, n, ctx, vars...)
288+
break
289+
}
246290
return tr
247291
}
248292

249-
// IsTranslated reports whether a string is translated
250-
func IsTranslated(str string) bool {
251-
return IsTranslatedND(GetDomain(), str, 0)
293+
// IsTranslated reports whether a string is translated in given languages.
294+
// When the langs argument is omitted, the output of GetLanguages is used.
295+
func IsTranslated(str string, langs ...string) bool {
296+
return IsTranslatedND(GetDomain(), str, 0, langs...)
252297
}
253298

254-
// IsTranslatedN reports whether a plural string is translated
255-
func IsTranslatedN(str string, n int) bool {
256-
return IsTranslatedND(GetDomain(), str, n)
299+
// IsTranslatedN reports whether a plural string is translated in given languages.
300+
// When the langs argument is omitted, the output of GetLanguages is used.
301+
func IsTranslatedN(str string, n int, langs ...string) bool {
302+
return IsTranslatedND(GetDomain(), str, n, langs...)
257303
}
258304

259-
// IsTranslatedD reports whether a domain string is translated
260-
func IsTranslatedD(dom, str string) bool {
261-
return IsTranslatedND(dom, str, 0)
305+
// IsTranslatedD reports whether a domain string is translated in given languages.
306+
// When the langs argument is omitted, the output of GetLanguages is used.
307+
func IsTranslatedD(dom, str string, langs ...string) bool {
308+
return IsTranslatedND(dom, str, 0, langs...)
262309
}
263310

264-
// IsTranslatedND reports whether a plural domain string is translated
265-
func IsTranslatedND(dom, str string, n int) bool {
311+
// IsTranslatedND reports whether a plural domain string is translated in any of given languages.
312+
// When the langs argument is omitted, the output of GetLanguages is used.
313+
func IsTranslatedND(dom, str string, n int, langs ...string) bool {
314+
if len(langs) == 0 {
315+
langs = GetLanguages()
316+
}
317+
266318
loadStorage(false)
267319

268320
globalConfig.RLock()
269321
defer globalConfig.RUnlock()
270322

271-
if _, ok := globalConfig.storage.Domains[dom]; !ok {
272-
globalConfig.storage.AddDomain(dom)
273-
}
323+
for _, lang := range langs {
324+
lang = SimplifiedLocale(lang)
274325

275-
return globalConfig.storage.IsTranslatedND(dom, str, n)
326+
for _, supportedLocale := range globalConfig.locales {
327+
if lang != supportedLocale.GetActualLanguage(dom) {
328+
continue
329+
}
330+
return supportedLocale.IsTranslatedND(dom, str, n)
331+
}
332+
}
333+
return false
276334
}
277335

278-
// IsTranslatedC reports whether a context string is translated
279-
func IsTranslatedC(str, ctx string) bool {
280-
return IsTranslatedNDC(GetDomain(), str, 0, ctx)
336+
// IsTranslatedC reports whether a context string is translated in given languages.
337+
// When the langs argument is omitted, the output of GetLanguages is used.
338+
func IsTranslatedC(str, ctx string, langs ...string) bool {
339+
return IsTranslatedNDC(GetDomain(), str, 0, ctx, langs...)
281340
}
282341

283-
// IsTranslatedNC reports whether a plural context string is translated
284-
func IsTranslatedNC(str string, n int, ctx string) bool {
285-
return IsTranslatedNDC(GetDomain(), str, n, ctx)
342+
// IsTranslatedNC reports whether a plural context string is translated in given languages.
343+
// When the langs argument is omitted, the output of GetLanguages is used.
344+
func IsTranslatedNC(str string, n int, ctx string, langs ...string) bool {
345+
return IsTranslatedNDC(GetDomain(), str, n, ctx, langs...)
286346
}
287347

288-
// IsTranslatedDC reports whether a domain context string is translated
289-
func IsTranslatedDC(dom, str, ctx string) bool {
290-
return IsTranslatedNDC(dom, str, 0, ctx)
348+
// IsTranslatedDC reports whether a domain context string is translated in given languages.
349+
// When the langs argument is omitted, the output of GetLanguages is used.
350+
func IsTranslatedDC(dom, str, ctx string, langs ...string) bool {
351+
return IsTranslatedNDC(dom, str, 0, ctx, langs...)
291352
}
292353

293-
// IsTranslatedNDC reports whether a plural domain context string is translated
294-
func IsTranslatedNDC(dom, str string, n int, ctx string) bool {
354+
// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages.
355+
// When the langs argument is omitted, the output of GetLanguages is used.
356+
func IsTranslatedNDC(dom, str string, n int, ctx string, langs ...string) bool {
357+
if len(langs) == 0 {
358+
langs = GetLanguages()
359+
}
360+
295361
loadStorage(false)
296362

297363
globalConfig.RLock()
298364
defer globalConfig.RUnlock()
299365

300-
return globalConfig.storage.IsTranslatedNDC(dom, str, n, ctx)
366+
for _, lang := range langs {
367+
lang = SimplifiedLocale(lang)
368+
369+
for _, locale := range globalConfig.locales {
370+
if lang != locale.GetActualLanguage(dom) {
371+
continue
372+
}
373+
return locale.IsTranslatedNDC(dom, str, n, ctx)
374+
}
375+
}
376+
return false
301377
}

gotext_test.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,13 +177,29 @@ msgstr "Another text on another domain"
177177
t.Errorf("Expected 'Another text on another domain' but got '%s'", tr)
178178
}
179179

180-
// Test IsTranslation functions
180+
// IsTranslated tests for when the string is translated in English.
181181
if !IsTranslated("My text") {
182-
t.Error("'My text' should be reported as translated.")
182+
t.Error("'My text' should be reported as translated when 'langs' is omitted.")
183183
}
184+
if !IsTranslated("My text", "en_US") {
185+
t.Error("'My text' should be reported as translated when 'langs' is 'en_US'.")
186+
}
187+
if IsTranslated("My text", "cs_CZ") {
188+
t.Error("'My text' should be reported as not translated when 'langs' is 'cs_CZ'.")
189+
}
190+
if !IsTranslated("My text", "en_US", "cs_CZ") {
191+
t.Error("'My text' should be reported as translated when 'langs' is 'en_US, cs_CZ'.")
192+
}
193+
194+
// IsTranslated tests for when the string is not translated in English
184195
if IsTranslated("Another string") {
185196
t.Error("'Another string' should be reported as not translated.")
186197
}
198+
if IsTranslated("String not in .po") {
199+
t.Error("'String not in .po' should be reported as not translated.")
200+
}
201+
202+
// IsTranslated tests for plurals and contexts
187203
plural := "One with var: %s"
188204
if !IsTranslated(plural) {
189205
t.Errorf("'%s' should be reported as translated for singular.", plural)

0 commit comments

Comments
 (0)