commit 9107e9b75a1076f488707035efcf8d46a5838213 Author: Leonel Quinteros Date: Sun Jun 19 19:36:33 2016 -0300 Initial import diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d537248 --- /dev/null +++ b/.gitignore @@ -0,0 +1,29 @@ +# Eclipse shit +.project +.settings +.buildpath + +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a753ef2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Leonel Quinteros + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..010db90 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +[![GoDoc](https://godoc.org/github.com/leonelquinteros/gotext?status.svg)](https://godoc.org/github.com/leonelquinteros/gotext) + +# Gotext + +GNU gettext utilities for Go + + +# Examples + +## Using package for single language/domain settings + +For quick/simple translations on a single file, you can use the package level functions directly. + +```go +import "github.com/leonelquinteros/gotext" + +func main() { + // Configure package + gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name") + + // Translate text from default domain + println(gotext.Get("My text on 'domain-name' domain")) + + // Translate text from a different domain without reconfigure + println(gotext.GetD("domain2", "Another text on a different domain")) +} + +``` + +## Using dynamic variables on translations + +All translation strings support dynamic variables to be inserted without translate. +Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string. + +```go +import "github.com/leonelquinteros/gotext" + +func main() { + // Configure package + gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name") + + // Set variables + name := "John" + + // Translate text with variables + println(gotext.Get("Hi, my name is %s", name)) +} + +``` + + +## Using Locale object + +When having multiple languages/domains/libraries at the same time, you can create Locale objects for each variation +so you can handle each settings on their own. + +```go +import "github.com/leonelquinteros/gotext" + +func main() { + // Create Locale with library path and language code + l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY") + + // Load domain '/path/to/locales/root/dir/es_UY/default.po' + l.AddDomain("default") + + // Translate text from default domain + println(l.Get("Translate this")) + + // Load different domain + l.AddDomain("translations") + + // Translate text from domain + println(l.GetD("translations", "Translate this")) +} +``` + +This is also helpful for using inside templates (from the "text/template" package), where you can pass the Locale object to the template. +If you set the Locale object as "Loc" in the template, then the templace code would look like: + +``` +{{ .Loc.Get "Translate this" }} +``` + + +## Using the Po object to handle .po files and PO-formatted strings + +For when you need to work with PO files and strings, +you can directly use the Po object to parse it and access the translations in there in the same way. + +```go +import "github.com/leonelquinteros/gotext" + +func main() { + // Set PO content + str := ` +msgid "Translate this" +msgstr "Translated text" + +msgid "Another string" +msgstr "" + +msgid "One with var: %s" +msgstr "This one sets the var: %s" +` + + // Create Po object + po := new(Po) + po.Parse(str) + + println(po.Get("Translate this")) +} +``` + + +# Contribute + +- Please, contribute. +- Use the package on your projects. +- Report issues on Github. +- Send pull requests for bugfixes and improvements. +- Send proposals on Github issues. diff --git a/gotext.go b/gotext.go new file mode 100644 index 0000000..284e4f9 --- /dev/null +++ b/gotext.go @@ -0,0 +1,93 @@ +/* +Package gotext implements GNU gettext utilities. +*/ +package gotext + +// Global environment variables +var ( + // Default domain to look at when no domain is specified. Used by package level functions. + domain = "default" + + // Language set. + language = "en_US" + + // Path to library directory where all locale directories and translation files are. + library = "/tmp" + + // Storage for package level methods + storage *Locale +) + +// loadStorage creates a new Locale object at package level based on the Global variables settings. +// It's called automatically when trying to use Get or GetD methods. +func loadStorage(force bool) { + if storage == nil || force { + storage = NewLocale(library, language) + } + + if _, ok := storage.domains[domain]; !ok || force { + storage.AddDomain(domain) + } +} + +// GetDomain is the domain getter for the package configuration +func GetDomain() string { + return domain +} + +// SetDomain sets the name for the domain to be used at package level. +// It reloads the corresponding translation file. +func SetDomain(dom string) { + domain = dom + loadStorage(true) +} + +// GetLanguage is the language getter for the package configuration +func GetLanguage() string { + return language +} + +// SetLanguage sets the language code to be used at package level. +// It reloads the corresponding translation file. +func SetLanguage(lang string) { + language = lang + loadStorage(true) +} + +// GetLibrary is the library getter for the package configuration +func GetLibrary() string { + return library +} + +// SetLibrary sets the root path for the loale directories and files to be used at package level. +// It reloads the corresponding translation file. +func SetLibrary(lib string) { + library = lib + loadStorage(true) +} + +// 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. +func Configure(lib, lang, dom string) { + library = lib + language = lang + domain = dom + + loadStorage(true) +} + +// Get uses the default domain globally set to return the corresponding translation of a given string. +func Get(str string, vars ...interface{}) string { + return GetD(domain, str, vars...) +} + +// GetD returns the corresponding translation in the given domain for a given string. +func GetD(dom, str string, vars ...interface{}) string { + // Try to load default package Locale storage + loadStorage(false) + + // Return translation + return storage.GetD(dom, str, vars...) +} diff --git a/gotext_test.go b/gotext_test.go new file mode 100644 index 0000000..3f77096 --- /dev/null +++ b/gotext_test.go @@ -0,0 +1,56 @@ +package gotext + +import ( + "os" + "path" + "testing" +) + +func TestPackageFunctions(t *testing.T) { + // Set PO content + str := `# Some comment +msgid "My text" +msgstr "Translated text" + +# More comments +msgid "Another string" +msgstr "" + +msgid "One with var: %s" +msgstr "This one sets the var: %s" + + ` + + // Create Locales directory on default location + dirname := path.Clean(library + 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") + + f, err := os.Create(filename) + if err != nil { + t.Fatalf("Can't create test file: %s", err.Error()) + } + defer f.Close() + + _, err = f.WriteString(str) + if err != nil { + t.Fatalf("Can't write to test file: %s", err.Error()) + } + + // Test translations + tr := Get("My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text' but got '%s'", tr) + } + + v := "Variable" + tr = Get("One with var: %s", v) + if tr != "This one sets the var: Variable" { + t.Errorf("Expected 'This one sets the var: Variable' but got '%s'", tr) + } +} diff --git a/locale.go b/locale.go new file mode 100644 index 0000000..4f2b56f --- /dev/null +++ b/locale.go @@ -0,0 +1,72 @@ +package gotext + +import ( + "fmt" + "os" + "path" +) + +type Locale struct { + // Path to locale files. + path string + + // Language for this Locale + lang string + + // List of available domains for this locale. + domains map[string]*Po +} + +// NewLocale creates and initializes a new Locale object for a given language. +// It receives a path for the i18n files directory (p) and a language code to use (l). +func NewLocale(p, l string) *Locale { + return &Locale{ + path: p, + lang: l, + domains: make(map[string]*Po), + } +} + +// 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) + + // Check for file. + filename := path.Clean(l.path + string(os.PathSeparator) + l.lang + string(os.PathSeparator) + dom + ".po") + + // Try to use the generic language dir if the provided isn't available + if _, err := os.Stat(filename); err != nil { + if len(l.lang) > 2 { + filename = path.Clean(l.path + string(os.PathSeparator) + l.lang[:2] + string(os.PathSeparator) + dom + ".po") + } + } + + // Parse file. + po.ParseFile(filename) + + // Save new domain + if l.domains == nil { + l.domains = make(map[string]*Po) + } + l.domains[dom] = po +} + +// Get uses a domain "default" to return the corresponding translation of a given string. +func (l *Locale) Get(str string, vars ...interface{}) string { + return l.GetD("default", str, vars...) +} + +// GetD returns the corresponding translation in the given domain for a given string. +func (l *Locale) GetD(dom, str string, vars ...interface{}) string { + if l.domains != nil { + if _, ok := l.domains[dom]; ok { + if l.domains[dom] != nil { + return l.domains[dom].Get(str, vars...) + } + } + } + + // Return the same we received by default + return fmt.Sprintf(str, vars...) +} diff --git a/locale_test.go b/locale_test.go new file mode 100644 index 0000000..db346f9 --- /dev/null +++ b/locale_test.go @@ -0,0 +1,62 @@ +package gotext + +import ( + "os" + "path" + "testing" +) + +func TestLocale(t *testing.T) { + // Set PO content + str := `# Some comment +msgid "My text" +msgstr "Translated text" + +# More comments +msgid "Another string" +msgstr "" + +msgid "One with var: %s" +msgstr "This one sets the var: %s" + + ` + + // Create Locales directory with simplified language code + dirname := path.Clean("/tmp" + string(os.PathSeparator) + "en") + err := os.MkdirAll(dirname, os.ModePerm) + if err != nil { + t.Fatalf("Can't create test directory: %s", err.Error()) + } + + // Write PO content to file + filename := path.Clean(dirname + string(os.PathSeparator) + "my_domain.po") + + f, err := os.Create(filename) + if err != nil { + t.Fatalf("Can't create test file: %s", err.Error()) + } + defer f.Close() + + _, err = f.WriteString(str) + if err != nil { + t.Fatalf("Can't write to test file: %s", err.Error()) + } + + // Create Locale with full language code + l := NewLocale("/tmp", "en_US") + + // Add domain + l.AddDomain("my_domain") + + // Test translations + tr := l.GetD("my_domain", "My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text' but got '%s'", tr) + } + + v := "Variable" + tr = l.GetD("my_domain", "One with var: %s", v) + if tr != "This one sets the var: Variable" { + t.Errorf("Expected 'This one sets the var: Variable' but got '%s'", tr) + } +} diff --git a/po.go b/po.go new file mode 100644 index 0000000..99304ca --- /dev/null +++ b/po.go @@ -0,0 +1,88 @@ +package gotext + +import ( + "fmt" + "io/ioutil" + "os" + "strconv" + "strings" +) + +type Po struct { + translations map[string]string +} + +// 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 + 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 + } + + po.Parse(string(data)) +} + +// Parse loads the translations specified in the provided string (str) +func (po *Po) Parse(str string) { + if po.translations == nil { + po.translations = make(map[string]string) + } + + lines := strings.Split(str, "\n") + + var msgid, msgstr string + + for _, l := range lines { + // Trim spaces + l = strings.TrimSpace(l) + + // Skip empty lines + if l == "" { + continue + } + + // Skip invalid lines + if !strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgstr") { + continue + } + + // Buffer msgid and continue + if strings.HasPrefix(l, "msgid") { + msgid = strings.TrimSpace(strings.TrimPrefix(l, "msgid")) + msgid, _ = strconv.Unquote(msgid) + + continue + } + + // Save translation for buffered msgid + if strings.HasPrefix(l, "msgstr") { + msgstr = strings.TrimSpace(strings.TrimPrefix(l, "msgstr")) + msgstr, _ = strconv.Unquote(msgstr) + + po.translations[msgid] = msgstr + } + } +} + +func (po *Po) Get(str string, vars ...interface{}) string { + if po.translations != nil { + if _, ok := po.translations[str]; ok { + return fmt.Sprintf(po.translations[str], vars...) + } + } + + // Return the same we received by default + return fmt.Sprintf(str, vars...) +} diff --git a/po_test.go b/po_test.go new file mode 100644 index 0000000..521c842 --- /dev/null +++ b/po_test.go @@ -0,0 +1,52 @@ +package gotext + +import ( + "os" + "path" + "testing" +) + +func TestPo(t *testing.T) { + // Set PO content + str := `# Some comment +msgid "My text" +msgstr "Translated text" + +# More comments +msgid "Another string" +msgstr "" + +msgid "One with var: %s" +msgstr "This one sets the var: %s" + + ` + // Write PO content to file + filename := path.Clean(os.TempDir() + string(os.PathSeparator) + "default.po") + + f, err := os.Create(filename) + if err != nil { + t.Fatalf("Can't create test file: %s", err.Error()) + } + defer f.Close() + + _, err = f.WriteString(str) + if err != nil { + t.Fatalf("Can't write to test file: %s", err.Error()) + } + + // Parse po file + po := new(Po) + po.ParseFile(filename) + + // Test translations + tr := po.Get("My text") + if tr != "Translated text" { + t.Errorf("Expected 'Translated text' but got '%s'", tr) + } + + v := "Variable" + tr = po.Get("One with var: %s", v) + if tr != "This one sets the var: Variable" { + t.Errorf("Expected 'This one sets the var: Variable' but got '%s'", tr) + } +}