Skip to content

Commit 1fc8dec

Browse files
Rewrite PO headers parsing and handling. Implement correct GNU gettext headers format. Fix tests. Fixes #10
1 parent 1bb9389 commit 1fc8dec

File tree

4 files changed

+164
-107
lines changed

4 files changed

+164
-107
lines changed

gotext_test.go

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package gotext
33
import (
44
"os"
55
"path"
6+
"sync"
67
"testing"
78
)
89

@@ -32,10 +33,12 @@ func TestGettersSetters(t *testing.T) {
3233
func TestPackageFunctions(t *testing.T) {
3334
// Set PO content
3435
str := `
35-
# msgid ""
36-
# msgstr ""
36+
msgid ""
37+
msgstr "Project-Id-Version: %s\n"
38+
"Report-Msgid-Bugs-To: %s\n"
39+
3740
# Initial comment
38-
# Headers below
41+
# More Headers below
3942
"Language: en\n"
4043
"Content-Type: text/plain; charset=UTF-8\n"
4144
"Content-Transfer-Encoding: 8bit\n"
@@ -55,14 +58,6 @@ msgstr[0] "This one is the singular: %s"
5558
msgstr[1] "This one is the plural: %s"
5659
msgstr[2] "And this is the second plural form: %s"
5760
58-
msgid "This one has invalid syntax translations"
59-
msgid_plural "Plural index"
60-
msgstr[abc] "Wrong index"
61-
msgstr[1 "Forgot to close brackets"
62-
msgstr[0] "Badly formatted string'
63-
64-
msgid "Invalid formatted id[] with no translations
65-
6661
msgctxt "Ctx"
6762
msgid "One with var: %s"
6863
msgid_plural "Several with vars: %s"
@@ -149,8 +144,8 @@ msgstr[1] ""
149144
func TestUntranslated(t *testing.T) {
150145
// Set PO content
151146
str := `
152-
# msgid ""
153-
# msgstr ""
147+
msgid ""
148+
msgstr ""
154149
# Initial comment
155150
# Headers below
156151
"Language: en\n"
@@ -258,22 +253,29 @@ msgstr[2] "And this is the second plural form: %s"
258253
t.Fatalf("Can't write to test file: %s", err.Error())
259254
}
260255

261-
// Init sync channels
262-
c1 := make(chan bool)
263-
c2 := make(chan bool)
256+
var wg sync.WaitGroup
264257

265258
for i := 0; i < 100; i++ {
259+
wg.Add(1)
266260
// Test translations
267-
go func(done chan bool) {
261+
go func() {
262+
defer wg.Done()
263+
268264
Get("My text")
269-
done <- true
270-
}(c1)
265+
GetN("One with var: %s", "Several with vars: %s", 0, "test")
266+
}()
267+
268+
wg.Add(1)
269+
go func() {
270+
defer wg.Done()
271271

272-
go func(done chan bool) {
273272
Get("My text")
274-
done <- true
275-
}(c2)
273+
GetN("One with var: %s", "Several with vars: %s", 1, "test")
274+
}()
276275

277276
Get("My text")
277+
GetN("One with var: %s", "Several with vars: %s", 2, "test")
278278
}
279+
280+
wg.Wait()
279281
}

locale_test.go

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import (
99
func TestLocale(t *testing.T) {
1010
// Set PO content
1111
str := `
12-
# msgid ""
13-
# msgstr ""
12+
msgid ""
13+
msgstr ""
1414
# Initial comment
1515
# Headers below
1616
"Language: en\n"
@@ -38,8 +38,6 @@ msgstr[abc] "Wrong index"
3838
msgstr[1 "Forgot to close brackets"
3939
msgstr[0] "Badly formatted string'
4040
41-
msgid "Invalid formatted id[] with no translations
42-
4341
msgctxt "Ctx"
4442
msgid "One with var: %s"
4543
msgid_plural "Several with vars: %s"

po.go

Lines changed: 73 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ Example:
7979
8080
*/
8181
type Po struct {
82-
// Headers
83-
RawHeaders string
82+
// Headers storage
83+
Headers textproto.MIMEHeader
8484

8585
// Language header
8686
Language string
@@ -140,7 +140,6 @@ func (po *Po) ParseFile(f string) {
140140
func (po *Po) Parse(str string) {
141141
// Lock while parsing
142142
po.Lock()
143-
defer po.Unlock()
144143

145144
// Init storage
146145
if po.translations == nil {
@@ -195,38 +194,42 @@ func (po *Po) Parse(str string) {
195194

196195
// Multi line strings and headers
197196
if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") {
198-
state = po.parseString(l, state)
197+
po.parseString(l, state)
199198
continue
200199
}
201200
}
202201

203202
// Save last translation buffer.
204203
po.saveBuffer()
205204

205+
// Unlock to parse headers
206+
po.Unlock()
207+
206208
// Parse headers
207209
po.parseHeaders()
208210
}
209211

210212
// saveBuffer takes the context and translation buffers
211213
// and saves it on the translations collection
212214
func (po *Po) saveBuffer() {
213-
// If we have something to save...
214-
if po.trBuffer.id != "" {
215-
// With no context...
216-
if po.ctxBuffer == "" {
217-
po.translations[po.trBuffer.id] = po.trBuffer
218-
} else {
219-
// With context...
220-
if _, ok := po.contexts[po.ctxBuffer]; !ok {
221-
po.contexts[po.ctxBuffer] = make(map[string]*translation)
222-
}
223-
po.contexts[po.ctxBuffer][po.trBuffer.id] = po.trBuffer
215+
// With no context...
216+
if po.ctxBuffer == "" {
217+
po.translations[po.trBuffer.id] = po.trBuffer
218+
} else {
219+
// With context...
220+
if _, ok := po.contexts[po.ctxBuffer]; !ok {
221+
po.contexts[po.ctxBuffer] = make(map[string]*translation)
224222
}
223+
po.contexts[po.ctxBuffer][po.trBuffer.id] = po.trBuffer
225224

226-
// Flush buffer
227-
po.trBuffer = newTranslation()
228-
po.ctxBuffer = ""
225+
// Cleanup current context buffer if needed
226+
if po.trBuffer.id != "" {
227+
po.ctxBuffer = ""
228+
}
229229
}
230+
231+
// Flush translation buffer
232+
po.trBuffer = newTranslation()
230233
}
231234

232235
// parseContext takes a line starting with "msgctxt",
@@ -286,70 +289,72 @@ func (po *Po) parseMessage(l string) {
286289

287290
// parseString takes a well formatted string without prefix
288291
// and creates headers or attach multi-line strings when corresponding
289-
func (po *Po) parseString(l string, state parseState) parseState {
292+
func (po *Po) parseString(l string, state parseState) {
293+
clean, _ := strconv.Unquote(l)
294+
290295
switch state {
291296
case msgStr:
292-
// Check for multiline from previously set msgid
293-
if po.trBuffer.id != "" {
294-
// Append to last translation found
295-
uq, _ := strconv.Unquote(l)
296-
po.trBuffer.trs[len(po.trBuffer.trs)-1] += uq
297+
// Append to last translation found
298+
po.trBuffer.trs[len(po.trBuffer.trs)-1] += clean
297299

298-
}
299300
case msgID:
300301
// Multiline msgid - Append to current id
301-
uq, _ := strconv.Unquote(l)
302-
po.trBuffer.id += uq
302+
po.trBuffer.id += clean
303+
303304
case msgIDPlural:
304305
// Multiline msgid - Append to current id
305-
uq, _ := strconv.Unquote(l)
306-
po.trBuffer.pluralID += uq
306+
po.trBuffer.pluralID += clean
307+
307308
case msgCtxt:
308309
// Multiline context - Append to current context
309-
ctxt, _ := strconv.Unquote(l)
310-
po.ctxBuffer += ctxt
311-
default:
312-
// Otherwise is a header
313-
h, _ := strconv.Unquote(strings.TrimSpace(l))
314-
po.RawHeaders += h
315-
return head
316-
}
310+
po.ctxBuffer += clean
317311

318-
return state
312+
}
319313
}
320314

321315
// isValidLine checks for line prefixes to detect valid syntax.
322316
func (po *Po) isValidLine(l string) bool {
323-
// Skip empty lines
324-
if l == "" {
325-
return false
317+
// Check prefix
318+
valid := []string{
319+
"\"",
320+
"msgctxt",
321+
"msgid",
322+
"msgid_plural",
323+
"msgstr",
326324
}
327325

328-
// Check prefix
329-
if !strings.HasPrefix(l, "\"") && !strings.HasPrefix(l, "msgctxt") && !strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") && !strings.HasPrefix(l, "msgstr") {
330-
return false
326+
for _, v := range valid {
327+
if strings.HasPrefix(l, v) {
328+
return true
329+
}
331330
}
332331

333-
return true
332+
return false
334333
}
335334

336335
// parseHeaders retrieves data from previously parsed headers
337336
func (po *Po) parseHeaders() {
338337
// Make sure we end with 2 carriage returns.
339-
po.RawHeaders += "\n\n"
338+
raw := po.Get("") + "\n\n"
340339

341340
// Read
342-
reader := bufio.NewReader(strings.NewReader(po.RawHeaders))
341+
reader := bufio.NewReader(strings.NewReader(raw))
343342
tp := textproto.NewReader(reader)
344343

345-
mimeHeader, err := tp.ReadMIMEHeader()
344+
var err error
345+
346+
// Sync Headers write.
347+
po.Lock()
348+
defer po.Unlock()
349+
350+
po.Headers, err = tp.ReadMIMEHeader()
346351
if err != nil {
347352
return
348353
}
349354

350355
// Get/save needed headers
351-
po.Language = mimeHeader.Get("Language")
352-
po.PluralForms = mimeHeader.Get("Plural-Forms")
356+
po.Language = po.Headers.Get("Language")
357+
po.PluralForms = po.Headers.Get("Plural-Forms")
353358

354359
// Parse Plural-Forms formula
355360
if po.PluralForms == "" {
@@ -422,12 +427,12 @@ func (po *Po) Get(str string, vars ...interface{}) string {
422427

423428
if po.translations != nil {
424429
if _, ok := po.translations[str]; ok {
425-
return fmt.Sprintf(po.translations[str].get(), vars...)
430+
return po.printf(po.translations[str].get(), vars...)
426431
}
427432
}
428433

429434
// Return the same we received by default
430-
return fmt.Sprintf(str, vars...)
435+
return po.printf(str, vars...)
431436
}
432437

433438
// GetN retrieves the (N)th plural form of translation for the given string.
@@ -439,15 +444,14 @@ func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
439444

440445
if po.translations != nil {
441446
if _, ok := po.translations[str]; ok {
442-
return fmt.Sprintf(po.translations[str].getN(po.pluralForm(n)), vars...)
447+
return po.printf(po.translations[str].getN(po.pluralForm(n)), vars...)
443448
}
444449
}
445450

446451
if n == 1 {
447-
return fmt.Sprintf(str, vars...)
452+
return po.printf(str, vars...)
448453
}
449-
450-
return fmt.Sprintf(plural, vars...)
454+
return po.printf(plural, vars...)
451455
}
452456

453457
// GetC retrieves the corresponding translation for a given string in the given context.
@@ -461,14 +465,14 @@ func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
461465
if _, ok := po.contexts[ctx]; ok {
462466
if po.contexts[ctx] != nil {
463467
if _, ok := po.contexts[ctx][str]; ok {
464-
return fmt.Sprintf(po.contexts[ctx][str].get(), vars...)
468+
return po.printf(po.contexts[ctx][str].get(), vars...)
465469
}
466470
}
467471
}
468472
}
469473

470474
// Return the string we received by default
471-
return fmt.Sprintf(str, vars...)
475+
return po.printf(str, vars...)
472476
}
473477

474478
// GetNC retrieves the (N)th plural form of translation for the given string in the given context.
@@ -482,14 +486,23 @@ func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{})
482486
if _, ok := po.contexts[ctx]; ok {
483487
if po.contexts[ctx] != nil {
484488
if _, ok := po.contexts[ctx][str]; ok {
485-
return fmt.Sprintf(po.contexts[ctx][str].getN(po.pluralForm(n)), vars...)
489+
return po.printf(po.contexts[ctx][str].getN(po.pluralForm(n)), vars...)
486490
}
487491
}
488492
}
489493
}
490494

491495
if n == 1 {
496+
return po.printf(str, vars...)
497+
}
498+
return po.printf(plural, vars...)
499+
}
500+
501+
// printf applies text formatting only when needed to parse variables.
502+
func (po *Po) printf(str string, vars ...interface{}) string {
503+
if len(vars) > 0 {
492504
return fmt.Sprintf(str, vars...)
493505
}
494-
return fmt.Sprintf(plural, vars...)
506+
507+
return str
495508
}

0 commit comments

Comments
 (0)