From ea87d40cc223cefe427a31c3f982f0c87f65fe52 Mon Sep 17 00:00:00 2001 From: Leonel Quinteros Date: Sun, 26 Jun 2016 15:43:54 -0300 Subject: [PATCH] Add Context (msgctxt) support --- README.md | 7 +-- gotext.go | 29 +++++++++++++ gotext_test.go | 51 +++++++++++++++++++--- locale.go | 43 +++++++++++++++++- locale_test.go | 65 ++++++++++++++++++++++++++++ po.go | 115 +++++++++++++++++++++++++++++++++++++++++++++---- po_test.go | 38 ++++++++++++++++ 7 files changed, 330 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 3bea63b..203a5c7 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,12 @@ Version: [0.9.1](https://github.com/leonelquinteros/gotext/releases/tag/v0.9.1) - Implements GNU gettext support in native Go. - Safe for concurrent use across multiple goroutines. - It works with UTF-8 encoding as it's the default for Go language. -- Unit tests available +- Unit tests available. - Language codes are automatically simplified from the form "en_UK" to "en" if the first isn't available. - Ready to use inside Go templates. -- Support for pluralization rules. -- Support for variables inside translation strings using Go's [fmt package syntax](https://golang.org/pkg/fmt/) +- Support for [pluralization rules](https://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html). +- Support for [message context](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html). +- Support for variables inside translation strings using Go's [fmt package syntax](https://golang.org/pkg/fmt/). diff --git a/gotext.go b/gotext.go index 294f817..ac46f09 100644 --- a/gotext.go +++ b/gotext.go @@ -124,3 +124,32 @@ func GetND(dom, str, plural string, n int, vars ...interface{}) string { // Return translation return storage.GetND(dom, str, plural, n, vars...) } + +// 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...) +} + +// GetNC retrieves the (N)th plural form translation for the given string in the given context in the "default" domain. +// If n == 0, usually the singular form of the string is returned as defined in the PO file. +// 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...) +} + +// 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 { + return GetNDC(dom, str, str, 0, ctx, vars...) +} + +// GetNDC retrieves the (N)th plural form 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) + + // Return translation + return storage.GetNDC(dom, str, plural, n, ctx, vars...) +} diff --git a/gotext_test.go b/gotext_test.go index 41f1b43..4f41d17 100644 --- a/gotext_test.go +++ b/gotext_test.go @@ -45,20 +45,41 @@ 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" + ` - // Set default configuration - Configure("/tmp", "en_US", "default") - // Create Locales directory on default location - dirname := path.Clean(library + string(os.PathSeparator) + "en_US") + dirname := path.Clean("/tmp" + string(os.PathSeparator) + "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.Clean(dirname + string(os.PathSeparator) + "default.po") f, err := os.Create(filename) if err != nil { @@ -71,6 +92,9 @@ msgstr[2] "And this is the second plural form: %s" t.Fatalf("Can't write to test file: %s", err.Error()) } + // Set package configuration + Configure("/tmp", "en_US", "default") + // Test translations tr := Get("My text") if tr != "Translated text" { @@ -88,6 +112,23 @@ msgstr[2] "And this is the second plural form: %s" if tr != "And this is the second plural form: Variable" { t.Errorf("Expected 'And this is the second plural form: Variable' but got '%s'", tr) } + + // Test context translations + tr = GetC("Some random in a context", "Ctx") + if tr != "Some random translation in a context" { + t.Errorf("Expected 'Some random translation in a context' but got '%s'", tr) + } + + v = "Variable" + tr = GetC("One with var: %s", "Ctx", v) + if tr != "This one is the singular in a Ctx context: Variable" { + t.Errorf("Expected 'This one is the singular in a Ctx context: Variable' but got '%s'", tr) + } + + tr = GetNC("One with var: %s", "Several with vars: %s", 1, "Ctx", v) + if tr != "This one is the plural in a Ctx context: Variable" { + t.Errorf("Expected 'This one is the plural in a Ctx context: Variable' but got '%s'", tr) + } } func TestPackageRace(t *testing.T) { diff --git a/locale.go b/locale.go index f3f9fb5..bfcf68a 100644 --- a/locale.go +++ b/locale.go @@ -99,13 +99,13 @@ func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string { return l.GetND("default", str, plural, n, vars...) } -// GetD returns the corresponding translation in the given domain for a given string. +// GetD returns the corresponding translation in the given domain for the given string. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func (l *Locale) GetD(dom, str string, vars ...interface{}) string { return l.GetND(dom, str, str, 0, vars...) } -// GetND retrieves the (N)th plural form translation in the given domain for a given string. +// GetND retrieves the (N)th plural form translation in the given domain for the given string. // If n == 0, usually the singular form of the string is returned as defined in the PO file. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string { @@ -124,3 +124,42 @@ func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) stri // Return the same we received by default return fmt.Sprintf(plural, vars...) } + +// GetC uses a domain "default" 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 (l *Locale) GetC(str, ctx string, vars ...interface{}) string { + return l.GetDC("default", str, ctx, vars...) +} + +// GetNC retrieves the (N)th plural form translation for the given string in the given context in the "default" domain. +// If n == 0, usually the singular form of the string is returned as defined in the PO file. +// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. +func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string { + return l.GetNDC("default", str, plural, n, ctx, vars...) +} + +// 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 (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string { + return l.GetNDC(dom, str, str, 0, ctx, vars...) +} + +// GetNDC retrieves the (N)th plural form translation in the given domain for the given string in the given context. +// If n == 0, usually the singular form of the string is returned as defined in the PO file. +// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. +func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string { + // Sync read + l.RLock() + defer l.RUnlock() + + if l.domains != nil { + if _, ok := l.domains[dom]; ok { + if l.domains[dom] != nil { + return l.domains[dom].GetNC(str, plural, n, ctx, vars...) + } + } + } + + // Return the same we received by default + return fmt.Sprintf(plural, vars...) +} diff --git a/locale_test.go b/locale_test.go index 371f84c..c0618d6 100644 --- a/locale_test.go +++ b/locale_test.go @@ -22,6 +22,30 @@ 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 @@ -82,6 +106,47 @@ msgstr[2] "And this is the second plural form: %s" if tr != "Several with vars: Variable" { t.Errorf("Expected 'Several with vars: Variable' but got '%s'", tr) } + + // Test inexistent translations + tr = l.Get("This is a test") + if tr != "This is a test" { + t.Errorf("Expected 'This is a test' but got '%s'", tr) + } + + tr = l.GetN("This is a test", "This are tests", 1) + if tr != "This are tests" { + t.Errorf("Expected 'This are tests' but got '%s'", tr) + } + + // Test syntax error parsed translations + tr = l.Get("This one has invalid syntax translations") + if tr != "This one has invalid syntax translations" { + t.Errorf("Expected 'This one has invalid syntax translations' but got '%s'", tr) + } + + tr = l.GetN("This one has invalid syntax translations", "This are tests", 1) + 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", 1, "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 2f649f9..d22adc7 100644 --- a/po.go +++ b/po.go @@ -66,6 +66,7 @@ Example: type Po struct { // Storage translations map[string]*translation + contexts map[string]map[string]*translation // Sync Mutex sync.RWMutex @@ -95,16 +96,23 @@ func (po *Po) ParseFile(f string) { // Parse loads the translations specified in the provided string (str) func (po *Po) Parse(str string) { + // Init storage if po.translations == nil { po.Lock() po.translations = make(map[string]*translation) + po.contexts = make(map[string]map[string]*translation) po.Unlock() } + // Get lines lines := strings.Split(str, "\n") + // Translation buffer tr := newTranslation() + // Context buffer + ctx := "" + for _, l := range lines { // Trim spaces l = strings.TrimSpace(l) @@ -115,19 +123,59 @@ func (po *Po) Parse(str string) { } // Skip invalid lines - if !strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") && !strings.HasPrefix(l, "msgstr") { + if !strings.HasPrefix(l, "msgctxt") && !strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") && !strings.HasPrefix(l, "msgstr") { + continue + } + + // Buffer context and continue + if strings.HasPrefix(l, "msgctxt") { + // Save current translation buffer. + po.Lock() + // 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 + } + po.Unlock() + + // Flush buffer + tr = newTranslation() + ctx = "" + + // Buffer context + ctx, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt"))) + + // Loop continue } // Buffer msgid and continue if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") { - // Save current translation buffer. - po.Lock() - po.translations[tr.id] = tr - po.Unlock() + // Save current translation buffer if not inside a context. + if ctx == "" { + po.Lock() + po.translations[tr.id] = tr + po.Unlock() - // Flush buffer - tr = newTranslation() + // 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"))) @@ -178,7 +226,15 @@ func (po *Po) Parse(str string) { // Save last translation buffer. if tr.id != "" { po.Lock() - po.translations[tr.id] = tr + 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.Unlock() } } @@ -217,3 +273,46 @@ func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string { // Return the plural string we received by default return fmt.Sprintf(plural, vars...) } + +// GetC retrieves the corresponding translation for a given string in the given context. +// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. +func (po *Po) GetC(str, ctx string, vars ...interface{}) string { + // Sync read + po.RLock() + defer po.RUnlock() + + if po.contexts != nil { + if _, ok := po.contexts[ctx]; ok { + if po.contexts[ctx] != nil { + if _, ok := po.contexts[ctx][str]; ok { + return fmt.Sprintf(po.contexts[ctx][str].get(), vars...) + } + } + } + } + + // Return the string we received by default + return fmt.Sprintf(str, vars...) +} + +// GetNC retrieves the (N)th plural form translation for the given string in the given context. +// If n == 0, usually the singular form of the string is returned as defined in the PO file. +// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. +func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string { + // Sync read + po.RLock() + defer po.RUnlock() + + if po.contexts != nil { + if _, ok := po.contexts[ctx]; ok { + if po.contexts[ctx] != nil { + if _, ok := po.contexts[ctx][str]; ok { + return fmt.Sprintf(po.contexts[ctx][str].getN(n), vars...) + } + } + } + } + + // Return the plural string we received by default + return fmt.Sprintf(plural, vars...) +} diff --git a/po_test.go b/po_test.go index 0fb71d7..bdb665f 100644 --- a/po_test.go +++ b/po_test.go @@ -28,7 +28,25 @@ 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" ` + // Write PO content to file filename := path.Clean(os.TempDir() + string(os.PathSeparator) + "default.po") @@ -91,6 +109,26 @@ msgstr[0] "Badly formatted string' if tr != "Plural index" { t.Errorf("Expected 'Plural index' but got '%s'", tr) } + + // Test context translations + v = "Test" + tr = po.GetC("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 = po.GetNC("One with var: %s", "Several with vars: %s", 1, "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 = po.Get("More") + if tr != "More translation" { + t.Errorf("Expected 'More translation' but got '%s'", tr) + } + } func TestTranslationObject(t *testing.T) {