Initial import

This commit is contained in:
Leonel Quinteros 2016-06-19 19:36:33 -03:00
commit 9107e9b75a
9 changed files with 595 additions and 0 deletions

29
.gitignore vendored Normal file
View file

@ -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

21
LICENSE Normal file
View file

@ -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.

122
README.md Normal file
View file

@ -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.

93
gotext.go Normal file
View file

@ -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...)
}

56
gotext_test.go Normal file
View file

@ -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)
}
}

72
locale.go Normal file
View file

@ -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...)
}

62
locale_test.go Normal file
View file

@ -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)
}
}

88
po.go Normal file
View file

@ -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...)
}

52
po_test.go Normal file
View file

@ -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)
}
}