20 Commits

Author SHA1 Message Date
Leonel Quinteros
af699a9df0 Add Travis badge 2016-07-15 19:49:54 -03:00
Leonel Quinteros
eaec826ac5 Reduce go versions on travis tests to 1.5 and forward 2016-07-15 19:46:51 -03:00
Leonel Quinteros
653444ba98 Fix race conditions using Anko. 2016-07-15 19:46:17 -03:00
Leonel Quinteros
74daa24696 Plural-Forms formula support. Headers parsing. Multiline strings support. 2016-07-15 19:04:59 -03:00
Leonel Quinteros
af707140e3 Link GNU home from readme 2016-07-12 16:46:55 -03:00
Leonel Quinteros
e1c7cc4c7d Improved readme docs 2016-07-01 12:43:21 -03:00
Leonel Quinteros
4ec3949a9c Improved readme docs 2016-07-01 12:40:59 -03:00
Leonel Quinteros
2981e87657 Set default library location to default path on GNU gettext utilities. 2016-07-01 12:21:59 -03:00
Leonel Quinteros
ad380b8ede Fix Locale.findPO to support language code simplification on LC_MESSAGES dir. 2016-07-01 11:21:13 -03:00
Leonel Quinteros
e62229fc8e Merge pull request #3 from cinghiale/support_LC_MESSAGES
added support for .po files stored in a LC_MESSAGES subdir
2016-07-01 10:59:45 -03:00
David Mugnai
8a0f825cf4 added support for .po files stored in a LC_MESSAGES subdir 2016-07-01 15:41:56 +02:00
Leonel Quinteros
f9c18b1237 Improved docs 2016-06-26 15:52:59 -03:00
Leonel Quinteros
80c24ef4e1 Link v1.0.0 release 2016-06-26 15:46:47 -03:00
Leonel Quinteros
ea87d40cc2 Add Context (msgctxt) support 2016-06-26 15:43:54 -03:00
Leonel Quinteros
21c6bc86cb Improve test coverage to ~100% 2016-06-26 12:15:08 -03:00
Leonel Quinteros
2c5ca9c0e6 Improve test coverage to ~100% 2016-06-26 11:54:28 -03:00
Leonel Quinteros
5f34149d25 Fix typos 2016-06-24 18:32:58 -03:00
Leonel Quinteros
50cf0f07b7 Release v0.9.1 2016-06-24 17:48:06 -03:00
Leonel Quinteros
d6f4cbb2d5 Add race conditions tests. Fix races. 2016-06-24 17:45:12 -03:00
Leonel Quinteros
6e728a3df5 Link to release on Readme 2016-06-24 17:14:07 -03:00
8 changed files with 1143 additions and 141 deletions

7
.travis.yml Normal file
View File

@@ -0,0 +1,7 @@
language: go
script: go test -v -race ./...
go:
- 1.5
- 1.6
- tip

102
README.md
View File

@@ -1,24 +1,37 @@
[![GoDoc](https://godoc.org/github.com/leonelquinteros/gotext?status.svg)](https://godoc.org/github.com/leonelquinteros/gotext)
[![Build Status](https://travis-ci.org/leonelquinteros/gotext.svg?branch=master)](https://travis-ci.org/leonelquinteros/gotext)
# Gotext
GNU gettext utilities for Go.
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
**Version: 0.9.0**
Version: [v1.1.0](https://github.com/leonelquinteros/gotext/releases/tag/v1.1.0)
#Features
# Features
- Implements GNU gettext support in native Go.
- Safe for concurrent use accross multiple goroutines.
- Complete support for [PO files](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) including:
- Support for multiline strings and headers.
- Support for variables inside translation strings using Go's [fmt syntax](https://golang.org/pkg/fmt/).
- Support for [pluralization rules](https://www.gnu.org/software/gettext/manual/html_node/Translating-plural-forms.html).
- Support for [message contexts](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html).
- Thread-safe: This package is safe for concurrent use across multiple goroutines.
- It works with UTF-8 encoding as it's the default for Go language.
- Unit tests available
- Language codes are automatically simplified from the form "en_UK" to "en" if the formed isn't available.
- Unit tests available.
- Language codes are automatically simplified from the form `en_UK` to `en` if the first isn't available.
- Ready to use inside Go templates.
- Support for pluralization rules.
- Support for variables inside translation strings using Go's [fmt package syntax](https://golang.org/pkg/fmt/)
# License
[MIT license](LICENSE)
# Documentation
Refer to the Godoc package documentation at (https://godoc.org/github.com/leonelquinteros/gotext)
# Installation
@@ -31,42 +44,62 @@ go get github.com/leonelquinteros/gotext
- No need for environment variables. Some naming conventions are applied but not needed.
# License
#### Version vendoring
[MIT license](LICENSE)
Stable releases use [semantic versioning](http://semver.org/spec/v2.0.0.html) tagging on this repository.
You can rely on this to use your preferred vendoring tool or to manually retrieve the corresponding release tag from the GitHub repository.
# Documentation
##### Vendoring with [gopkg.in](http://labix.org/gopkg.in)
Refer to the Godoc package documentation at (https://godoc.org/github.com/leonelquinteros/gotext)
[http://gopkg.in/leonelquinteros/gotext.v1](http://gopkg.in/leonelquinteros/gotext.v1)
To get the latest v1 package stable release, execute:
```
go get gopkg.in/leonelquinteros/gotext.v1
```
To import this package, add the following line to your code:
```go
import "gopkg.in/leonelquinteros/gotext.v1"
```
Refer to it as gotext.
# Locales directories structure
The package will asume a directories structure starting with a base path that will be provided to the package configuration
The package will assume a directories structure starting with a base path that will be provided to the package configuration
or to object constructors depending on the use, but either will use the same convention to lookup inside the base path.
Inside the base directory where will be the language directories named using the language and country 2-letter codes (en_US, es_AR, ...).
All package functions can lookup after the simplified version for each language in case the full code isn't present but the more general language code exists.
So if the language set is "en_UK", but there is no directory named after that code and there is a directory named "en",
So if the language set is `en_UK`, but there is no directory named after that code and there is a directory named `en`,
all package functions will be able to resolve this generalization and provide translations for the more general library.
The language codes are assumed to be [ISO 639-1](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) codes (2-letter codes).
That said, most functions will work with any coding standard as long the directory name matches the language code set on the configuration.
That said, most functions will work with any coding standard as long the directory name matches the language code set on the configuration.
A normal library directory structure may look like:
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
A library directory structure can look like:
```
/path/to/locales
/path/to/locales/en_US
/path/to/locales/en_US/default.po
/path/to/locales/en_US/extras.po
/path/to/locales/en_US/LC_MESSAGES
/path/to/locales/en_US/LC_MESSAGES/default.po
/path/to/locales/en_US/LC_MESSAGES/extras.po
/path/to/locales/en_UK
/path/to/locales/en_UK/default.po
/path/to/locales/en_UK/extras.po
/path/to/locales/en_UK/LC_MESSAGES
/path/to/locales/en_UK/LC_MESSAGES/default.po
/path/to/locales/en_UK/LC_MESSAGES/extras.po
/path/to/locales/en_AU
/path/to/locales/en_AU/default.po
/path/to/locales/en_AU/extras.po
/path/to/locales/en_AU/LC_MESSAGES
/path/to/locales/en_AU/LC_MESSAGES/default.po
/path/to/locales/en_AU/LC_MESSAGES/extras.po
/path/to/locales/es
/path/to/locales/es/default.po
/path/to/locales/es/extras.po
@@ -84,9 +117,9 @@ And so on...
# About translation function names
The standard GNU gettext defines helper functions that maps to the gettext() function and it's widely adopted by most implementations.
The standard GNU gettext defines helper functions that maps to the `gettext()` function and it's widely adopted by most implementations.
The basic translation function is usually _() in the form:
The basic translation function is usually `_()` in the form:
```
_("Translate this")
@@ -109,8 +142,8 @@ func _(str string, vars ...interface{}) string {
This is valid and can be used within a package.
In normal conditions the Go compiler will optimize the calls to _() by replacing its content in place of the function call to reduce the function calling overhead.
This is a normal Go compiler behaviour.
In normal conditions the Go compiler will optimize the calls to `_()` by replacing its content in place of the function call to reduce the function calling overhead.
This is a normal Go compiler behavior.
@@ -185,7 +218,7 @@ func main() {
```
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:
If you set the Locale object as "Loc" in the template, then the template code would look like:
```
{{ .Loc.Get "Translate this" }}
@@ -225,6 +258,10 @@ msgstr "This one sets the var: %s"
## Use plural forms of translations
PO format supports defining one or more plural forms for the same translation.
Relying on the PO file headers, a Plural-Forms formula can be set on the translation file
as defined in (https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html)
Plural formulas are parsed and evaluated using [Anko](https://github.com/mattn/anko)
```go
import "github.com/leonelquinteros/gotext"
@@ -232,6 +269,12 @@ import "github.com/leonelquinteros/gotext"
func main() {
// Set PO content
str := `
msgid ""
msgstr ""
# Header below
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
msgid "Translate this"
msgstr "Translated text"
@@ -242,15 +285,14 @@ msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"
msgstr[2] "And this is the second plural form: %s"
`
// Create Po object
po := new(Po)
po.Parse(str)
println(po.GetN("One with var: %s", "Several with vars: %s", 2, v))
// "And this is the second plural form: Variable"
println(po.GetN("One with var: %s", "Several with vars: %s", 54, v))
// "This one is the plural: Variable"
}
```

View File

@@ -1,8 +1,6 @@
/*
Package gotext implements GNU gettext utilities.
Version 0.9.0 (stable)
For quick/simple translations you can use the package level functions directly.
import "github.com/leonelquinteros/gotext"
@@ -30,7 +28,7 @@ var (
language = "en_US"
// Path to library directory where all locale directories and translation files are.
library = "/tmp"
library = "/usr/local/share/locale"
// Storage for package level methods
storage *Locale
@@ -102,8 +100,7 @@ func Get(str string, vars ...interface{}) string {
return GetD(domain, str, vars...)
}
// GetN retrieves the (N)th plural form translation for the given string in the "default" domain.
// If n == 0, usually the singular form of the string is returned as defined in the PO file.
// 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("default", str, plural, n, vars...)
@@ -112,10 +109,10 @@ func GetN(str, plural string, n int, vars ...interface{}) 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, 0, vars...)
return GetND(dom, str, str, 1, vars...)
}
// GetND retrieves the (N)th plural form 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
@@ -124,3 +121,31 @@ func GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Return translation
return storage.GetND(dom, str, plural, n, vars...)
}
// GetC uses the default domain globally set to return the corresponding translation of the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetC(str, ctx string, vars ...interface{}) string {
return GetDC(domain, str, ctx, vars...)
}
// GetNC retrieves the (N)th plural form 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("default", str, plural, n, ctx, vars...)
}
// GetDC returns the corresponding translation in the given domain for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func GetDC(dom, str, ctx string, vars ...interface{}) string {
return GetNDC(dom, str, str, 1, ctx, vars...)
}
// 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 storage.GetNDC(dom, str, plural, n, ctx, vars...)
}

View File

@@ -6,7 +6,142 @@ import (
"testing"
)
func TestGettersSetters(t *testing.T) {
SetDomain("test")
dom := GetDomain()
if dom != "test" {
t.Errorf("Expected GetDomain to return 'test', but got '%s'", dom)
}
SetLibrary("/tmp/test")
lib := GetLibrary()
if lib != "/tmp/test" {
t.Errorf("Expected GetLibrary to return '/tmp/test', but got '%s'", lib)
}
SetLanguage("es")
lang := GetLanguage()
if lang != "es" {
t.Errorf("Expected GetLanguage to return 'es', but got '%s'", lang)
}
}
func TestPackageFunctions(t *testing.T) {
// Set PO content
str := `
msgid ""
msgstr ""
# Initial comment
# Headers below
"Language: en\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
# Some comment
msgid "My text"
msgstr "Translated text"
# More comments
msgid "Another string"
msgstr ""
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"
msgstr[2] "And this is the second plural form: %s"
msgid "This one has invalid syntax translations"
msgid_plural "Plural index"
msgstr[abc] "Wrong index"
msgstr[1 "Forgot to close brackets"
msgstr[0] "Badly formatted string'
msgid "Invalid formatted id[] with no translations
msgctxt "Ctx"
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"
msgid "Some random"
msgstr "Some random translation"
msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgid "More"
msgstr "More translation"
`
// Create Locales directory on default location
dirname := path.Clean("/tmp" + string(os.PathSeparator) + "en_US")
err := os.MkdirAll(dirname, os.ModePerm)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
// Write PO content to default domain file
filename := path.Clean(dirname + string(os.PathSeparator) + "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())
}
// Set package configuration
Configure("/tmp", "en_US", "default")
// 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 is the singular: Variable" {
t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr)
}
// Test plural
tr = 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 context translations
tr = GetC("Some random in a context", "Ctx")
if tr != "Some random translation in a context" {
t.Errorf("Expected 'Some random translation in a context' but got '%s'", tr)
}
v = "Variable"
tr = GetC("One with var: %s", "Ctx", v)
if tr != "This one is the singular in a Ctx context: Variable" {
t.Errorf("Expected 'This one is the singular in a Ctx context: Variable' but got '%s'", tr)
}
tr = GetNC("One with var: %s", "Several with vars: %s", 19, "Ctx", v)
if tr != "This one is the plural in a Ctx context: Variable" {
t.Errorf("Expected 'This one is the plural in a Ctx context: Variable' but got '%s'", tr)
}
}
func TestPackageRace(t *testing.T) {
// Set PO content
str := `# Some comment
msgid "My text"
@@ -45,21 +180,20 @@ msgstr[2] "And this is the second plural form: %s"
t.Fatalf("Can't write to test file: %s", err.Error())
}
// Init sync channels
c1 := make(chan bool)
c2 := make(chan bool)
// Test translations
tr := Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
}
go func(done chan bool) {
Get("My text")
done <- true
}(c1)
v := "Variable"
tr = 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)
}
go func(done chan bool) {
Get("My text")
done <- true
}(c2)
// Test plural
tr = GetN("One with var: %s", "Several with vars: %s", 2, v)
if tr != "And this is the second plural form: Variable" {
t.Errorf("Expected 'And this is the second plural form: Variable' but got '%s'", tr)
}
Get("My text")
}

View File

@@ -4,6 +4,7 @@ import (
"fmt"
"os"
"path"
"sync"
)
/*
@@ -19,13 +20,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/default.po'
// Load domain '/path/to/i18n/dir/en_US/LC_MESSAGES/default.po'
l.AddDomain("default")
// Translate text from default domain
println(l.Get("Translate this"))
// Load different domain ('/path/to/i18n/dir/en_US/extras.po')
// Load different domain ('/path/to/i18n/dir/en_US/LC_MESSAGES/extras.po')
l.AddDomain("extras")
// Translate text from domain
@@ -42,6 +43,9 @@ type Locale struct {
// List of available domains for this locale.
domains map[string]*Po
// Sync Mutex
sync.RWMutex
}
// NewLocale creates and initializes a new Locale object for a given language.
@@ -54,25 +58,43 @@ func NewLocale(p, l string) *Locale {
}
}
func (l *Locale) findPO(dom string) string {
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+".po")
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")
if _, err := os.Stat(filename); err == nil {
return filename
}
}
filename = path.Join(l.path, l.lang, dom+".po")
if _, err := os.Stat(filename); err == nil {
return filename
}
if len(l.lang) > 2 {
filename = path.Join(l.path, l.lang[:2], dom+".po")
}
return filename
}
// 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)
po.ParseFile(l.findPO(dom))
// Save new domain
l.Lock()
defer l.Unlock()
if l.domains == nil {
l.domains = make(map[string]*Po)
}
@@ -85,23 +107,25 @@ func (l *Locale) Get(str string, vars ...interface{}) string {
return l.GetD("default", str, vars...)
}
// GetN retrieves the (N)th plural form translation for the given string in the "default" domain.
// If n == 0, usually the singular form of the string is returned as defined in the PO file.
// 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("default", str, plural, n, vars...)
}
// GetD returns the corresponding translation in the given domain for a given string.
// GetD returns the corresponding translation in the given domain for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
return l.GetND(dom, str, str, 0, vars...)
return l.GetND(dom, str, str, 1, vars...)
}
// GetND retrieves the (N)th plural form translation in the given domain for a given string.
// If n == 0, usually the singular form of the string is returned as defined in the PO file.
// 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 {
@@ -113,3 +137,40 @@ func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) stri
// Return the same we received by default
return fmt.Sprintf(plural, vars...)
}
// GetC uses a domain "default" to return the corresponding translation of the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetC(str, ctx string, vars ...interface{}) string {
return l.GetDC("default", str, ctx, vars...)
}
// GetNC retrieves the (N)th plural form 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("default", str, plural, n, ctx, vars...)
}
// GetDC returns the corresponding translation in the given domain for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
return l.GetNDC(dom, str, str, 1, ctx, vars...)
}
// 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...)
}
}
}
// Return the same we received by default
return fmt.Sprintf(plural, vars...)
}

View File

@@ -7,6 +7,159 @@ import (
)
func TestLocale(t *testing.T) {
// Set PO content
str := `
msgid ""
msgstr ""
# Initial comment
# Headers below
"Language: en\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
# Some comment
msgid "My text"
msgstr "Translated text"
# More comments
msgid "Another string"
msgstr ""
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"
msgstr[2] "And this is the second plural form: %s"
msgid "This one has invalid syntax translations"
msgid_plural "Plural index"
msgstr[abc] "Wrong index"
msgstr[1 "Forgot to close brackets"
msgstr[0] "Badly formatted string'
msgid "Invalid formatted id[] with no translations
msgctxt "Ctx"
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"
msgid "Some random"
msgstr "Some random translation"
msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgid "More"
msgstr "More translation"
`
// Create Locales directory with simplified language code
dirname := path.Join("/tmp", "en", "LC_MESSAGES")
err := os.MkdirAll(dirname, os.ModePerm)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
// Write PO content to file
filename := path.Join(dirname, "my_domain.po")
f, err := os.Create(filename)
if err != nil {
t.Fatalf("Can't create test file: %s", err.Error())
}
defer f.Close()
_, err = f.WriteString(str)
if err != nil {
t.Fatalf("Can't write to test file: %s", err.Error())
}
// Create Locale with full language code
l := NewLocale("/tmp", "en_US")
// Force nil domain storage
l.domains = nil
// Add domain
l.AddDomain("my_domain")
// Test 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 is the singular: Variable" {
t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr)
}
// Test plural
tr = l.GetND("my_domain", "One with var: %s", "Several with vars: %s", 7, v)
if tr != "This one is the plural: Variable" {
t.Errorf("Expected 'This one is the plural: Variable' but got '%s'", tr)
}
// Test non-existent "deafult" domain responses
tr = l.Get("My text")
if tr != "My text" {
t.Errorf("Expected 'My text' but got '%s'", tr)
}
tr = l.GetN("One with var: %s", "Several with vars: %s", 2, v)
if tr != "Several with vars: Variable" {
t.Errorf("Expected 'Several with vars: Variable' but got '%s'", tr)
}
// Test inexistent translations
tr = l.Get("This is a test")
if tr != "This is a test" {
t.Errorf("Expected 'This is a test' but got '%s'", tr)
}
tr = l.GetN("This is a test", "This are tests", 1)
if tr != "This are tests" {
t.Errorf("Expected 'This are tests' but got '%s'", tr)
}
// Test syntax error parsed translations
tr = l.Get("This one has invalid syntax translations")
if tr != "This one has invalid syntax translations" {
t.Errorf("Expected 'This one has invalid syntax translations' but got '%s'", tr)
}
tr = l.GetN("This one has invalid syntax translations", "This are tests", 1)
if tr != "This are tests" {
t.Errorf("Expected 'Plural index' but got '%s'", tr)
}
// Test context translations
v = "Test"
tr = l.GetDC("my_domain", "One with var: %s", "Ctx", v)
if tr != "This one is the singular in a Ctx context: Test" {
t.Errorf("Expected 'This one is the singular in a Ctx context: Test' but got '%s'", tr)
}
// Test plural
tr = l.GetNDC("my_domain", "One with var: %s", "Several with vars: %s", 3, "Ctx", v)
if tr != "This one is the plural in a Ctx context: Test" {
t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr)
}
// Test last translation
tr = l.GetD("my_domain", "More")
if tr != "More translation" {
t.Errorf("Expected 'More translation' but got '%s'", tr)
}
}
func TestLocaleRace(t *testing.T) {
// Set PO content
str := `# Some comment
msgid "My text"
@@ -25,14 +178,14 @@ msgstr[2] "And this is the second plural form: %s"
`
// Create Locales directory with simplified language code
dirname := path.Clean("/tmp" + string(os.PathSeparator) + "en")
dirname := path.Join("/tmp", "es")
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")
filename := path.Join(dirname, "race.po")
f, err := os.Create(filename)
if err != nil {
@@ -46,26 +199,28 @@ msgstr[2] "And this is the second plural form: %s"
}
// Create Locale with full language code
l := NewLocale("/tmp", "en_US")
l := NewLocale("/tmp", "es")
// Add domain
l.AddDomain("my_domain")
// Init sync channels
ac := make(chan bool)
rc := make(chan bool)
// Test translations
tr := l.GetD("my_domain", "My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
}
// Add domain in goroutine
go func(l *Locale, done chan bool) {
l.AddDomain("race")
done <- true
}(l, ac)
v := "Variable"
tr = l.GetD("my_domain", "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)
}
// Get translations in goroutine
go func(l *Locale, done chan bool) {
l.GetD("race", "My text")
done <- true
}(l, rc)
// Test plural
tr = l.GetND("my_domain", "One with var: %s", "Several with vars: %s", 2, v)
if tr != "And this is the second plural form: Variable" {
t.Errorf("Expected 'And this is the second plural form: Variable' but got '%s'", tr)
}
// Get translations at top level
l.GetD("race", "My text")
// Wait for goroutines to finish
<-ac
<-rc
}

260
po.go
View File

@@ -1,8 +1,11 @@
package gotext
import (
"bufio"
"fmt"
"github.com/leonelquinteros/anko/vm"
"io/ioutil"
"net/textproto"
"os"
"strconv"
"strings"
@@ -44,8 +47,8 @@ func (t *translation) getN(n int) string {
/*
Po parses the content of any PO file and provides all the translation functions needed.
It's the base object used by all packafe methods.
And it's safe for concurrent use by multiple goroutines by using the sync package for write locking.
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:
@@ -64,8 +67,22 @@ Example:
*/
type Po struct {
// Headers
RawHeaders string
// Language header
Language string
// Plural-Forms header
PluralForms string
// Parsed Plural-Forms header values
nplurals int
plural string
// Storage
translations map[string]*translation
contexts map[string]map[string]*translation
// Sync Mutex
sync.RWMutex
@@ -95,16 +112,25 @@ func (po *Po) ParseFile(f string) {
// Parse loads the translations specified in the provided string (str)
func (po *Po) Parse(str string) {
// Lock while parsing
po.Lock()
defer po.Unlock()
// Init storage
if po.translations == nil {
po.Lock()
po.translations = make(map[string]*translation)
po.Unlock()
po.contexts = make(map[string]map[string]*translation)
}
// Get lines
lines := strings.Split(str, "\n")
// Translation buffer
tr := newTranslation()
// Context buffer
ctx := ""
for _, l := range lines {
// Trim spaces
l = strings.TrimSpace(l)
@@ -115,19 +141,55 @@ func (po *Po) Parse(str string) {
}
// Skip invalid lines
if !strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") && !strings.HasPrefix(l, "msgstr") {
if !strings.HasPrefix(l, "\"") && !strings.HasPrefix(l, "msgctxt") && !strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") && !strings.HasPrefix(l, "msgstr") {
continue
}
// Buffer context and continue
if strings.HasPrefix(l, "msgctxt") {
// Save current translation buffer.
// No context
if ctx == "" {
po.translations[tr.id] = tr
} else {
// Save context
if _, ok := po.contexts[ctx]; !ok {
po.contexts[ctx] = make(map[string]*translation)
}
po.contexts[ctx][tr.id] = tr
}
// Flush buffer
tr = newTranslation()
ctx = ""
// Buffer context
ctx, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt")))
// Loop
continue
}
// Buffer msgid and continue
if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") {
// Save current translation buffer.
po.Lock()
po.translations[tr.id] = tr
po.Unlock()
// Save current translation buffer if not inside a context.
if ctx == "" {
po.translations[tr.id] = tr
// Flush buffer
tr = newTranslation()
// Flush buffer
tr = newTranslation()
ctx = ""
} else if ctx != "" && tr.id != "" {
// Save current translation buffer inside a context
if _, ok := po.contexts[ctx]; !ok {
po.contexts[ctx] = make(map[string]*translation)
}
po.contexts[ctx][tr.id] = tr
// Flush buffer
tr = newTranslation()
ctx = ""
}
// Set id
tr.id, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid")))
@@ -150,21 +212,21 @@ func (po *Po) Parse(str string) {
// Check for indexed translation forms
if strings.HasPrefix(l, "[") {
in := strings.Index(l, "]")
if in == -1 {
idx := strings.Index(l, "]")
if idx == -1 {
// Skip wrong index formatting
continue
}
// Parse index
i, err := strconv.Atoi(l[1:in])
i, err := strconv.Atoi(l[1:idx])
if err != nil {
// Skip wrong index formatting
continue
}
// Parse translation string
tr.trs[i], _ = strconv.Unquote(strings.TrimSpace(l[in+1:]))
tr.trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:]))
// Loop
continue
@@ -172,20 +234,131 @@ func (po *Po) Parse(str string) {
// Save single translation form under 0 index
tr.trs[0], _ = strconv.Unquote(l)
// Loop
continue
}
// Multi line strings and headers
if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") {
// Check for multiline from previously set msgid
if tr.id != "" {
// Append to last translation found
uq, _ := strconv.Unquote(l)
tr.trs[len(tr.trs)-1] += uq
// Loop
continue
}
// Otherwise is a header
h, err := strconv.Unquote(strings.TrimSpace(l))
if err != nil {
continue
}
po.RawHeaders += h
continue
}
}
// Save last translation buffer.
if tr.id != "" {
po.Lock()
po.translations[tr.id] = tr
po.Unlock()
if ctx == "" {
po.translations[tr.id] = tr
} else {
// Save context
if _, ok := po.contexts[ctx]; !ok {
po.contexts[ctx] = make(map[string]*translation)
}
po.contexts[ctx][tr.id] = tr
}
}
// Parse headers
po.RawHeaders += "\n\n"
reader := bufio.NewReader(strings.NewReader(po.RawHeaders))
tp := textproto.NewReader(reader)
mimeHeader, err := tp.ReadMIMEHeader()
if err != nil {
return
}
// Get/save needed headers
po.Language = mimeHeader.Get("Language")
po.PluralForms = mimeHeader.Get("Plural-Forms")
// Parse Plural-Forms formula
if po.PluralForms == "" {
return
}
// Split plural form header value
pfs := strings.Split(po.PluralForms, ";")
// Parse values
for _, i := range pfs {
vs := strings.SplitN(i, "=", 2)
if len(vs) != 2 {
continue
}
switch strings.TrimSpace(vs[0]) {
case "nplurals":
po.nplurals, _ = strconv.Atoi(vs[1])
case "plural":
po.plural = vs[1]
}
}
}
// pluralForm calculates the plural form index corresponding to n.
// Returns 0 on error
func (po *Po) pluralForm(n int) int {
po.RLock()
defer po.RUnlock()
// Failsafe
if po.nplurals < 1 {
return 0
}
if po.plural == "" {
return 0
}
// Init compiler
env := vm.NewEnv()
env.Define("n", n)
plural, err := env.Execute(po.plural)
if err != nil {
return 0
}
if plural.Type().Name() == "bool" {
if plural.Bool() {
return 1
} else {
return 0
}
}
if int(plural.Int()) > po.nplurals {
return 0
}
return int(plural.Int())
}
// 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
po.RLock()
defer po.RUnlock()
if po.translations != nil {
if _, ok := po.translations[str]; ok {
return fmt.Sprintf(po.translations[str].get(), vars...)
@@ -196,13 +369,58 @@ func (po *Po) Get(str string, vars ...interface{}) string {
return fmt.Sprintf(str, vars...)
}
// GetN retrieves the (N)th plural form translation for the given string.
// If n == 0, usually the singular form of the string is returned as defined in the PO file.
// 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
po.RLock()
defer po.RUnlock()
if po.translations != nil {
if _, ok := po.translations[str]; ok {
return fmt.Sprintf(po.translations[str].getN(n), vars...)
return fmt.Sprintf(po.translations[str].getN(po.pluralForm(n)), vars...)
}
}
// Return the plural string we received by default
return fmt.Sprintf(plural, vars...)
}
// GetC retrieves the corresponding translation for a given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
// Sync read
po.RLock()
defer po.RUnlock()
if po.contexts != nil {
if _, ok := po.contexts[ctx]; ok {
if po.contexts[ctx] != nil {
if _, ok := po.contexts[ctx][str]; ok {
return fmt.Sprintf(po.contexts[ctx][str].get(), vars...)
}
}
}
}
// Return the string we received by default
return fmt.Sprintf(str, vars...)
}
// GetNC retrieves the (N)th plural form 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
po.RLock()
defer po.RUnlock()
if po.contexts != nil {
if _, ok := po.contexts[ctx]; ok {
if po.contexts[ctx] != nil {
if _, ok := po.contexts[ctx][str]; ok {
return fmt.Sprintf(po.contexts[ctx][str].getN(po.pluralForm(n)), vars...)
}
}
}
}

View File

@@ -7,6 +7,375 @@ import (
)
func TestPo(t *testing.T) {
// Set PO content
str := `
msgid ""
msgstr ""
# Initial comment
# Headers below
"Language: en\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
# Some comment
msgid "My text"
msgstr "Translated text"
# More comments
msgid "Another string"
msgstr ""
#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"
msgstr[2] "And this is the second plural form: %s"
msgid "This one has invalid syntax translations"
msgid_plural "Plural index"
msgstr[abc] "Wrong index"
msgstr[1 "Forgot to close brackets"
msgstr[0] "Badly formatted string'
msgid "Invalid formatted id[] with no translations
msgctxt "Ctx"
msgid "One with var: %s"
msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"
msgid "Some random"
msgstr "Some random translation"
msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgid "More"
msgstr "More translation"
`
// 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())
}
// Create po object
po := new(Po)
// Try to parse a directory
po.ParseFile(path.Clean(os.TempDir()))
// Parse file
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 is the singular: Variable" {
t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr)
}
// Test multi-line
tr = po.Get("Multi-line")
if tr != "Multi line" {
t.Errorf("Expected 'Multi line' but got '%s'", tr)
}
// Test plural
tr = po.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 inexistent translations
tr = po.Get("This is a test")
if tr != "This is a test" {
t.Errorf("Expected 'This is a test' but got '%s'", tr)
}
tr = po.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 syntax error parsed translations
tr = po.Get("This one has invalid syntax translations")
if tr != "" {
t.Errorf("Expected '' but got '%s'", tr)
}
tr = po.GetN("This one has invalid syntax translations", "This are tests", 4)
if tr != "Plural index" {
t.Errorf("Expected 'Plural index' but got '%s'", tr)
}
// Test context translations
v = "Test"
tr = po.GetC("One with var: %s", "Ctx", v)
if tr != "This one is the singular in a Ctx context: Test" {
t.Errorf("Expected 'This one is the singular in a Ctx context: Test' but got '%s'", tr)
}
// Test plural
tr = po.GetNC("One with var: %s", "Several with vars: %s", 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 last translation
tr = po.Get("More")
if tr != "More translation" {
t.Errorf("Expected 'More translation' but got '%s'", tr)
}
}
func TestPoHeaders(t *testing.T) {
// Set PO content
str := `
msgid ""
msgstr ""
# Initial comment
# Headers below
"Language: en\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
# Some comment
msgid "Example"
msgstr "Translated example"
`
// Create po object
po := new(Po)
// Parse
po.Parse(str)
// Check headers expected
if po.Language != "en" {
t.Errorf("Expected 'Language: en' but got '%s'", po.Language)
}
// Check headers expected
if po.PluralForms != "nplurals=2; plural=(n != 1);" {
t.Errorf("Expected 'Plural-Forms: nplurals=2; plural=(n != 1);' but got '%s'", po.PluralForms)
}
}
func TestPluralForms(t *testing.T) {
// Single form
str := `
"Plural-Forms: nplurals=1; plural=0;"
# Some comment
msgid "Singular"
msgid_plural "Plural"
msgstr[0] "Singular form"
msgstr[1] "Plural form 1"
msgstr[2] "Plural form 2"
msgstr[3] "Plural form 3"
`
// Create po object
po := new(Po)
// Parse
po.Parse(str)
// Check plural form
n := po.pluralForm(0)
if n != 0 {
t.Errorf("Expected 0 for pluralForm(0), got %d", n)
}
n = po.pluralForm(1)
if n != 0 {
t.Errorf("Expected 0 for pluralForm(1), got %d", n)
}
n = po.pluralForm(2)
if n != 0 {
t.Errorf("Expected 0 for pluralForm(2), got %d", n)
}
n = po.pluralForm(3)
if n != 0 {
t.Errorf("Expected 0 for pluralForm(3), got %d", n)
}
n = po.pluralForm(50)
if n != 0 {
t.Errorf("Expected 0 for pluralForm(50), got %d", n)
}
// ------------------------------------------------------------------------
// 2 forms
str = `
"Plural-Forms: nplurals=2; plural=n != 1;"
# Some comment
msgid "Singular"
msgid_plural "Plural"
msgstr[0] "Singular form"
msgstr[1] "Plural form 1"
msgstr[2] "Plural form 2"
msgstr[3] "Plural form 3"
`
// Create po object
po = new(Po)
// Parse
po.Parse(str)
// Check plural form
n = po.pluralForm(0)
if n != 1 {
t.Errorf("Expected 1 for pluralForm(0), got %d", n)
}
n = po.pluralForm(1)
if n != 0 {
t.Errorf("Expected 0 for pluralForm(1), got %d", n)
}
n = po.pluralForm(2)
if n != 1 {
t.Errorf("Expected 1 for pluralForm(2), got %d", n)
}
n = po.pluralForm(3)
if n != 1 {
t.Errorf("Expected 1 for pluralForm(3), got %d", n)
}
// ------------------------------------------------------------------------
// 3 forms
str = `
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;"
# Some comment
msgid "Singular"
msgid_plural "Plural"
msgstr[0] "Singular form"
msgstr[1] "Plural form 1"
msgstr[2] "Plural form 2"
msgstr[3] "Plural form 3"
`
// Create po object
po = new(Po)
// Parse
po.Parse(str)
// Check plural form
n = po.pluralForm(0)
if n != 2 {
t.Errorf("Expected 2 for pluralForm(0), got %d", n)
}
n = po.pluralForm(1)
if n != 0 {
t.Errorf("Expected 0 for pluralForm(1), got %d", n)
}
n = po.pluralForm(2)
if n != 1 {
t.Errorf("Expected 1 for pluralForm(2), got %d", n)
}
n = po.pluralForm(3)
if n != 1 {
t.Errorf("Expected 1 for pluralForm(3), got %d", n)
}
n = po.pluralForm(100)
if n != 1 {
t.Errorf("Expected 1 for pluralForm(100), got %d", n)
}
n = po.pluralForm(49)
if n != 1 {
t.Errorf("Expected 1 for pluralForm(3), got %d", n)
}
// ------------------------------------------------------------------------
// 3 forms special
str = `
"Plural-Forms: nplurals=3;"
"plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
# Some comment
msgid "Singular"
msgid_plural "Plural"
msgstr[0] "Singular form"
msgstr[1] "Plural form 1"
msgstr[2] "Plural form 2"
msgstr[3] "Plural form 3"
`
// Create po object
po = new(Po)
// Parse
po.Parse(str)
// Check plural form
n = po.pluralForm(1)
if n != 0 {
t.Errorf("Expected 0 for pluralForm(1), got %d", n)
}
n = po.pluralForm(2)
if n != 1 {
t.Errorf("Expected 1 for pluralForm(2), got %d", n)
}
n = po.pluralForm(4)
if n != 1 {
t.Errorf("Expected 4 for pluralForm(4), got %d", n)
}
n = po.pluralForm(0)
if n != 2 {
t.Errorf("Expected 2 for pluralForm(2), got %d", n)
}
n = po.pluralForm(1000)
if n != 2 {
t.Errorf("Expected 2 for pluralForm(1000), got %d", n)
}
}
func TestTranslationObject(t *testing.T) {
tr := newTranslation()
str := tr.get()
if str != "" {
t.Errorf("Expected '' but got '%s'", str)
}
// Set id
tr.id = "Text"
// Get again
str = tr.get()
if str != "Text" {
t.Errorf("Expected 'Text' but got '%s'", str)
}
}
func TestPoRace(t *testing.T) {
// Set PO content
str := `# Some comment
msgid "My text"
@@ -23,39 +392,30 @@ msgstr[1] "This one is the plural: %s"
msgstr[2] "And this is the second plural form: %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
// Create Po object
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)
}
// Create sync channels
pc := make(chan bool)
rc := make(chan bool)
v := "Variable"
tr = po.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)
}
// Parse po content in a goroutine
go func(po *Po, done chan bool) {
po.Parse(str)
done <- true
}(po, pc)
// Test plural
tr = po.GetN("One with var: %s", "Several with vars: %s", 2, v)
if tr != "And this is the second plural form: Variable" {
t.Errorf("Expected 'And this is the second plural form: Variable' but got '%s'", tr)
}
// Read some translation on a goroutine
go func(po *Po, done chan bool) {
po.Get("My text")
done <- true
}(po, rc)
// Read something at top level
po.Get("My text")
// Wait for goroutines to finish
<-pc
<-rc
}