diff --git a/.gitignore b/.gitignore index d537248..e8f41ab 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,9 @@ .settings .buildpath +# golang jetbrains shit +.idea + # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a diff --git a/fixtures/de/default.mo b/fixtures/de/default.mo new file mode 100644 index 0000000..c81308e Binary files /dev/null and b/fixtures/de/default.mo differ diff --git a/fixtures/de/default.po b/fixtures/de/default.po new file mode 100644 index 0000000..62e19ac --- /dev/null +++ b/fixtures/de/default.po @@ -0,0 +1,77 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: Josef Fröhle \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de\n" +"X-Generator: Poedit 2.0.6\n" +"X-Poedit-SourceCharset: UTF-8\n" + +# Initial comment +# Headers below +msgid "language" +msgstr "de" + +# Some comment +msgid "My text" +msgstr "Translated text" + +# More comments +msgid "Another string" +msgstr "" + +# Multi-line msgid +msgid "" +"multi\n" +"line\n" +"id" +msgstr "id with multiline content" + +# Multi-line msgid_plural +msgid "" +"multi\n" +"line\n" +"plural\n" +"id" +msgstr "plural id with multiline content" + +# Multi-line string +msgid "Multi-line" +msgstr "" +"Multi \n" +"line" + +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" + +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 "Empty translation" +msgstr "" + +msgid "Empty plural form singular" +msgid_plural "Empty plural form" +msgstr[0] "Singular translated" +msgstr[1] "" + +msgid "More" +msgstr "More translation" diff --git a/fixtures/de_DE/LC_MESSAGES/default.mo b/fixtures/de_DE/LC_MESSAGES/default.mo new file mode 100644 index 0000000..a02a5b8 Binary files /dev/null and b/fixtures/de_DE/LC_MESSAGES/default.mo differ diff --git a/fixtures/de_DE/LC_MESSAGES/default.po b/fixtures/de_DE/LC_MESSAGES/default.po new file mode 100644 index 0000000..b2db769 --- /dev/null +++ b/fixtures/de_DE/LC_MESSAGES/default.po @@ -0,0 +1,77 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: Josef Fröhle \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: de_DE\n" +"X-Generator: Poedit 2.0.6\n" +"X-Poedit-SourceCharset: UTF-8\n" + +# Initial comment +# Headers below +msgid "language" +msgstr "de_DE" + +# Some comment +msgid "My text" +msgstr "Translated text" + +# More comments +msgid "Another string" +msgstr "" + +# Multi-line msgid +msgid "" +"multi\n" +"line\n" +"id" +msgstr "id with multiline content" + +# Multi-line msgid_plural +msgid "" +"multi\n" +"line\n" +"plural\n" +"id" +msgstr "plural id with multiline content" + +# Multi-line string +msgid "Multi-line" +msgstr "" +"Multi \n" +"line" + +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" + +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 "Empty translation" +msgstr "" + +msgid "Empty plural form singular" +msgid_plural "Empty plural form" +msgstr[0] "Singular translated" +msgstr[1] "" + +msgid "More" +msgstr "More translation" diff --git a/fixtures/en_AU/default.po b/fixtures/en_AU/default.po new file mode 100644 index 0000000..28c7730 --- /dev/null +++ b/fixtures/en_AU/default.po @@ -0,0 +1,68 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: Josef Fröhle \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_US\n" +"X-Generator: Poedit 2.0.6\n" +"X-Poedit-SourceCharset: UTF-8\n" + +# Initial comment +# Headers below +msgid "language" +msgstr "en_AU" + +# Some comment +msgid "My text" +msgstr "Translated text" + +# More comments +msgid "Another string" +msgstr "" + +# Multi-line msgid +msgid "multilineid" +msgstr "id with multiline content" + +# Multi-line msgid_plural +msgid "multilinepluralid" +msgstr "plural id with multiline content" + +# Multi-line string +msgid "Multi-line" +msgstr "Multi line" + +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" + +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 "Empty translation" +msgstr "" + +msgid "Empty plural form singular" +msgid_plural "Empty plural form" +msgstr[0] "Singular translated" +msgstr[1] "" + +msgid "More" +msgstr "More translation" diff --git a/fixtures/en_GB/default.mo b/fixtures/en_GB/default.mo new file mode 100644 index 0000000..7318082 Binary files /dev/null and b/fixtures/en_GB/default.mo differ diff --git a/fixtures/en_US/default.mo b/fixtures/en_US/default.mo new file mode 100644 index 0000000..af0926c Binary files /dev/null and b/fixtures/en_US/default.mo differ diff --git a/fixtures/en_US/default.po b/fixtures/en_US/default.po new file mode 100644 index 0000000..a18c966 --- /dev/null +++ b/fixtures/en_US/default.po @@ -0,0 +1,68 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n != 1);\n" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: Josef Fröhle \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: en_US\n" +"X-Generator: Poedit 2.0.6\n" +"X-Poedit-SourceCharset: UTF-8\n" + +# Initial comment +# Headers below +msgid "language" +msgstr "en_US" + +# Some comment +msgid "My text" +msgstr "Translated text" + +# More comments +msgid "Another string" +msgstr "" + +# Multi-line msgid +msgid "multilineid" +msgstr "id with multiline content" + +# Multi-line msgid_plural +msgid "multilinepluralid" +msgstr "plural id with multiline content" + +# Multi-line string +msgid "Multi-line" +msgstr "Multi line" + +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" + +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 "Empty translation" +msgstr "" + +msgid "Empty plural form singular" +msgid_plural "Empty plural form" +msgstr[0] "Singular translated" +msgstr[1] "" + +msgid "More" +msgstr "More translation" diff --git a/fixtures/fr/LC_MESSAGES/default.mo b/fixtures/fr/LC_MESSAGES/default.mo new file mode 100644 index 0000000..0bf1a67 Binary files /dev/null and b/fixtures/fr/LC_MESSAGES/default.mo differ diff --git a/fixtures/fr/LC_MESSAGES/default.po b/fixtures/fr/LC_MESSAGES/default.po new file mode 100644 index 0000000..10458e5 --- /dev/null +++ b/fixtures/fr/LC_MESSAGES/default.po @@ -0,0 +1,77 @@ +msgid "" +msgstr "" +"Plural-Forms: nplurals=2; plural=(n > 1);\n" +"Project-Id-Version: \n" +"POT-Creation-Date: \n" +"PO-Revision-Date: \n" +"Last-Translator: Josef Fröhle \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: 8bit\n" +"Language: fr\n" +"X-Generator: Poedit 2.0.6\n" +"X-Poedit-SourceCharset: UTF-8\n" + +# Initial comment +# Headers below +msgid "language" +msgstr "fr" + +# Some comment +msgid "My text" +msgstr "Translated text" + +# More comments +msgid "Another string" +msgstr "" + +# Multi-line msgid +msgid "" +"multi\n" +"line\n" +"id" +msgstr "id with multiline content" + +# Multi-line msgid_plural +msgid "" +"multi\n" +"line\n" +"plural\n" +"id" +msgstr "plural id with multiline content" + +# Multi-line string +msgid "Multi-line" +msgstr "" +"Multi \n" +"line" + +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" + +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 "Empty translation" +msgstr "" + +msgid "Empty plural form singular" +msgid_plural "Empty plural form" +msgstr[0] "Singular translated" +msgstr[1] "" + +msgid "More" +msgstr "More translation" diff --git a/gotext.go b/gotext.go index c5c636d..0c602b8 100644 --- a/gotext.go +++ b/gotext.go @@ -23,7 +23,6 @@ For quick/simple translations you can use the package level functions directly. package gotext import ( - "fmt" "sync" ) @@ -37,7 +36,7 @@ type config struct { // Language set. language string - // Path to library directory where all locale directories and translation files are. + // Path to library directory where all locale directories and Translation files are. library string // Storage for package level methods @@ -65,7 +64,7 @@ func loadStorage(force bool) { globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language) } - if _, ok := globalConfig.storage.domains[globalConfig.domain]; !ok || force { + if _, ok := globalConfig.storage.Domains[globalConfig.domain]; !ok || force { globalConfig.storage.AddDomain(globalConfig.domain) } @@ -74,18 +73,27 @@ func loadStorage(force bool) { // GetDomain is the domain getter for the package configuration func GetDomain() string { + var dom string globalConfig.RLock() - dom := globalConfig.domain + if globalConfig.storage != nil { + dom = globalConfig.storage.GetDomain() + } + if dom == "" { + dom = globalConfig.domain + } globalConfig.RUnlock() return dom } // SetDomain sets the name for the domain to be used at package level. -// It reloads the corresponding translation file. +// It reloads the corresponding Translation file. func SetDomain(dom string) { globalConfig.Lock() globalConfig.domain = dom + if globalConfig.storage != nil { + globalConfig.storage.SetDomain(dom) + } globalConfig.Unlock() loadStorage(true) @@ -101,10 +109,10 @@ func GetLanguage() string { } // SetLanguage sets the language code to be used at package level. -// It reloads the corresponding translation file. +// It reloads the corresponding Translation file. func SetLanguage(lang string) { globalConfig.Lock() - globalConfig.language = lang + globalConfig.language = SimplifiedLocale(lang) globalConfig.Unlock() loadStorage(true) @@ -120,7 +128,7 @@ func GetLibrary() string { } // SetLibrary sets the root path for the loale directories and files to be used at package level. -// It reloads the corresponding translation file. +// It reloads the corresponding Translation file. func SetLibrary(lib string) { globalConfig.Lock() globalConfig.library = lib @@ -129,47 +137,48 @@ func SetLibrary(lib string) { loadStorage(true) } -// Configure sets all configuration variables to be used at package level and reloads the corresponding translation file. +// Configure sets all configuration variables to be used at package level and reloads the corresponding Translation file. // It receives the library path, language code and domain name. // This function is recommended to be used when changing more than one setting, -// as using each setter will introduce a I/O overhead because the translation file will be loaded after each set. +// as using each setter will introduce a I/O overhead because the Translation file will be loaded after each set. func Configure(lib, lang, dom string) { globalConfig.Lock() globalConfig.library = lib - globalConfig.language = lang + globalConfig.language = SimplifiedLocale(lang) globalConfig.domain = dom + globalConfig.storage.SetDomain(dom) globalConfig.Unlock() loadStorage(true) } -// Get uses the default domain globally set to return the corresponding translation of a given string. +// Get uses the default domain globally set to return the corresponding Translation of a given string. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func Get(str string, vars ...interface{}) string { return GetD(GetDomain(), str, vars...) } -// GetN retrieves the (N)th plural form of translation for the given string in the default domain. +// GetN retrieves the (N)th plural form of Translation for the given string in the default domain. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func GetN(str, plural string, n int, vars ...interface{}) string { return GetND(GetDomain(), 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 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 { return GetND(dom, str, str, 1, vars...) } -// GetND retrieves the (N)th plural form of translation in the given domain for a given string. +// 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) - // Return translation + // Return Translation globalConfig.RLock() tr := globalConfig.storage.GetND(dom, str, plural, n, vars...) globalConfig.RUnlock() @@ -177,43 +186,34 @@ func GetND(dom, str, plural string, n int, vars ...interface{}) string { return tr } -// GetC uses the default domain globally set to return the corresponding translation of the given string in the given context. +// 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(GetDomain(), str, ctx, vars...) } -// GetNC retrieves the (N)th plural form of translation for the given string in the given context in the default domain. +// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the default domain. // 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(GetDomain(), str, plural, n, ctx, vars...) } -// GetDC returns the corresponding translation in the given domain for the given string in the given context. +// 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, 1, ctx, vars...) } -// GetNDC retrieves the (N)th plural form of translation in the given domain for a given string. +// 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) - // Return translation + // Return Translation globalConfig.RLock() tr := globalConfig.storage.GetNDC(dom, str, plural, n, ctx, vars...) globalConfig.RUnlock() return tr } - -// printf applies text formatting only when needed to parse variables. -func printf(str string, vars ...interface{}) string { - if len(vars) > 0 { - return fmt.Sprintf(str, vars...) - } - - return str -} diff --git a/gotext_test.go b/gotext_test.go index 9e173bd..5b7439a 100644 --- a/gotext_test.go +++ b/gotext_test.go @@ -3,6 +3,7 @@ package gotext import ( "os" "path" + "path/filepath" "sync" "testing" ) @@ -65,14 +66,14 @@ 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" +msgstr "Some random Translation" msgctxt "Ctx" msgid "Some random in a context" -msgstr "Some random translation in a context" +msgstr "Some random Translation in a context" msgid "More" -msgstr "More translation" +msgstr "More Translation" msgid "Untranslated" msgid_plural "Several untranslated" @@ -95,13 +96,15 @@ msgstr[1] "" 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()) } + // Move file close to write the file, so we can use it in the next step + f.Close() + // Set package configuration Configure("/tmp", "en_US", "default") @@ -125,8 +128,8 @@ msgstr[1] "" // 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) + if tr != "Some random Translation in a context" { + t.Errorf("Expected 'Some random Translation in a context' but got '%s'", tr) } v = "Variable" @@ -214,6 +217,38 @@ msgstr[1] "" } } +func TestMoAndPoTranslator(t *testing.T) { + + fixPath, _ := filepath.Abs("./fixtures/") + + Configure(fixPath, "en_GB", "default") + + // Check default domain Translation + SetDomain("default") + tr := Get("My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text'. Got '%s'", tr) + } + tr = Get("language") + if tr != "en_GB" { + t.Errorf("Expected 'en_GB'. Got '%s'", tr) + } + + // Change Language (locale) + SetLanguage("en_AU") + + // Check default domain Translation + SetDomain("default") + tr = Get("My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text'. Got '%s'", tr) + } + tr = Get("language") + if tr != "en_AU" { + t.Errorf("Expected 'en_AU'. Got '%s'", tr) + } +} + func TestDomains(t *testing.T) { // Set PO content strDefault := ` @@ -222,13 +257,13 @@ msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Default text" msgid_plural "Default texts" -msgstr[0] "Default translation" +msgstr[0] "Default Translation" msgstr[1] "Default translations" msgctxt "Ctx" msgid "Default context" msgid_plural "Default contexts" -msgstr[0] "Default ctx translation" +msgstr[0] "Default ctx Translation" msgstr[1] "Default ctx translations" ` @@ -238,13 +273,13 @@ msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Custom text" msgid_plural "Custom texts" -msgstr[0] "Custom translation" +msgstr[0] "Custom Translation" msgstr[1] "Custom translations" msgctxt "Ctx" msgid "Custom context" msgid_plural "Custom contexts" -msgstr[0] "Custom ctx translation" +msgstr[0] "Custom ctx Translation" msgstr[1] "Custom ctx translations" ` @@ -278,19 +313,19 @@ msgstr[1] "Custom ctx translations" Configure("/tmp", "en_US", "default") - // Check default domain translation + // Check default domain Translation SetDomain("default") tr := Get("Default text") - if tr != "Default translation" { - t.Errorf("Expected 'Default translation'. Got '%s'", tr) + if tr != "Default Translation" { + t.Errorf("Expected 'Default Translation'. Got '%s'", tr) } tr = GetN("Default text", "Default texts", 23) if tr != "Default translations" { t.Errorf("Expected 'Default translations'. Got '%s'", tr) } tr = GetC("Default context", "Ctx") - if tr != "Default ctx translation" { - t.Errorf("Expected 'Default ctx translation'. Got '%s'", tr) + if tr != "Default ctx Translation" { + t.Errorf("Expected 'Default ctx Translation'. Got '%s'", tr) } tr = GetNC("Default context", "Default contexts", 23, "Ctx") if tr != "Default ctx translations" { @@ -299,16 +334,16 @@ msgstr[1] "Custom ctx translations" SetDomain("custom") tr = Get("Custom text") - if tr != "Custom translation" { - t.Errorf("Expected 'Custom translation'. Got '%s'", tr) + if tr != "Custom Translation" { + t.Errorf("Expected 'Custom Translation'. Got '%s'", tr) } tr = GetN("Custom text", "Custom texts", 23) if tr != "Custom translations" { t.Errorf("Expected 'Custom translations'. Got '%s'", tr) } tr = GetC("Custom context", "Ctx") - if tr != "Custom ctx translation" { - t.Errorf("Expected 'Custom ctx translation'. Got '%s'", tr) + if tr != "Custom ctx Translation" { + t.Errorf("Expected 'Custom ctx Translation'. Got '%s'", tr) } tr = GetNC("Custom context", "Custom contexts", 23, "Ctx") if tr != "Custom ctx translations" { @@ -334,7 +369,7 @@ msgstr[2] "And this is the second plural form: %s" msgctxt "Ctx" msgid "Some random in a context" -msgstr "Some random translation in a context" +msgstr "Some random Translation in a context" ` diff --git a/helper.go b/helper.go new file mode 100644 index 0000000..e8dd12b --- /dev/null +++ b/helper.go @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +package gotext + +import ( + "fmt" + "regexp" + "strings" +) + +var re = regexp.MustCompile(`%\(([a-zA-Z0-9_]+)\)[.0-9]*[xsvTtbcdoqXxUeEfFgGp]`) + +func SimplifiedLocale(lang string) string { + // en_US/en_US.UTF-8/zh_CN/zh_TW/el_GR@euro/... + if idx := strings.Index(lang, ":"); idx != -1 { + lang = lang[:idx] + } + if idx := strings.Index(lang, "@"); idx != -1 { + lang = lang[:idx] + } + if idx := strings.Index(lang, "."); idx != -1 { + lang = lang[:idx] + } + return strings.TrimSpace(lang) +} + +// printf applies text formatting only when needed to parse variables. +func Printf(str string, vars ...interface{}) string { + if len(vars) > 0 { + return fmt.Sprintf(str, vars...) + } + + return str +} + + +// NPrintf support named format +func NPrintf(format string, params map[string]interface{}) { + f, p := parseSprintf(format, params) + fmt.Printf(f, p...) +} + +// Sprintf support named format +// Sprintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"}) +func Sprintf(format string, params map[string]interface{}) string { + f, p := parseSprintf(format, params) + return fmt.Sprintf(f, p...) +} + +func parseSprintf(format string, params map[string]interface{}) (string, []interface{}) { + f, n := reformatSprintf(format) + var p []interface{} + for _, v := range n { + p = append(p, params[v]) + } + return f, p +} + +func reformatSprintf(f string) (string, []string) { + m := re.FindAllStringSubmatch(f, -1) + i := re.FindAllStringSubmatchIndex(f, -1) + + ord := []string{} + for _, v := range m { + ord = append(ord, v[1]) + } + + pair := []int{0} + for _, v := range i { + pair = append(pair, v[2]-1) + pair = append(pair, v[3]+1) + } + pair = append(pair, len(f)) + plen := len(pair) + + out := "" + for n := 0; n < plen; n += 2 { + out += f[pair[n]:pair[n+1]] + } + + return out, ord +} \ No newline at end of file diff --git a/helper_test.go b/helper_test.go new file mode 100644 index 0000000..c65e7cb --- /dev/null +++ b/helper_test.go @@ -0,0 +1,112 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +package gotext + +import ( + "reflect" + "testing" +) + +func TestSimplifiedLocale(t *testing.T) { + tr :=SimplifiedLocale("de_DE@euro") + if tr != "de_DE" { + t.Errorf("Expected 'de_DE' but got '%s'", tr) + } + + tr =SimplifiedLocale("de_DE.UTF-8") + if tr != "de_DE" { + t.Errorf("Expected 'de_DE' but got '%s'", tr) + } + + tr =SimplifiedLocale("de_DE:latin1") + if tr != "de_DE" { + t.Errorf("Expected 'de_DE' but got '%s'", tr) + } +} + +func TestReformattingSingleNamedPattern(t *testing.T) { + pat := "%(name_me)x" + + f, n := reformatSprintf(pat) + + if f != "%x" { + t.Errorf("pattern should be %%x but %v", f) + } + + if !reflect.DeepEqual(n, []string{"name_me"}) { + t.Errorf("named var should be {name_me} but %v", n) + } +} + +func TestReformattingMultipleNamedPattern(t *testing.T) { + pat := "%(name_me)x and %(another_name)v" + + f, n := reformatSprintf(pat) + + if f != "%x and %v" { + t.Errorf("pattern should be %%x and %%v but %v", f) + } + + if !reflect.DeepEqual(n, []string{"name_me", "another_name"}) { + t.Errorf("named var should be {name_me, another_name} but %v", n) + } +} + +func TestReformattingRepeatedNamedPattern(t *testing.T) { + pat := "%(name_me)x and %(another_name)v and %(name_me)v" + + f, n := reformatSprintf(pat) + + if f != "%x and %v and %v" { + t.Errorf("pattern should be %%x and %%v and %%v but %v", f) + } + + if !reflect.DeepEqual(n, []string{"name_me", "another_name", "name_me"}) { + t.Errorf("named var should be {name_me, another_name, name_me} but %v", n) + } +} + +func TestSprintf(t *testing.T) { + pat := "%(brother)s loves %(sister)s. %(sister)s also loves %(brother)s." + params := map[string]interface{}{ + "sister": "Susan", + "brother": "Louis", + } + + s := Sprintf(pat, params) + + if s != "Louis loves Susan. Susan also loves Louis." { + t.Errorf("result should be Louis loves Susan. Susan also love Louis. but %v", s) + } +} + +func TestNPrintf(t *testing.T) { + pat := "%(brother)s loves %(sister)s. %(sister)s also loves %(brother)s.\n" + params := map[string]interface{}{ + "sister": "Susan", + "brother": "Louis", + } + + NPrintf(pat, params) + +} + +func TestSprintfFloatsWithPrecision(t *testing.T) { + pat := "%(float)f / %(floatprecision).1f / %(long)g / %(longprecision).3g" + params := map[string]interface{}{ + "float": 5.034560, + "floatprecision": 5.03456, + "long": 5.03456, + "longprecision": 5.03456, + } + + s := Sprintf(pat, params) + + expectedresult := "5.034560 / 5.0 / 5.03456 / 5.03" + if s != expectedresult { + t.Errorf("result should be (%v) but is (%v)", expectedresult, s) + } +} \ No newline at end of file diff --git a/locale.go b/locale.go index 070d25d..7a40025 100644 --- a/locale.go +++ b/locale.go @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + package gotext import ( @@ -22,13 +27,13 @@ Example: // Create Locale with library path and language code l := gotext.NewLocale("/path/to/i18n/dir", "en_US") - // Load domain '/path/to/i18n/dir/en_US/LC_MESSAGES/default.po' + // Load domain '/path/to/i18n/dir/en_US/LC_MESSAGES/default.{po,mo}' l.AddDomain("default") // Translate text from default domain fmt.Println(l.Get("Translate this")) - // Load different domain ('/path/to/i18n/dir/en_US/LC_MESSAGES/extras.po') + // Load different domain ('/path/to/i18n/dir/en_US/LC_MESSAGES/extras.{po,mo}') l.AddDomain("extras") // Translate text from domain @@ -43,8 +48,11 @@ type Locale struct { // Language for this Locale lang string - // List of available domains for this locale. - domains map[string]*Po + // List of available Domains for this locale. + Domains map[string]Translator + + // First AddDomain is default Domain + defaultDomain string // Sync Mutex sync.RWMutex @@ -55,124 +63,162 @@ type Locale struct { func NewLocale(p, l string) *Locale { return &Locale{ path: p, - lang: l, - domains: make(map[string]*Po), + lang: SimplifiedLocale(l), + Domains: make(map[string]Translator), } } -func (l *Locale) findPO(dom string) string { - filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+".po") +func (l *Locale) findExt(dom, ext string) string { + filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext) if _, err := os.Stat(filename); err == nil { return filename } if len(l.lang) > 2 { - filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+".po") + filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext) if _, err := os.Stat(filename); err == nil { return filename } } - filename = path.Join(l.path, l.lang, dom+".po") + filename = path.Join(l.path, l.lang, dom+"."+ext) if _, err := os.Stat(filename); err == nil { return filename } if len(l.lang) > 2 { - filename = path.Join(l.path, l.lang[:2], dom+".po") + filename = path.Join(l.path, l.lang[:2], dom+"."+ext) + if _, err := os.Stat(filename); err == nil { + return filename + } } - return filename + return "" } // AddDomain creates a new domain for a given locale object and initializes the Po object. // If the domain exists, it gets reloaded. func (l *Locale) AddDomain(dom string) { - po := new(Po) + var poObj Translator - // Parse file. - po.ParseFile(l.findPO(dom)) + file := l.findExt(dom, "po") + if file != "" { + poObj = new(Po) + // Parse file. + poObj.ParseFile(file) + } else { + file = l.findExt(dom, "mo") + if file != "" { + poObj = new(Mo) + // Parse file. + poObj.ParseFile(file) + } else { + // fallback return if no file found with + return + } + } // Save new domain l.Lock() - defer l.Unlock() - if l.domains == nil { - l.domains = make(map[string]*Po) + if l.Domains == nil { + l.Domains = make(map[string]Translator) } - l.domains[dom] = po + if l.defaultDomain == "" { + l.defaultDomain = dom + } + l.Domains[dom] = poObj + + // Unlock "Save new domain" + l.Unlock() } -// Get uses a domain "default" to return the corresponding translation of a given string. +// GetDomain is the domain getter for the package configuration +func (l *Locale) GetDomain() string { + l.RLock() + dom := l.defaultDomain + l.RUnlock() + return dom +} + +// SetDomain sets the name for the domain to be used at package level. +// It reloads the corresponding Translation file. +func (l *Locale) SetDomain(dom string) { + l.Lock() + l.defaultDomain = dom + l.Unlock() +} + +// Get uses a domain "default" to return the corresponding Translation of a given string. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func (l *Locale) Get(str string, vars ...interface{}) string { - return l.GetD(GetDomain(), str, vars...) + return l.GetD(l.defaultDomain, str, vars...) } -// GetN retrieves the (N)th plural form of translation for the given string in the "default" domain. +// GetN retrieves the (N)th plural form of Translation for the given string in the "default" domain. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string { - return l.GetND(GetDomain(), str, plural, n, vars...) + return l.GetND(l.defaultDomain, str, plural, n, vars...) } -// GetD returns the corresponding translation in the given domain for the 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, 1, vars...) } -// GetND retrieves the (N)th plural form of translation in the given domain for the given string. +// GetND retrieves the (N)th plural form of 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) GetND(dom, str, plural string, n int, 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].GetN(str, plural, n, vars...) + if l.Domains != nil { + if _, ok := l.Domains[dom]; ok { + if l.Domains[dom] != nil { + return l.Domains[dom].GetN(str, plural, n, vars...) } } } // Return the same we received by default - return printf(plural, vars...) + return Printf(plural, vars...) } -// GetC uses a domain "default" to return the corresponding translation of the given string in the given context. +// 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(GetDomain(), str, ctx, vars...) + return l.GetDC(l.defaultDomain, str, ctx, vars...) } -// GetNC retrieves the (N)th plural form of translation for the given string in the given context in the "default" domain. +// GetNC retrieves the (N)th plural form of Translation for the given string in the given context in the "default" domain. // 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(GetDomain(), str, plural, n, ctx, vars...) + return l.GetNDC(l.defaultDomain, str, plural, n, ctx, vars...) } -// GetDC returns the corresponding translation in the given domain for the given string in the given context. +// 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, 1, ctx, vars...) } -// GetNDC retrieves the (N)th plural form of translation in the given domain for the given string in the given context. +// GetNDC retrieves the (N)th plural form of 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) 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...) + 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 printf(plural, vars...) + return Printf(plural, vars...) } diff --git a/locale_test.go b/locale_test.go index faad2d9..2899148 100644 --- a/locale_test.go +++ b/locale_test.go @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + package gotext import ( @@ -45,14 +50,14 @@ 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" +msgstr "Some random Translation" msgctxt "Ctx" msgid "Some random in a context" -msgstr "Some random translation in a context" +msgstr "Some random Translation in a context" msgid "More" -msgstr "More translation" +msgstr "More Translation" ` @@ -81,14 +86,11 @@ msgstr "More translation" l := NewLocale("/tmp", "en_US") // Force nil domain storage - l.domains = nil + l.Domains = nil // Add domain l.AddDomain("my_domain") - // Set global domain - SetDomain("my_domain") - // Test translations tr := l.GetD("my_domain", "My text") if tr != "Translated text" { @@ -109,8 +111,8 @@ msgstr "More translation" // Test context translations tr = l.GetC("Some random in a context", "Ctx") - if tr != "Some random translation in a context" { - t.Errorf("Expected 'Some random translation in a context'. Got '%s'", tr) + if tr != "Some random Translation in a context" { + t.Errorf("Expected 'Some random Translation in a context'. Got '%s'", tr) } v = "Test" @@ -130,10 +132,10 @@ msgstr "More translation" t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr) } - // Test last translation + // Test last Translation tr = l.GetD("my_domain", "More") - if tr != "More translation" { - t.Errorf("Expected 'More translation' but got '%s'", tr) + if tr != "More Translation" { + t.Errorf("Expected 'More Translation' but got '%s'", tr) } } @@ -178,14 +180,14 @@ 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" +msgstr "Some random Translation" msgctxt "Ctx" msgid "Some random in a context" -msgstr "Some random translation in a context" +msgstr "Some random Translation in a context" msgid "More" -msgstr "More translation" +msgstr "More Translation" ` @@ -214,16 +216,28 @@ msgstr "More translation" l := NewLocale("/tmp", "en_US") // Force nil domain storage - l.domains = nil + l.Domains = nil // Add domain l.AddDomain("my_domain") - // Set default domain to make it fail - SetDomain("default") + // Test non-existent "default" domain responses + tr := l.GetDomain() + if tr != "my_domain" { + t.Errorf("Expected 'my_domain' but got '%s'", tr) + } - // Test non-existent "deafult" domain responses - tr := l.Get("My text") + // Set default domain to make it fail + l.SetDomain("default") + + // Test non-existent "default" domain responses + tr = l.GetDomain() + if tr != "default" { + t.Errorf("Expected 'default' but got '%s'", tr) + } + + // Test non-existent "default" domain responses + tr = l.Get("My text") if tr != "My text" { t.Errorf("Expected 'My text' but got '%s'", tr) } @@ -255,6 +269,121 @@ msgstr "More translation" if tr != "This are tests" { t.Errorf("Expected 'Plural index' but got '%s'", tr) } + + + + // Create Locale with full language code + l = NewLocale("/tmp", "golem") + + // Force nil domain storage + l.Domains = nil + + // Add domain + l.SetDomain("my_domain") + + // Test non-existent "default" domain responses + tr = l.GetDomain() + if tr != "my_domain" { + t.Errorf("Expected 'my_domain' 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) + } + + + + // Create Locale with full language code + l = NewLocale("fixtures/", "fr_FR") + + // Force nil domain storage + l.Domains = nil + + // Add domain + l.SetDomain("default") + + // Test non-existent "default" domain responses + tr = l.GetDomain() + if tr != "default" { + t.Errorf("Expected 'my_domain' 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) + } + + // Create Locale with full language code + l = NewLocale("fixtures/", "de_DE") + + // Force nil domain storage + l.Domains = nil + + // Add domain + l.SetDomain("default") + + // Test non-existent "default" domain responses + tr = l.GetDomain() + if tr != "default" { + t.Errorf("Expected 'my_domain' 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) + } + + + // Create Locale with full language code + l = NewLocale("fixtures/", "de_AT") + + // Force nil domain storage + l.Domains = nil + + // Add domain + l.SetDomain("default") + + // Test non-existent "default" domain responses + tr = l.GetDomain() + if tr != "default" { + t.Errorf("Expected 'my_domain' 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) + } + + // Test syntax error parsed translations + tr = l.GetNDC("mega", "This one has invalid syntax translations","plural",2,"ctx") + if tr != "plural" { + t.Errorf("Expected 'plural' 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) + } } func TestLocaleRace(t *testing.T) { diff --git a/mo.go b/mo.go new file mode 100644 index 0000000..ebd6489 --- /dev/null +++ b/mo.go @@ -0,0 +1,421 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +package gotext + +import ( + "bufio" + "bytes" + "encoding/binary" + "io/ioutil" + "net/textproto" + "os" + "strconv" + "strings" + "sync" + + "github.com/leonelquinteros/gotext/plurals" +) + +const ( + MoMagicLittleEndian = 0x950412de + MoMagicBigEndian = 0xde120495 + + EotSeparator = "\x04" // msgctxt and msgid separator + NulSeparator = "\x00" // msgid and msgstr separator +) +/* +Mo parses the content of any MO file and provides all the Translation functions needed. +It's the base object used by all package methods. +And it's safe for concurrent use by multiple goroutines by using the sync package for locking. + +Example: + + import ( + "fmt" + "github.com/leonelquinteros/gotext" + ) + + func main() { + // Create po object + po := gotext.NewMoTranslator() + + // Parse .po file + po.ParseFile("/path/to/po/file/translations.mo") + + // Get Translation + fmt.Println(po.Get("Translate this")) + } + +*/ +type Mo struct { + // Headers storage + Headers textproto.MIMEHeader + + // Language header + Language string + + // Plural-Forms header + PluralForms string + + // Parsed Plural-Forms header values + nplurals int + plural string + pluralforms plurals.Expression + + // Storage + translations map[string]*Translation + contexts map[string]map[string]*Translation + + // Sync Mutex + sync.RWMutex + + // Parsing buffers + trBuffer *Translation + ctxBuffer string +} + +func NewMoTranslator() Translator { + return new(Mo) +} + +// ParseFile tries to read the file by its provided path (f) and parse its content as a .po file. +func (mo *Mo) ParseFile(f string) { + // Check if file exists + info, err := os.Stat(f) + if err != nil { + return + } + + // Check that isn't a directory + if info.IsDir() { + return + } + + // Parse file content + data, err := ioutil.ReadFile(f) + if err != nil { + return + } + + mo.Parse(data) +} + +// Parse loads the translations specified in the provided string (str) +func (mo *Mo) Parse(buf []byte) { + // Lock while parsing + mo.Lock() + + // Init storage + if mo.translations == nil { + mo.translations = make(map[string]*Translation) + mo.contexts = make(map[string]map[string]*Translation) + } + + r := bytes.NewReader(buf) + + var magicNumber uint32 + if err := binary.Read(r, binary.LittleEndian, &magicNumber); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + var bo binary.ByteOrder + switch magicNumber { + case MoMagicLittleEndian: + bo = binary.LittleEndian + case MoMagicBigEndian: + bo = binary.BigEndian + default: + return + // return fmt.Errorf("gettext: %v", "invalid magic number") + } + + var header struct { + MajorVersion uint16 + MinorVersion uint16 + MsgIdCount uint32 + MsgIdOffset uint32 + MsgStrOffset uint32 + HashSize uint32 + HashOffset uint32 + } + if err := binary.Read(r, bo, &header); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + if v := header.MajorVersion; v != 0 && v != 1 { + return + // return fmt.Errorf("gettext: %v", "invalid version number") + } + if v := header.MinorVersion; v != 0 && v != 1 { + return + // return fmt.Errorf("gettext: %v", "invalid version number") + } + + msgIdStart := make([]uint32, header.MsgIdCount) + msgIdLen := make([]uint32, header.MsgIdCount) + if _, err := r.Seek(int64(header.MsgIdOffset), 0); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + for i := 0; i < int(header.MsgIdCount); i++ { + if err := binary.Read(r, bo, &msgIdLen[i]); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + if err := binary.Read(r, bo, &msgIdStart[i]); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + } + + msgStrStart := make([]int32, header.MsgIdCount) + msgStrLen := make([]int32, header.MsgIdCount) + if _, err := r.Seek(int64(header.MsgStrOffset), 0); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + for i := 0; i < int(header.MsgIdCount); i++ { + if err := binary.Read(r, bo, &msgStrLen[i]); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + if err := binary.Read(r, bo, &msgStrStart[i]); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + } + + for i := 0; i < int(header.MsgIdCount); i++ { + if _, err := r.Seek(int64(msgIdStart[i]), 0); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + msgIdData := make([]byte, msgIdLen[i]) + if _, err := r.Read(msgIdData); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + + if _, err := r.Seek(int64(msgStrStart[i]), 0); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + msgStrData := make([]byte, msgStrLen[i]) + if _, err := r.Read(msgStrData); err != nil { + return + // return fmt.Errorf("gettext: %v", err) + } + + if len(msgIdData) == 0 { + mo.addTranslation(msgIdData, msgStrData) + } else { + mo.addTranslation(msgIdData, msgStrData) + } + } + + // Unlock to parse headers + mo.Unlock() + + // Parse headers + mo.parseHeaders() + return + // return nil +} + +func (mo *Mo) addTranslation(msgid, msgstr []byte) { + translation := NewTranslation() + var msgctxt []byte + var msgidPlural []byte + + d := bytes.Split(msgid, []byte(EotSeparator)) + if len(d) == 1 { + msgid = d[0] + } else { + msgid, msgctxt = d[1], d[0] + } + + dd := bytes.Split(msgid, []byte(NulSeparator)) + if len(dd) > 1 { + msgid = dd[0] + dd = dd[1:] + } + + translation.ID = string(msgid) + + msgidPlural = bytes.Join(dd, []byte(NulSeparator)) + if len(msgidPlural) > 0 { + translation.PluralID = string(msgidPlural) + } + + ddd := bytes.Split(msgstr, []byte(NulSeparator)) + if len(ddd) > 0 { + for i, s := range ddd { + translation.Trs[i] = string(s) + } + } + + if len(msgctxt) > 0 { + // With context... + if _, ok := mo.contexts[string(msgctxt)]; !ok { + mo.contexts[string(msgctxt)] = make(map[string]*Translation) + } + mo.contexts[string(msgctxt)][translation.ID] = translation + } else { + mo.translations[translation.ID] = translation + } +} + +// parseHeaders retrieves data from previously parsed headers +func (mo *Mo) parseHeaders() { + // Make sure we end with 2 carriage returns. + raw := mo.Get("") + "\n\n" + + // Read + reader := bufio.NewReader(strings.NewReader(raw)) + tp := textproto.NewReader(reader) + + var err error + + // Sync Headers write. + mo.Lock() + defer mo.Unlock() + + mo.Headers, err = tp.ReadMIMEHeader() + if err != nil { + return + } + + // Get/save needed headers + mo.Language = mo.Headers.Get("Language") + mo.PluralForms = mo.Headers.Get("Plural-Forms") + + // Parse Plural-Forms formula + if mo.PluralForms == "" { + return + } + + // Split plural form header value + pfs := strings.Split(mo.PluralForms, ";") + + // Parse values + for _, i := range pfs { + vs := strings.SplitN(i, "=", 2) + if len(vs) != 2 { + continue + } + + switch strings.TrimSpace(vs[0]) { + case "nplurals": + mo.nplurals, _ = strconv.Atoi(vs[1]) + + case "plural": + mo.plural = vs[1] + + if expr, err := plurals.Compile(mo.plural); err == nil { + mo.pluralforms = expr + } + + } + } +} + +// pluralForm calculates the plural form index corresponding to n. +// Returns 0 on error +func (mo *Mo) pluralForm(n int) int { + mo.RLock() + defer mo.RUnlock() + + // Failure fallback + if mo.pluralforms == nil { + /* Use the Germanic plural rule. */ + if n == 1 { + return 0 + } else { + return 1 + } + } + return mo.pluralforms.Eval(uint32(n)) +} + +// Get retrieves the corresponding Translation for the given string. +// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. +func (mo *Mo) Get(str string, vars ...interface{}) string { + // Sync read + mo.RLock() + defer mo.RUnlock() + + if mo.translations != nil { + if _, ok := mo.translations[str]; ok { + return Printf(mo.translations[str].Get(), vars...) + } + } + + // Return the same we received by default + return Printf(str, vars...) +} + +// GetN retrieves the (N)th plural form of Translation for the given string. +// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. +func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string { + // Sync read + mo.RLock() + defer mo.RUnlock() + + if mo.translations != nil { + if _, ok := mo.translations[str]; ok { + return Printf(mo.translations[str].GetN(mo.pluralForm(n)), vars...) + } + } + + if n == 1 { + return Printf(str, vars...) + } + return Printf(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 (mo *Mo) GetC(str, ctx string, vars ...interface{}) string { + // Sync read + mo.RLock() + defer mo.RUnlock() + + if mo.contexts != nil { + if _, ok := mo.contexts[ctx]; ok { + if mo.contexts[ctx] != nil { + if _, ok := mo.contexts[ctx][str]; ok { + return Printf(mo.contexts[ctx][str].Get(), vars...) + } + } + } + } + + // Return the string we received by default + return Printf(str, vars...) +} + +// GetNC retrieves the (N)th plural form of Translation 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 (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string { + // Sync read + mo.RLock() + defer mo.RUnlock() + + if mo.contexts != nil { + if _, ok := mo.contexts[ctx]; ok { + if mo.contexts[ctx] != nil { + if _, ok := mo.contexts[ctx][str]; ok { + return Printf(mo.contexts[ctx][str].GetN(mo.pluralForm(n)), vars...) + } + } + } + } + + if n == 1 { + return Printf(str, vars...) + } + return Printf(plural, vars...) +} diff --git a/mo_test.go b/mo_test.go new file mode 100644 index 0000000..a3939c3 --- /dev/null +++ b/mo_test.go @@ -0,0 +1,204 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +package gotext + +import ( + "os" + "path" + "testing" +) + +func TestMo_Get(t *testing.T) { + + // Create po object + mo := new(Mo) + + // Try to parse a directory + mo.ParseFile(path.Clean(os.TempDir())) + + // Parse file + mo.ParseFile("fixtures/en_US/default.mo") + + // Test translations + tr := mo.Get("My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text' but got '%s'", tr) + } + // Test translations + tr = mo.Get("language") + if tr != "en_US" { + t.Errorf("Expected 'en_US' but got '%s'", tr) + } +} + +func TestMo(t *testing.T) { + + // Create po object + mo := new(Mo) + + // Try to parse a directory + mo.ParseFile(path.Clean(os.TempDir())) + + // Parse file + mo.ParseFile("fixtures/en_US/default.mo") + + // Test translations + tr := mo.Get("My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text' but got '%s'", tr) + } + + v := "Variable" + tr = mo.Get("One with var: %s", v) + if tr != "This one is the singular: Variable" { + t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr) + } + + // Test multi-line id + tr = mo.Get("multilineid") + if tr != "id with multiline content" { + t.Errorf("Expected 'id with multiline content' but got '%s'", tr) + } + + // Test multi-line plural id + tr = mo.Get("multilinepluralid") + if tr != "plural id with multiline content" { + t.Errorf("Expected 'plural id with multiline content' but got '%s'", tr) + } + + // Test multi-line + tr = mo.Get("Multi-line") + if tr != "Multi line" { + t.Errorf("Expected 'Multi line' but got '%s'", tr) + } + + // Test plural + tr = mo.GetN("One with var: %s", "Several with vars: %s", 2, v) + if tr != "This one is the plural: Variable" { + t.Errorf("Expected 'This one is the plural: Variable' but got '%s'", tr) + } + + // Test not existent translations + tr = mo.Get("This is a test") + if tr != "This is a test" { + t.Errorf("Expected 'This is a test' but got '%s'", tr) + } + + tr = mo.GetN("This is a test", "This are tests", 100) + if tr != "This are tests" { + t.Errorf("Expected 'This are tests' but got '%s'", tr) + } + + // Test context translations + v = "Test" + tr = mo.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 = mo.GetNC("One with var: %s", "Several with vars: %s", 17, "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 default plural vs singular return responses + tr = mo.GetN("Original", "Original plural", 4) + if tr != "Original plural" { + t.Errorf("Expected 'Original plural' but got '%s'", tr) + } + tr = mo.GetN("Original", "Original plural", 1) + if tr != "Original" { + t.Errorf("Expected 'Original' but got '%s'", tr) + } + + // Test empty Translation strings + tr = mo.Get("Empty Translation") + if tr != "Empty Translation" { + t.Errorf("Expected 'Empty Translation' but got '%s'", tr) + } + + tr = mo.Get("Empty plural form singular") + if tr != "Singular translated" { + t.Errorf("Expected 'Singular translated' but got '%s'", tr) + } + + tr = mo.GetN("Empty plural form singular", "Empty plural form", 1) + if tr != "Singular translated" { + t.Errorf("Expected 'Singular translated' but got '%s'", tr) + } + + tr = mo.GetN("Empty plural form singular", "Empty plural form", 2) + if tr != "Empty plural form" { + t.Errorf("Expected 'Empty plural form' but got '%s'", tr) + } + + // Test last Translation + tr = mo.Get("More") + if tr != "More translation" { + t.Errorf("Expected 'More translation' but got '%s'", tr) + } +} + +func TestMoRace(t *testing.T) { + + // Create Po object + mo := new(Mo) + + // Create sync channels + pc := make(chan bool) + rc := make(chan bool) + + // Parse po content in a goroutine + go func(mo *Mo, done chan bool) { + // Parse file + mo.ParseFile("fixtures/en_US/default.mo") + done <- true + }(mo, pc) + + // Read some Translation on a goroutine + go func(mo *Mo, done chan bool) { + mo.Get("My text") + done <- true + }(mo, rc) + + // Read something at top level + mo.Get("My text") + + // Wait for goroutines to finish + <-pc + <-rc +} + +func TestNewMoTranslatorRace(t *testing.T) { + + // Create Po object + mo := NewMoTranslator() + + // Create sync channels + pc := make(chan bool) + rc := make(chan bool) + + // Parse po content in a goroutine + go func(mo Translator, done chan bool) { + // Parse file + mo.ParseFile("fixtures/en_US/default.mo") + done <- true + }(mo, pc) + + // Read some Translation on a goroutine + go func(mo Translator, done chan bool) { + mo.Get("My text") + done <- true + }(mo, rc) + + // Read something at top level + mo.Get("My text") + + // Wait for goroutines to finish + <-pc + <-rc +} diff --git a/po.go b/po.go index 6b453a4..a33143f 100644 --- a/po.go +++ b/po.go @@ -1,3 +1,8 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + package gotext import ( @@ -12,50 +17,8 @@ import ( "github.com/leonelquinteros/gotext/plurals" ) -type translation struct { - id string - pluralID string - trs map[int]string -} - -func newTranslation() *translation { - tr := new(translation) - tr.trs = make(map[int]string) - - return tr -} - -func (t *translation) get() string { - // Look for translation index 0 - if _, ok := t.trs[0]; ok { - if t.trs[0] != "" { - return t.trs[0] - } - } - - // Return untranslated id by default - return t.id -} - -func (t *translation) getN(n int) string { - // Look for translation index - if _, ok := t.trs[n]; ok { - if t.trs[n] != "" { - return t.trs[n] - } - } - - // Return untranslated singular if corresponding - if n == 0 { - return t.id - } - - // Return untranslated plural by default - return t.pluralID -} - /* -Po parses the content of any PO file and provides all the translation functions needed. +Po parses the content of any PO file and provides all the Translation functions needed. It's the base object used by all package methods. And it's safe for concurrent use by multiple goroutines by using the sync package for locking. @@ -68,12 +31,12 @@ Example: func main() { // Create po object - po := new(gotext.Po) + po := gotext.NewPoTranslator() // Parse .po file po.ParseFile("/path/to/po/file/translations.po") - // Get translation + // Get Translation fmt.Println(po.Get("Translate this")) } @@ -94,14 +57,14 @@ type Po struct { pluralforms plurals.Expression // Storage - translations map[string]*translation - contexts map[string]map[string]*translation + translations map[string]*Translation + contexts map[string]map[string]*Translation // Sync Mutex sync.RWMutex // Parsing buffers - trBuffer *translation + trBuffer *Translation ctxBuffer string } @@ -115,6 +78,10 @@ const ( msgStr ) +func NewPoTranslator() Translator { + return new(Po) +} + // ParseFile tries to read the file by its provided path (f) and parse its content as a .po file. func (po *Po) ParseFile(f string) { // Check if file exists @@ -134,25 +101,25 @@ func (po *Po) ParseFile(f string) { return } - po.Parse(string(data)) + po.Parse(data) } // Parse loads the translations specified in the provided string (str) -func (po *Po) Parse(str string) { +func (po *Po) Parse(buf []byte) { // Lock while parsing po.Lock() // Init storage if po.translations == nil { - po.translations = make(map[string]*translation) - po.contexts = make(map[string]map[string]*translation) + po.translations = make(map[string]*Translation) + po.contexts = make(map[string]map[string]*Translation) } // Get lines - lines := strings.Split(str, "\n") + lines := strings.Split(string(buf), "\n") // Init buffer - po.trBuffer = newTranslation() + po.trBuffer = NewTranslation() po.ctxBuffer = "" state := head @@ -186,7 +153,7 @@ func (po *Po) Parse(str string) { continue } - // Save translation + // Save Translation if strings.HasPrefix(l, "msgstr") { po.parseMessage(l) state = msgStr @@ -200,7 +167,7 @@ func (po *Po) Parse(str string) { } } - // Save last translation buffer. + // Save last Translation buffer. po.saveBuffer() // Unlock to parse headers @@ -210,33 +177,33 @@ func (po *Po) Parse(str string) { po.parseHeaders() } -// saveBuffer takes the context and translation buffers +// saveBuffer takes the context and Translation buffers // and saves it on the translations collection func (po *Po) saveBuffer() { // With no context... if po.ctxBuffer == "" { - po.translations[po.trBuffer.id] = po.trBuffer + 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] = make(map[string]*Translation) } - po.contexts[po.ctxBuffer][po.trBuffer.id] = po.trBuffer + po.contexts[po.ctxBuffer][po.trBuffer.ID] = po.trBuffer // Cleanup current context buffer if needed - if po.trBuffer.id != "" { + if po.trBuffer.ID != "" { po.ctxBuffer = "" } } - // Flush translation buffer - po.trBuffer = newTranslation() + // Flush Translation buffer + po.trBuffer = NewTranslation() } // parseContext takes a line starting with "msgctxt", -// saves the current translation buffer and creates a new context. +// saves the current Translation buffer and creates a new context. func (po *Po) parseContext(l string) { - // Save current translation buffer. + // Save current Translation buffer. po.saveBuffer() // Buffer context @@ -244,25 +211,25 @@ func (po *Po) parseContext(l string) { } // parseID takes a line starting with "msgid", -// saves the current translation and creates a new msgid buffer. +// saves the current Translation and creates a new msgid buffer. func (po *Po) parseID(l string) { - // Save current translation buffer. + // Save current Translation buffer. po.saveBuffer() // Set id - po.trBuffer.id, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid"))) + 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"))) + 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 + // Check for indexed Translation forms if strings.HasPrefix(l, "[") { idx := strings.Index(l, "]") if idx == -1 { @@ -277,15 +244,15 @@ func (po *Po) parseMessage(l string) { return } - // Parse translation string - po.trBuffer.trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:])) + // 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) + // Save single Translation form under 0 index + po.trBuffer.Trs[0], _ = strconv.Unquote(l) } // parseString takes a well formatted string without prefix @@ -295,16 +262,16 @@ func (po *Po) parseString(l string, state parseState) { switch state { case msgStr: - // Append to last translation found - po.trBuffer.trs[len(po.trBuffer.trs)-1] += clean + // Append to last Translation found + po.trBuffer.Trs[len(po.trBuffer.Trs)-1] += clean case msgID: // Multiline msgid - Append to current id - po.trBuffer.id += clean + po.trBuffer.ID += clean case msgIDPlural: // Multiline msgid - Append to current id - po.trBuffer.pluralID += clean + po.trBuffer.PluralID += clean case msgCtxt: // Multiline context - Append to current context @@ -405,7 +372,7 @@ func (po *Po) pluralForm(n int) int { return po.pluralforms.Eval(uint32(n)) } -// Get retrieves the corresponding translation for the given string. +// Get retrieves the corresponding Translation for the given string. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func (po *Po) Get(str string, vars ...interface{}) string { // Sync read @@ -414,15 +381,15 @@ func (po *Po) Get(str string, vars ...interface{}) string { if po.translations != nil { if _, ok := po.translations[str]; ok { - return printf(po.translations[str].get(), vars...) + return Printf(po.translations[str].Get(), vars...) } } // Return the same we received by default - return printf(str, vars...) + return Printf(str, vars...) } -// GetN retrieves the (N)th plural form of translation for the given string. +// GetN retrieves the (N)th plural form of Translation for the given string. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string { // Sync read @@ -431,17 +398,17 @@ func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string { if po.translations != nil { if _, ok := po.translations[str]; ok { - return printf(po.translations[str].getN(po.pluralForm(n)), vars...) + return Printf(po.translations[str].GetN(po.pluralForm(n)), vars...) } } if n == 1 { - return printf(str, vars...) + return Printf(str, vars...) } - return printf(plural, vars...) + return Printf(plural, vars...) } -// GetC retrieves the corresponding translation for a given string in the given context. +// 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 @@ -452,17 +419,17 @@ func (po *Po) GetC(str, ctx string, vars ...interface{}) string { if _, ok := po.contexts[ctx]; ok { if po.contexts[ctx] != nil { if _, ok := po.contexts[ctx][str]; ok { - return printf(po.contexts[ctx][str].get(), vars...) + return Printf(po.contexts[ctx][str].Get(), vars...) } } } } // Return the string we received by default - return printf(str, vars...) + return Printf(str, vars...) } -// GetNC retrieves the (N)th plural form of translation for the given string in the given context. +// GetNC retrieves the (N)th plural form of Translation 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 (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string { // Sync read @@ -473,14 +440,14 @@ func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) if _, ok := po.contexts[ctx]; ok { if po.contexts[ctx] != nil { if _, ok := po.contexts[ctx][str]; ok { - return printf(po.contexts[ctx][str].getN(po.pluralForm(n)), vars...) + return Printf(po.contexts[ctx][str].GetN(po.pluralForm(n)), vars...) } } } } if n == 1 { - return printf(str, vars...) + return Printf(str, vars...) } - return printf(plural, vars...) + return Printf(plural, vars...) } diff --git a/po_test.go b/po_test.go index d898522..51b3cdf 100644 --- a/po_test.go +++ b/po_test.go @@ -1,11 +1,40 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + package gotext import ( "os" "path" "testing" + ) +func TestPo_Get(t *testing.T) { + + // Create po object + po := new(Po) + + // Try to parse a directory + po.ParseFile(path.Clean(os.TempDir())) + + // Parse file + po.ParseFile("fixtures/en_US/default.po") + + // Test translations + tr := po.Get("My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text' but got '%s'", tr) + } + // Test translations + tr = po.Get("language") + if tr != "en_US" { + t.Errorf("Expected 'en_US' but got '%s'", tr) + } +} + func TestPo(t *testing.T) { // Set PO content str := ` @@ -28,13 +57,15 @@ msgid "Another string" msgstr "" # Multi-line msgid -msgid "multi" +msgid "" +"multi" "line" "id" msgstr "id with multiline content" # Multi-line msgid_plural -msgid "multi" +msgid "" +"multi" "line" "plural" "id" @@ -42,7 +73,8 @@ msgstr "plural id with multiline content" #Multi-line string msgid "Multi-line" -msgstr "Multi " +msgstr "" +"Multi " "line" msgid "One with var: %s" @@ -58,13 +90,13 @@ 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" +msgstr "Some random Translation" msgctxt "Ctx" msgid "Some random in a context" -msgstr "Some random translation in a context" +msgstr "Some random Translation in a context" -msgid "Empty translation" +msgid "Empty Translation" msgstr "" msgid "Empty plural form singular" @@ -73,7 +105,7 @@ msgstr[0] "Singular translated" msgstr[1] "" msgid "More" -msgstr "More translation" +msgstr "More Translation" ` @@ -170,10 +202,10 @@ msgstr "More translation" t.Errorf("Expected 'Original' but got '%s'", tr) } - // Test empty translation strings - tr = po.Get("Empty translation") - if tr != "Empty translation" { - t.Errorf("Expected 'Empty translation' but got '%s'", tr) + // Test empty Translation strings + tr = po.Get("Empty Translation") + if tr != "Empty Translation" { + t.Errorf("Expected 'Empty Translation' but got '%s'", tr) } tr = po.Get("Empty plural form singular") @@ -191,10 +223,10 @@ msgstr "More translation" t.Errorf("Expected 'Empty plural form' but got '%s'", tr) } - // Test last translation + // Test last Translation tr = po.Get("More") - if tr != "More translation" { - t.Errorf("Expected 'More translation' but got '%s'", tr) + if tr != "More Translation" { + t.Errorf("Expected 'More Translation' but got '%s'", tr) } } @@ -215,7 +247,7 @@ msgstr[2] "TR Plural 2: %s" ` // Create po object po := new(Po) - po.Parse(str) + po.Parse([]byte(str)) v := "Var" tr := po.GetN("Singular: %s", "Plural: %s", 2, v) @@ -229,7 +261,6 @@ msgstr[2] "TR Plural 2: %s" } } - func TestPluralNoHeaderInformation(t *testing.T) { // Set PO content str := ` @@ -246,7 +277,7 @@ msgstr[2] "TR Plural 2: %s" ` // Create po object po := new(Po) - po.Parse(str) + po.Parse([]byte(str)) v := "Var" tr := po.GetN("Singular: %s", "Plural: %s", 2, v) @@ -281,7 +312,7 @@ msgstr "Translated example" po := new(Po) // Parse - po.Parse(str) + po.Parse([]byte(str)) // Check headers expected if po.Language != "en" { @@ -305,9 +336,9 @@ msgstr "Translated example" po := new(Po) // Parse - po.Parse(str) + po.Parse([]byte(str)) - // Check translation expected + // Check Translation expected if po.Get("Example") != "Translated example" { t.Errorf("Expected 'Translated example' but got '%s'", po.Get("Example")) } @@ -333,7 +364,7 @@ msgstr[3] "Plural form 3" po := new(Po) // Parse - po.Parse(str) + po.Parse([]byte(str)) // Check plural form n := po.pluralForm(0) @@ -378,7 +409,7 @@ msgstr[3] "Plural form 3" po := new(Po) // Parse - po.Parse(str) + po.Parse([]byte(str)) // Check plural form n := po.pluralForm(0) @@ -419,7 +450,7 @@ msgstr[3] "Plural form 3" po := new(Po) // Parse - po.Parse(str) + po.Parse([]byte(str)) // Check plural form n := po.pluralForm(0) @@ -469,7 +500,7 @@ msgstr[3] "Plural form 3" po := new(Po) // Parse - po.Parse(str) + po.Parse([]byte(str)) // Check plural form n := po.pluralForm(1) @@ -495,19 +526,18 @@ msgstr[3] "Plural form 3" } func TestTranslationObject(t *testing.T) { - tr := newTranslation() - str := tr.get() + tr := NewTranslation() + str := tr.Get() if str != "" { t.Errorf("Expected '' but got '%s'", str) } // Set id - tr.id = "Text" + tr.ID = "Text" + str = tr.Get() // Get again - str = tr.get() - if str != "Text" { t.Errorf("Expected 'Text' but got '%s'", str) } @@ -540,11 +570,11 @@ msgstr[2] "And this is the second plural form: %s" // Parse po content in a goroutine go func(po *Po, done chan bool) { - po.Parse(str) + po.Parse([]byte(str)) done <- true }(po, pc) - // Read some translation on a goroutine + // Read some Translation on a goroutine go func(po *Po, done chan bool) { po.Get("My text") done <- true @@ -557,3 +587,33 @@ msgstr[2] "And this is the second plural form: %s" <-pc <-rc } + +func TestNewPoTranslatorRace(t *testing.T) { + + // Create Po object + mo := NewPoTranslator() + + // Create sync channels + pc := make(chan bool) + rc := make(chan bool) + + // Parse po content in a goroutine + go func(mo Translator, done chan bool) { + // Parse file + mo.ParseFile("fixtures/en_US/default.po") + done <- true + }(mo, pc) + + // Read some Translation on a goroutine + go func(mo Translator, done chan bool) { + mo.Get("My text") + done <- true + }(mo, rc) + + // Read something at top level + mo.Get("My text") + + // Wait for goroutines to finish + <-pc + <-rc +} diff --git a/translation.go b/translation.go new file mode 100644 index 0000000..4ab2f13 --- /dev/null +++ b/translation.go @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +package gotext + +type Translation struct { + ID string + PluralID string + Trs map[int]string +} + +func NewTranslation() *Translation { + tr := new(Translation) + tr.Trs = make(map[int]string) + + return tr +} + +func (t *Translation) Get() string { + // Look for Translation index 0 + if _, ok := t.Trs[0]; ok { + if t.Trs[0] != "" { + return t.Trs[0] + } + } + + // Return untranslated id by default + return t.ID +} + +func (t *Translation) GetN(n int) string { + // Look for Translation index + if _, ok := t.Trs[n]; ok { + if t.Trs[n] != "" { + return t.Trs[n] + } + } + + // Return untranslated singular if corresponding + if n == 0 { + return t.ID + } + + // Return untranslated plural by default + return t.PluralID +} diff --git a/translator.go b/translator.go new file mode 100644 index 0000000..9b2924b --- /dev/null +++ b/translator.go @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved. + * Licensed under the MIT License. See LICENSE file in the project root for full license information. + */ + +package gotext + +type Translator interface { + ParseFile(f string) + Parse(buf []byte) + Get(str string, vars ...interface{}) string + GetN(str, plural string, n int, vars ...interface{}) string + GetC(str, ctx string, vars ...interface{}) string + GetNC(str, plural string, n int, ctx string, vars ...interface{}) string +}