diff --git a/locale_test.go b/locale_test.go index 515853d..062b0bf 100644 --- a/locale_test.go +++ b/locale_test.go @@ -106,12 +106,115 @@ msgstr "More translation" t.Errorf("Expected 'This one is the plural: Variable' but got '%s'", tr) } + // Test context translations + v = "Test" + tr = l.GetDC("my_domain", "One with var: %s", "Ctx", v) + if tr != "This one is the singular in a Ctx context: Test" { + t.Errorf("Expected 'This one is the singular in a Ctx context: Test' but got '%s'", tr) + } + + // Test plural + tr = l.GetNDC("my_domain", "One with var: %s", "Several with vars: %s", 3, "Ctx", v) + if tr != "This one is the plural in a Ctx context: Test" { + t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr) + } + + // Test last translation + tr = l.GetD("my_domain", "More") + if tr != "More translation" { + t.Errorf("Expected 'More translation' but got '%s'", tr) + } +} + +func TestLocaleFails(t *testing.T) { + // Set PO content + str := ` +msgid "" +msgstr "" +# Initial comment +# Headers below +"Language: en\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" + +# Some comment +msgid "My text" +msgstr "Translated text" + +# More comments +msgid "Another string" +msgstr "" + +msgid "One with var: %s" +msgid_plural "Several with vars: %s" +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" + +msgid "This one has invalid syntax translations" +msgid_plural "Plural index" +msgstr[abc] "Wrong index" +msgstr[1 "Forgot to close brackets" +msgstr[0] "Badly formatted string' + +msgid "Invalid formatted id[] with no translations + +msgctxt "Ctx" +msgid "One with var: %s" +msgid_plural "Several with vars: %s" +msgstr[0] "This one is the singular in a Ctx context: %s" +msgstr[1] "This one is the plural in a Ctx context: %s" + +msgid "Some random" +msgstr "Some random translation" + +msgctxt "Ctx" +msgid "Some random in a context" +msgstr "Some random translation in a context" + +msgid "More" +msgstr "More translation" + + ` + + // Create Locales directory with simplified language code + dirname := path.Join("/tmp", "en", "LC_MESSAGES") + err := os.MkdirAll(dirname, os.ModePerm) + if err != nil { + t.Fatalf("Can't create test directory: %s", err.Error()) + } + + // Write PO content to file + filename := path.Join(dirname, "my_domain.po") + + f, err := os.Create(filename) + if err != nil { + t.Fatalf("Can't create test file: %s", err.Error()) + } + defer f.Close() + + _, err = f.WriteString(str) + if err != nil { + t.Fatalf("Can't write to test file: %s", err.Error()) + } + + // Create Locale with full language code + l := NewLocale("/tmp", "en_US") + + // Force nil domain storage + l.domains = nil + + // Add domain + l.AddDomain("my_domain") + // Test non-existent "deafult" domain responses - tr = l.Get("My text") + tr := l.Get("My text") if tr != "My text" { t.Errorf("Expected 'My text' but got '%s'", tr) } + v := "Variable" tr = l.GetN("One with var: %s", "Several with vars: %s", 2, v) if tr != "Several with vars: Variable" { t.Errorf("Expected 'Several with vars: Variable' but got '%s'", tr) @@ -138,25 +241,6 @@ msgstr "More translation" if tr != "This are tests" { t.Errorf("Expected 'Plural index' but got '%s'", tr) } - - // Test context translations - v = "Test" - tr = l.GetDC("my_domain", "One with var: %s", "Ctx", v) - if tr != "This one is the singular in a Ctx context: Test" { - t.Errorf("Expected 'This one is the singular in a Ctx context: Test' but got '%s'", tr) - } - - // Test plural - tr = l.GetNDC("my_domain", "One with var: %s", "Several with vars: %s", 3, "Ctx", v) - if tr != "This one is the plural in a Ctx context: Test" { - t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr) - } - - // Test last translation - tr = l.GetD("my_domain", "More") - if tr != "More translation" { - t.Errorf("Expected 'More translation' but got '%s'", tr) - } } func TestLocaleRace(t *testing.T) { diff --git a/po.go b/po.go index d33a285..57a8184 100644 --- a/po.go +++ b/po.go @@ -86,6 +86,10 @@ type Po struct { // Sync Mutex sync.RWMutex + + // Parsing buffers + trBuffer *translation + ctxBuffer string } // ParseFile tries to read the file by its provided path (f) and parse its content as a .po file. @@ -125,159 +129,176 @@ func (po *Po) Parse(str string) { // Get lines lines := strings.Split(str, "\n") - // Translation buffer - tr := newTranslation() - - // Context buffer - ctx := "" + // Init buffer + po.trBuffer = newTranslation() + po.ctxBuffer = "" for _, l := range lines { // Trim spaces l = strings.TrimSpace(l) - // Skip empty lines - if l == "" { - continue - } - // Skip invalid lines - if !strings.HasPrefix(l, "\"") && !strings.HasPrefix(l, "msgctxt") && !strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") && !strings.HasPrefix(l, "msgstr") { + if !po.isValidLine(l) { continue } // Buffer context and continue if strings.HasPrefix(l, "msgctxt") { - // Save current translation buffer. - // No context - if ctx == "" { - po.translations[tr.id] = tr - } else { - // Save context - if _, ok := po.contexts[ctx]; !ok { - po.contexts[ctx] = make(map[string]*translation) - } - po.contexts[ctx][tr.id] = tr - } - - // Flush buffer - tr = newTranslation() - ctx = "" - - // Buffer context - ctx, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt"))) - - // Loop + po.parseContext(l) continue } // Buffer msgid and continue if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") { - // Save current translation buffer if not inside a context. - if ctx == "" { - po.translations[tr.id] = tr - - // Flush buffer - tr = newTranslation() - ctx = "" - } else if ctx != "" && tr.id != "" { - // Save current translation buffer inside a context - if _, ok := po.contexts[ctx]; !ok { - po.contexts[ctx] = make(map[string]*translation) - } - po.contexts[ctx][tr.id] = tr - - // Flush buffer - tr = newTranslation() - ctx = "" - } - - // Set id - tr.id, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid"))) - - // Loop + po.parseID(l) continue } // Check for plural form if strings.HasPrefix(l, "msgid_plural") { - tr.pluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural"))) - - // Loop + po.parsePluralID(l) continue } // Save translation if strings.HasPrefix(l, "msgstr") { - l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr")) - - // Check for indexed translation forms - if strings.HasPrefix(l, "[") { - idx := strings.Index(l, "]") - if idx == -1 { - // Skip wrong index formatting - continue - } - - // Parse index - i, err := strconv.Atoi(l[1:idx]) - if err != nil { - // Skip wrong index formatting - continue - } - - // Parse translation string - tr.trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:])) - - // Loop - continue - } - - // Save single translation form under 0 index - tr.trs[0], _ = strconv.Unquote(l) - - // Loop + po.parseMessage(l) continue } // Multi line strings and headers if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") { - // Check for multiline from previously set msgid - if tr.id != "" { - // Append to last translation found - uq, _ := strconv.Unquote(l) - tr.trs[len(tr.trs)-1] += uq - - // Loop - continue - } - - // Otherwise is a header - h, err := strconv.Unquote(strings.TrimSpace(l)) - if err != nil { - continue - } - - po.RawHeaders += h + po.parseString(l) continue } } // Save last translation buffer. - if tr.id != "" { - if ctx == "" { - po.translations[tr.id] = tr - } else { - // Save context - if _, ok := po.contexts[ctx]; !ok { - po.contexts[ctx] = make(map[string]*translation) - } - po.contexts[ctx][tr.id] = tr - } - } + po.saveBuffer() // Parse headers + po.parseHeaders() +} + +// saveBuffer takes the context and translation buffers +// and saves it on the translations collection +func (po *Po) saveBuffer() { + // If we have something to save... + if po.trBuffer.id != "" { + // With no context... + if po.ctxBuffer == "" { + po.translations[po.trBuffer.id] = po.trBuffer + } else { + // With context... + if _, ok := po.contexts[po.ctxBuffer]; !ok { + po.contexts[po.ctxBuffer] = make(map[string]*translation) + } + po.contexts[po.ctxBuffer][po.trBuffer.id] = po.trBuffer + } + + // Flush buffer + po.trBuffer = newTranslation() + po.ctxBuffer = "" + } +} + +// parseContext takes a line starting with "msgctxt", +// saves the current translation buffer and creates a new context. +func (po *Po) parseContext(l string) { + // Save current translation buffer. + po.saveBuffer() + + // Buffer context + po.ctxBuffer, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt"))) +} + +// parseID takes a line starting with "msgid", +// saves the current translation and creates a new msgid buffer. +func (po *Po) parseID(l string) { + // Save current translation buffer. + po.saveBuffer() + + // Set id + po.trBuffer.id, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid"))) +} + +// parsePluralID saves the plural id buffer from a line starting with "msgid_plural" +func (po *Po) parsePluralID(l string) { + po.trBuffer.pluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural"))) +} + +// parseMessage takes a line starting with "msgstr" and saves it into the current buffer. +func (po *Po) parseMessage(l string) { + l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr")) + + // Check for indexed translation forms + if strings.HasPrefix(l, "[") { + idx := strings.Index(l, "]") + if idx == -1 { + // Skip wrong index formatting + return + } + + // Parse index + i, err := strconv.Atoi(l[1:idx]) + if err != nil { + // Skip wrong index formatting + return + } + + // Parse translation string + po.trBuffer.trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:])) + + // Loop + return + } + + // Save single translation form under 0 index + po.trBuffer.trs[0], _ = strconv.Unquote(l) +} + +// parseString takes a well formatted string without prefix +// and creates headers or attach multi-line strings when corresponding +func (po *Po) parseString(l string) { + // Check for multiline from previously set msgid + if po.trBuffer.id != "" { + // Append to last translation found + uq, _ := strconv.Unquote(l) + po.trBuffer.trs[len(po.trBuffer.trs)-1] += uq + + return + } + + // Otherwise is a header + h, err := strconv.Unquote(strings.TrimSpace(l)) + if err != nil { + return + } + + po.RawHeaders += h +} + +// isValidLine checks for line prefixes to detect valid syntax. +func (po *Po) isValidLine(l string) bool { + // Skip empty lines + if l == "" { + return false + } + + // Check prefix + if !strings.HasPrefix(l, "\"") && !strings.HasPrefix(l, "msgctxt") && !strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") && !strings.HasPrefix(l, "msgstr") { + return false + } + + return true +} + +// parseHeaders retrieves data from previously parsed headers +func (po *Po) parseHeaders() { + // Make sure we end with 2 carriage returns. po.RawHeaders += "\n\n" + // Read reader := bufio.NewReader(strings.NewReader(po.RawHeaders)) tp := textproto.NewReader(reader) @@ -340,7 +361,7 @@ func (po *Po) pluralForm(n int) int { if plural.Type().Name() == "bool" { if plural.Bool() { return 1 - } + } // Else return 0 } diff --git a/po_test.go b/po_test.go index 97d4a66..9f59a33 100644 --- a/po_test.go +++ b/po_test.go @@ -186,7 +186,7 @@ msgstr "Translated example" } } -func TestPluralForms(t *testing.T) { +func TestPluralFormsSingle(t *testing.T) { // Single form str := ` "Plural-Forms: nplurals=1; plural=0;" @@ -227,10 +227,11 @@ msgstr[3] "Plural form 3" if n != 0 { t.Errorf("Expected 0 for pluralForm(50), got %d", n) } +} - // ------------------------------------------------------------------------ +func TestPluralForms2(t *testing.T) { // 2 forms - str = ` + str := ` "Plural-Forms: nplurals=2; plural=n != 1;" # Some comment @@ -243,13 +244,13 @@ msgstr[3] "Plural form 3" ` // Create po object - po = new(Po) + po := new(Po) // Parse po.Parse(str) // Check plural form - n = po.pluralForm(0) + n := po.pluralForm(0) if n != 1 { t.Errorf("Expected 1 for pluralForm(0), got %d", n) } @@ -265,10 +266,11 @@ msgstr[3] "Plural form 3" if n != 1 { t.Errorf("Expected 1 for pluralForm(3), got %d", n) } +} - // ------------------------------------------------------------------------ +func TestPluralForms3(t *testing.T) { // 3 forms - str = ` + str := ` "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;" # Some comment @@ -281,13 +283,13 @@ msgstr[3] "Plural form 3" ` // Create po object - po = new(Po) + po := new(Po) // Parse po.Parse(str) // Check plural form - n = po.pluralForm(0) + n := po.pluralForm(0) if n != 2 { t.Errorf("Expected 2 for pluralForm(0), got %d", n) } @@ -311,10 +313,11 @@ msgstr[3] "Plural form 3" if n != 1 { t.Errorf("Expected 1 for pluralForm(3), got %d", n) } +} - // ------------------------------------------------------------------------ +func TestPluralFormsSpecial(t *testing.T) { // 3 forms special - str = ` + str := ` "Plural-Forms: nplurals=3;" "plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" @@ -328,13 +331,13 @@ msgstr[3] "Plural form 3" ` // Create po object - po = new(Po) + po := new(Po) // Parse po.Parse(str) // Check plural form - n = po.pluralForm(1) + n := po.pluralForm(1) if n != 0 { t.Errorf("Expected 0 for pluralForm(1), got %d", n) }