Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| c8ac87a60c | |||
| 67d7b9ca60 | |||
| bb9b8c2585 | |||
| b4ab6497b5 | |||
| 2441e55972 | |||
|
|
75a1fb9b04 | ||
|
|
3bb41424ff | ||
|
|
d791a97f01 | ||
|
|
192fa01364 | ||
|
|
362a1d6915 | ||
|
|
60d22914a5 | ||
|
|
f7464201e6 | ||
|
|
d26d707c73 | ||
|
|
b6df672e9a | ||
|
|
c40a6e9dd5 | ||
|
|
be1a13b346 | ||
|
|
36588ae653 | ||
|
|
20f50a5bba | ||
|
|
5b0ba55f37 | ||
|
|
df996c3ae1 | ||
|
|
f46758caed | ||
|
|
4aa838b8c3 | ||
|
|
478f4d29b7 | ||
|
|
9091772533 | ||
|
|
3b92b162e1 | ||
|
|
b8c9a57c39 | ||
|
|
1891633250 | ||
|
|
75a3d22c53 | ||
|
|
2b59b30398 | ||
|
|
208807f5ca | ||
|
|
823ca32c7a | ||
|
|
99a9166ded | ||
|
|
6431cb3aea | ||
|
|
0e382cfe26 | ||
|
|
ff3209d159 | ||
|
|
0216b71049 | ||
|
|
37bac2fe57 | ||
|
|
7b73c0d36b | ||
|
|
477ce49ddf | ||
|
|
f28243c54b | ||
|
|
4cbf30d337 | ||
|
|
302c88af99 | ||
|
|
8e9d9df2e2 | ||
|
|
431a313411 | ||
|
|
3c52f5c10b | ||
|
|
7fd6ac336a | ||
|
|
9161ab1e62 | ||
|
|
2044aeae11 | ||
|
|
7c73cc2975 | ||
|
|
48726a37dd | ||
|
|
c62dae3c6b | ||
|
|
3adb2eae86 | ||
| 4172a9c4a7 | |||
| cd46239477 | |||
|
|
8c36835ece | ||
| a19c5fd581 | |||
|
|
92b69ffa4c | ||
|
|
c583d0991b | ||
|
|
4d0fbfd720 | ||
|
|
d0a0759bca | ||
|
|
972f5685e7 | ||
|
|
36b0166de9 | ||
|
|
b44b8cf3f4 | ||
|
|
3f452f14f2 | ||
|
|
7d86bb66fe | ||
|
|
50cdb4e058 | ||
|
|
1fc8dec04d | ||
|
|
1bb93891f4 | ||
|
|
1e28907f7a | ||
|
|
4b94e83723 | ||
|
|
756045ab5e | ||
|
|
2bb9254f26 | ||
|
|
88952938dc | ||
|
|
cabefc2786 | ||
|
|
b8456329aa | ||
|
|
9bcfe68591 | ||
|
|
28bb992796 | ||
|
|
eccb6c7cf3 | ||
|
|
1c611eff63 | ||
|
|
4a52c7709d | ||
|
|
2c51ed2000 | ||
|
|
d52a867157 | ||
|
|
744b85e833 | ||
|
|
ffcea86f47 | ||
|
|
a735812a72 | ||
|
|
0284dca059 | ||
|
|
9a30bf7f45 | ||
|
|
520de6b223 | ||
|
|
468e983e10 | ||
|
|
04d24e4d7c | ||
|
|
41d0924ac6 | ||
|
|
745244309c |
14
.github/ISSUE_TEMPLATE.md
vendored
Normal file
14
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
# Please describe your issue
|
||||||
|
|
||||||
|
### Is this a bug, an improvement, a proposal or something else? Describe it.
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
### What's the expected behaviour, the current behaviour and the steps to reproduce it?
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
### Comments
|
||||||
|
|
||||||
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
23
.github/PULL_REQUEST_TEMPLATE.md
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Before creating your Pull Request...
|
||||||
|
|
||||||
|
- New Pull Requests should include a good description of what's being merged.
|
||||||
|
- Ideally, all Pull Requests are preceded by a discussion initiated in an Issue on this repository.
|
||||||
|
- For bug fixes is mandatory to have tests that cover and fail when the bug is present and will pass after this Pull Request.
|
||||||
|
- For changes and improvements, new tests have to be provided to cover the new features.
|
||||||
|
|
||||||
|
|
||||||
|
## Is this a fix, improvement or something else?
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
## What does this change implement/fix?
|
||||||
|
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
## I have ...
|
||||||
|
|
||||||
|
- [ ] answered the 2 questions above,
|
||||||
|
- [ ] discussed this change in an issue,
|
||||||
|
- [ ] included tests to cover this changes.
|
||||||
35
.github/workflows/build.yml
vendored
Normal file
35
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
name: Gotext build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ master ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
|
||||||
|
- name: Set up Go 1.13
|
||||||
|
uses: actions/setup-go@v1
|
||||||
|
with:
|
||||||
|
go-version: 1.13
|
||||||
|
id: go
|
||||||
|
|
||||||
|
- name: Check out code into the Go module directory
|
||||||
|
uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Get dependencies
|
||||||
|
run: |
|
||||||
|
go get -v -u -t -d ./...
|
||||||
|
|
||||||
|
- name: Build package
|
||||||
|
run: go build -v .
|
||||||
|
|
||||||
|
- name: Install xgotext CLI
|
||||||
|
run: go install -v git.deineagentur.com/DeineAgenturUG/gotext/cli/xgotext
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
run: go test -v -race ./...
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,6 +3,9 @@
|
|||||||
.settings
|
.settings
|
||||||
.buildpath
|
.buildpath
|
||||||
|
|
||||||
|
# golang jetbrains shit
|
||||||
|
.idea
|
||||||
|
|
||||||
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
# Compiled Object files, Static and Dynamic libs (Shared Objects)
|
||||||
*.o
|
*.o
|
||||||
*.a
|
*.a
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
language: go
|
language: go
|
||||||
script: go test -v -race ./...
|
|
||||||
|
|
||||||
go:
|
go:
|
||||||
- 1.5
|
- "1.9"
|
||||||
- 1.6
|
- "1.10"
|
||||||
- tip
|
- "1.11"
|
||||||
|
- "1.12"
|
||||||
|
- "tip"
|
||||||
|
|||||||
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
# Contributor Covenant Code of Conduct
|
||||||
|
|
||||||
|
## Our Pledge
|
||||||
|
|
||||||
|
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||||
|
|
||||||
|
## Our Standards
|
||||||
|
|
||||||
|
Examples of behavior that contributes to creating a positive environment include:
|
||||||
|
|
||||||
|
* Using welcoming and inclusive language
|
||||||
|
* Being respectful of differing viewpoints and experiences
|
||||||
|
* Gracefully accepting constructive criticism
|
||||||
|
* Focusing on what is best for the community
|
||||||
|
* Showing empathy towards other community members
|
||||||
|
|
||||||
|
Examples of unacceptable behavior by participants include:
|
||||||
|
|
||||||
|
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||||
|
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||||
|
* Public or private harassment
|
||||||
|
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||||
|
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||||
|
|
||||||
|
## Our Responsibilities
|
||||||
|
|
||||||
|
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||||
|
|
||||||
|
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
|
||||||
|
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at leonel.quinteros@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||||
|
|
||||||
|
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||||
|
|
||||||
|
## Attribution
|
||||||
|
|
||||||
|
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||||
|
|
||||||
|
[homepage]: http://contributor-covenant.org
|
||||||
|
[version]: http://contributor-covenant.org/version/1/4/
|
||||||
19
CONTRIBUTING.md
Normal file
19
CONTRIBUTING.md
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
# CONTRIBUTING
|
||||||
|
|
||||||
|
This open source project welcomes everybody that wants to contribute to it by implementing new features, fixing bugs, testing, creating documentation or simply talk about it.
|
||||||
|
|
||||||
|
Most contributions will start by creating a new Issue to discuss what is the contribution about and to agree on the steps to move forward.
|
||||||
|
|
||||||
|
## Issues
|
||||||
|
|
||||||
|
All issues reports are welcome. Open a new Issue whenever you want to report a bug, request a change or make a proposal.
|
||||||
|
|
||||||
|
This should be your start point of contribution.
|
||||||
|
|
||||||
|
|
||||||
|
## Pull Requests
|
||||||
|
|
||||||
|
If you have any changes that can be merged, feel free to send a Pull Request.
|
||||||
|
|
||||||
|
Usually, you'd want to create a new Issue to discuss about the change you want to merge and why it's needed or what it solves.
|
||||||
|
|
||||||
194
README.md
194
README.md
@@ -1,11 +1,13 @@
|
|||||||
[](https://godoc.org/github.com/leonelquinteros/gotext)
|
[](https://git.deineagentur.com/DeineAgenturUG/gotext)
|
||||||
|
[](LICENSE)
|
||||||
|
[](https://godoc.org/git.deineagentur.com/DeineAgenturUG/gotext)
|
||||||
|

|
||||||
[](https://travis-ci.org/leonelquinteros/gotext)
|
[](https://travis-ci.org/leonelquinteros/gotext)
|
||||||
|
[](https://goreportcard.com/report/git.deineagentur.com/DeineAgenturUG/gotext)
|
||||||
|
|
||||||
# Gotext
|
# Gotext
|
||||||
|
|
||||||
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
|
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
|
||||||
|
|
||||||
Version: [v1.1.0](https://github.com/leonelquinteros/gotext/releases/tag/v1.1.0)
|
|
||||||
|
|
||||||
|
|
||||||
# Features
|
# Features
|
||||||
@@ -16,11 +18,14 @@ Version: [v1.1.0](https://github.com/leonelquinteros/gotext/releases/tag/v1.1.0)
|
|||||||
- Support for variables inside translation strings using Go's [fmt syntax](https://golang.org/pkg/fmt/).
|
- 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 [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).
|
- 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.
|
- Support for MO files.
|
||||||
|
- 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.
|
- It works with UTF-8 encoding as it's the default for Go language.
|
||||||
- Unit tests available.
|
- Unit tests available.
|
||||||
- Language codes are automatically simplified from the form `en_UK` to `en` if the first isn't available.
|
- Language codes are automatically simplified from the form `en_UK` to `en` if the first isn't available.
|
||||||
- Ready to use inside Go templates.
|
- Ready to use inside Go templates.
|
||||||
|
- Objects are serializable to []byte to store them in cache.
|
||||||
|
- Support for Go Modules.
|
||||||
|
|
||||||
|
|
||||||
# License
|
# License
|
||||||
@@ -30,28 +35,55 @@ Version: [v1.1.0](https://github.com/leonelquinteros/gotext/releases/tag/v1.1.0)
|
|||||||
|
|
||||||
# Documentation
|
# Documentation
|
||||||
|
|
||||||
Refer to the Godoc package documentation at (https://godoc.org/github.com/leonelquinteros/gotext)
|
Refer to the Godoc package documentation at (https://godoc.org/git.deineagentur.com/DeineAgenturUG/gotext)
|
||||||
|
|
||||||
|
|
||||||
# Installation
|
# Installation
|
||||||
|
|
||||||
```
|
```
|
||||||
go get github.com/leonelquinteros/gotext
|
go get git.deineagentur.com/DeineAgenturUG/gotext
|
||||||
```
|
```
|
||||||
|
|
||||||
- There are no requirements or dependencies to use this package.
|
- There are no requirements or dependencies to use this package.
|
||||||
- No need to install GNU gettext utilities (unless specific needs of CLI tools).
|
- No need to install GNU gettext utilities (unless specific needs of CLI tools).
|
||||||
- No need for environment variables. Some naming conventions are applied but not needed.
|
- No need for environment variables. Some naming conventions are applied but not needed.
|
||||||
|
|
||||||
|
|
||||||
#### Version vendoring
|
## Version vendoring
|
||||||
|
|
||||||
Stable releases use [semantic versioning](http://semver.org/spec/v2.0.0.html) tagging on this repository.
|
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.
|
You can rely on this to use your preferred vendoring tool or to manually retrieve the corresponding release tag from the GitHub repository.
|
||||||
|
|
||||||
|
|
||||||
##### Vendoring with [gopkg.in](http://labix.org/gopkg.in)
|
### Vendoring with [Go Modules](https://github.com/golang/go/wiki/Modules) (Recommended)
|
||||||
|
|
||||||
|
Add `git.deineagentur.com/DeineAgenturUG/gotext` inside the `require` section in your `go.mod` file.
|
||||||
|
|
||||||
|
i.e.
|
||||||
|
```
|
||||||
|
require (
|
||||||
|
git.deineagentur.com/DeineAgenturUG/gotext v1.5.0
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Vendoring with [dep](https://golang.github.io/dep/)
|
||||||
|
|
||||||
|
To use last stable version (v1.5.0 at the moment of writing)
|
||||||
|
|
||||||
|
```
|
||||||
|
dep ensure -add git.deineagentur.com/DeineAgenturUG/gotext@v1.5.0
|
||||||
|
```
|
||||||
|
|
||||||
|
Import as
|
||||||
|
|
||||||
|
```go
|
||||||
|
import "git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### Vendoring with [gopkg.in](http://labix.org/gopkg.in)
|
||||||
|
|
||||||
[http://gopkg.in/leonelquinteros/gotext.v1](http://gopkg.in/leonelquinteros/gotext.v1)
|
[http://gopkg.in/leonelquinteros/gotext.v1](http://gopkg.in/leonelquinteros/gotext.v1)
|
||||||
|
|
||||||
@@ -61,7 +93,7 @@ To get the latest v1 package stable release, execute:
|
|||||||
go get gopkg.in/leonelquinteros/gotext.v1
|
go get gopkg.in/leonelquinteros/gotext.v1
|
||||||
```
|
```
|
||||||
|
|
||||||
To import this package, add the following line to your code:
|
Import as
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "gopkg.in/leonelquinteros/gotext.v1"
|
import "gopkg.in/leonelquinteros/gotext.v1"
|
||||||
@@ -72,19 +104,19 @@ Refer to it as gotext.
|
|||||||
|
|
||||||
# Locales directories structure
|
# Locales directories structure
|
||||||
|
|
||||||
The package will assume 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.
|
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, ...).
|
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.
|
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.
|
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).
|
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.
|
||||||
|
|
||||||
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
|
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
|
||||||
A library directory structure can look like:
|
A library directory structure can look like:
|
||||||
|
|
||||||
```
|
```
|
||||||
/path/to/locales
|
/path/to/locales
|
||||||
@@ -109,44 +141,11 @@ A library directory structure can look like:
|
|||||||
/path/to/locales/fr
|
/path/to/locales/fr
|
||||||
/path/to/locales/fr/default.po
|
/path/to/locales/fr/default.po
|
||||||
/path/to/locales/fr/extras.po
|
/path/to/locales/fr/extras.po
|
||||||
```
|
```
|
||||||
|
|
||||||
And so on...
|
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 basic translation function is usually `_()` in the form:
|
|
||||||
|
|
||||||
```
|
|
||||||
_("Translate this")
|
|
||||||
```
|
|
||||||
|
|
||||||
In Go, this can't be implemented by a reusable package as the function name has to start with a capital letter in order to be exported.
|
|
||||||
|
|
||||||
Each implementation of this package can declare this helper functions inside their own packages if this function naming are desired/needed:
|
|
||||||
|
|
||||||
```go
|
|
||||||
package main
|
|
||||||
|
|
||||||
import "github.com/leonelquinteros/gotext"
|
|
||||||
|
|
||||||
func _(str string, vars ...interface{}) string {
|
|
||||||
return gotext.Get(str, vars...)
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
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 behavior.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Usage examples
|
# Usage examples
|
||||||
|
|
||||||
## Using package for single language/domain settings
|
## Using package for single language/domain settings
|
||||||
@@ -154,38 +153,44 @@ This is a normal Go compiler behavior.
|
|||||||
For quick/simple translations you can use the package level functions directly.
|
For quick/simple translations you can use the package level functions directly.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "github.com/leonelquinteros/gotext"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Configure package
|
// Configure package
|
||||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
||||||
|
|
||||||
// Translate text from default domain
|
// Translate text from default domain
|
||||||
println(gotext.Get("My text on 'domain-name' domain"))
|
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
|
||||||
|
|
||||||
// Translate text from a different domain without reconfigure
|
// Translate text from a different domain without reconfigure
|
||||||
println(gotext.GetD("domain2", "Another text on a different domain"))
|
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Using dynamic variables on translations
|
## Using dynamic variables on translations
|
||||||
|
|
||||||
All translation strings support dynamic variables to be inserted without translate.
|
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.
|
Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "github.com/leonelquinteros/gotext"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Configure package
|
// Configure package
|
||||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
||||||
|
|
||||||
// Set variables
|
// Set variables
|
||||||
name := "John"
|
name := "John"
|
||||||
|
|
||||||
// Translate text with variables
|
// Translate text with variables
|
||||||
println(gotext.Get("Hi, my name is %s", name))
|
fmt.Println(gotext.Get("Hi, my name is %s", name))
|
||||||
}
|
}
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -193,32 +198,35 @@ func main() {
|
|||||||
|
|
||||||
## Using Locale object
|
## Using Locale object
|
||||||
|
|
||||||
When having multiple languages/domains/libraries at the same time, you can create Locale objects for each variation
|
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.
|
so you can handle each settings on their own.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "github.com/leonelquinteros/gotext"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Create Locale with library path and language code
|
// Create Locale with library path and language code
|
||||||
l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY")
|
l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY")
|
||||||
|
|
||||||
// Load domain '/path/to/locales/root/dir/es_UY/default.po'
|
// Load domain '/path/to/locales/root/dir/es_UY/default.po'
|
||||||
l.AddDomain("default")
|
l.AddDomain("default")
|
||||||
|
|
||||||
// Translate text from default domain
|
// Translate text from default domain
|
||||||
println(l.Get("Translate this"))
|
fmt.Println(l.Get("Translate this"))
|
||||||
|
|
||||||
// Load different domain
|
// Load different domain
|
||||||
l.AddDomain("translations")
|
l.AddDomain("translations")
|
||||||
|
|
||||||
// Translate text from domain
|
// Translate text from domain
|
||||||
println(l.GetD("translations", "Translate this"))
|
fmt.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.
|
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 template 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" }}
|
{{ .Loc.Get "Translate this" }}
|
||||||
@@ -227,11 +235,14 @@ If you set the Locale object as "Loc" in the template, then the template code wo
|
|||||||
|
|
||||||
## Using the Po object to handle .po files and PO-formatted strings
|
## Using the Po object to handle .po files and PO-formatted strings
|
||||||
|
|
||||||
For when you need to work with PO files and 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.
|
you can directly use the Po object to parse it and access the translations in there in the same way.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "github.com/leonelquinteros/gotext"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Set PO content
|
// Set PO content
|
||||||
@@ -245,12 +256,12 @@ msgstr ""
|
|||||||
msgid "One with var: %s"
|
msgid "One with var: %s"
|
||||||
msgstr "This one sets the var: %s"
|
msgstr "This one sets the var: %s"
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create Po object
|
// Create Po object
|
||||||
po := new(Po)
|
po := new(gotext.Po)
|
||||||
po.Parse(str)
|
po.Parse(str)
|
||||||
|
|
||||||
println(po.Get("Translate this"))
|
fmt.Println(po.Get("Translate this"))
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -261,10 +272,13 @@ 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
|
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)
|
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)
|
Plural formulas are parsed and evaluated using [Kinako](https://github.com/mattn/kinako)
|
||||||
|
|
||||||
```go
|
```go
|
||||||
import "github.com/leonelquinteros/gotext"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Set PO content
|
// Set PO content
|
||||||
@@ -286,21 +300,21 @@ msgid_plural "Several with vars: %s"
|
|||||||
msgstr[0] "This one is the singular: %s"
|
msgstr[0] "This one is the singular: %s"
|
||||||
msgstr[1] "This one is the plural: %s"
|
msgstr[1] "This one is the plural: %s"
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create Po object
|
// Create Po object
|
||||||
po := new(Po)
|
po := new(gotext.Po)
|
||||||
po.Parse(str)
|
po.Parse(str)
|
||||||
|
|
||||||
println(po.GetN("One with var: %s", "Several with vars: %s", 54, v))
|
fmt.Println(po.GetN("One with var: %s", "Several with vars: %s", 54, v))
|
||||||
// "This one is the plural: Variable"
|
// "This one is the plural: Variable"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
# Contribute
|
# Contribute
|
||||||
|
|
||||||
- Please, contribute.
|
- Please, contribute.
|
||||||
- Use the package on your projects.
|
- Use the package on your projects.
|
||||||
- Report issues on Github.
|
- Report issues on Github.
|
||||||
- Send pull requests for bugfixes and improvements.
|
- Send pull requests for bugfixes and improvements.
|
||||||
- Send proposals on Github issues.
|
- Send proposals on Github issues.
|
||||||
|
|||||||
48
cli/xgotext/README.md
Normal file
48
cli/xgotext/README.md
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# xgotext
|
||||||
|
|
||||||
|
CLI tool to extract translation strings from Go packages into .POT files.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
```
|
||||||
|
go install git.deineagentur.com/DeineAgenturUG/gotext/cli/xgotext
|
||||||
|
```
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
```
|
||||||
|
Usage of xgotext:
|
||||||
|
-default string
|
||||||
|
Name of default domain (default "default")
|
||||||
|
-exclude string
|
||||||
|
Comma separated list of directories to exclude (default ".git")
|
||||||
|
-in string
|
||||||
|
input dir: /path/to/go/pkg
|
||||||
|
-out string
|
||||||
|
output dir: /path/to/i18n/files
|
||||||
|
```
|
||||||
|
|
||||||
|
## Implementation
|
||||||
|
|
||||||
|
This is the first (naive) implementation for this tool.
|
||||||
|
|
||||||
|
It will scan the Go package provided for method calls that matches the method names from the gotext package and write the corresponding translation files to the output directory.
|
||||||
|
|
||||||
|
Isn't able to parse calls to translation functions using parameters inside variables, if the translation string is inside a variable and that variable is used to invoke the translation function, this tool won't be able to parse that string. See this example code:
|
||||||
|
|
||||||
|
```go
|
||||||
|
// This line will be added to the .po file
|
||||||
|
gotext.Get("Translate this")
|
||||||
|
|
||||||
|
tr := "Translate this string"
|
||||||
|
// The following line will NOT be added to the .pot file
|
||||||
|
gotext.Get(tr)
|
||||||
|
```
|
||||||
|
|
||||||
|
The CLI tool traverse sub-directories based on the given input directory.
|
||||||
|
|
||||||
|
|
||||||
|
## Contribute
|
||||||
|
|
||||||
|
Please
|
||||||
|
|
||||||
28
cli/xgotext/fixtures/i18n/default.po
Normal file
28
cli/xgotext/fixtures/i18n/default.po
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: \n"
|
||||||
|
"X-Generator: xgotext\n"
|
||||||
|
|
||||||
|
|
||||||
|
#: fixtures/main.go:23
|
||||||
|
#. gotext.Get
|
||||||
|
msgid "My text on 'domain-name' domain"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: fixtures/main.go:38
|
||||||
|
#. l.GetN
|
||||||
|
msgid "Singular"
|
||||||
|
msgid_plural "Plural"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
#: fixtures/main.go:40
|
||||||
|
#. l.GetN
|
||||||
|
msgid "SingularVar"
|
||||||
|
msgid_plural "PluralVar"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
15
cli/xgotext/fixtures/i18n/domain.po
Normal file
15
cli/xgotext/fixtures/i18n/domain.po
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: \n"
|
||||||
|
"X-Generator: xgotext\n"
|
||||||
|
|
||||||
|
|
||||||
|
#: fixtures/main.go:42
|
||||||
|
#. l.GetDC
|
||||||
|
msgctxt "ctx"
|
||||||
|
msgid "string"
|
||||||
|
msgstr ""
|
||||||
14
cli/xgotext/fixtures/i18n/domain2.po
Normal file
14
cli/xgotext/fixtures/i18n/domain2.po
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: \n"
|
||||||
|
"X-Generator: xgotext\n"
|
||||||
|
|
||||||
|
|
||||||
|
#: fixtures/main.go:26
|
||||||
|
#. gotext.GetD
|
||||||
|
msgid "Another text on a different domain"
|
||||||
|
msgstr ""
|
||||||
22
cli/xgotext/fixtures/i18n/translations.po
Normal file
22
cli/xgotext/fixtures/i18n/translations.po
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: \n"
|
||||||
|
"X-Generator: xgotext\n"
|
||||||
|
|
||||||
|
|
||||||
|
#: fixtures/main.go:35
|
||||||
|
#. l.GetD
|
||||||
|
msgid "Translate this"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: fixtures/main.go:43
|
||||||
|
#. l.GetNDC
|
||||||
|
msgctxt "NDC-CTX"
|
||||||
|
msgid "ndc"
|
||||||
|
msgid_plural "ndcs"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
90
cli/xgotext/fixtures/main.go
Normal file
90
cli/xgotext/fixtures/main.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
alias "git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext/cli/xgotext/fixtures/pkg"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fake object with methods similar to gotext
|
||||||
|
type Fake struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get by id
|
||||||
|
func (f Fake) Get(id int) int {
|
||||||
|
return 42
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fake object with same methods as gotext
|
||||||
|
type Fake2 struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get by str
|
||||||
|
func (f Fake2) Get(s string) string {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Configure package
|
||||||
|
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
||||||
|
|
||||||
|
// Translate text from default domain
|
||||||
|
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
|
||||||
|
// same as before
|
||||||
|
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
|
||||||
|
|
||||||
|
// unsupported function call
|
||||||
|
trStr := "some string to translate"
|
||||||
|
fmt.Println(gotext.Get(trStr))
|
||||||
|
|
||||||
|
// same with alias package name
|
||||||
|
fmt.Println(alias.Get("alias call"))
|
||||||
|
|
||||||
|
// Translate text from a different domain without reconfigure
|
||||||
|
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
|
||||||
|
|
||||||
|
// 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("translations")
|
||||||
|
l.SetDomain("translations")
|
||||||
|
|
||||||
|
// Translate text from domain
|
||||||
|
fmt.Println(l.GetD("translations", "Translate this"))
|
||||||
|
|
||||||
|
// Get plural translations
|
||||||
|
l.GetN("Singular", "Plural", 4)
|
||||||
|
num := 17
|
||||||
|
l.GetN("SingularVar", "PluralVar", num)
|
||||||
|
|
||||||
|
l.GetDC("domain2", "string", "ctx")
|
||||||
|
l.GetNDC("translations", "ndc", "ndcs", 7, "NDC-CTX")
|
||||||
|
|
||||||
|
// try fake structs
|
||||||
|
f := Fake{}
|
||||||
|
f.Get(3)
|
||||||
|
|
||||||
|
f2 := Fake2{}
|
||||||
|
f2.Get("3")
|
||||||
|
|
||||||
|
// use translator of sub object
|
||||||
|
t := pkg.Translate{}
|
||||||
|
t.L.Get("translate package")
|
||||||
|
t.S.L.Get("translate sub package")
|
||||||
|
|
||||||
|
// redefine alias with fake struct
|
||||||
|
alias := Fake2{}
|
||||||
|
alias.Get("3")
|
||||||
|
|
||||||
|
err := errors.New("test")
|
||||||
|
fmt.Print(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// dummy function
|
||||||
|
func dummy(locale *gotext.Locale) {
|
||||||
|
locale.Get("inside dummy")
|
||||||
|
}
|
||||||
16
cli/xgotext/fixtures/pkg/pkg.go
Normal file
16
cli/xgotext/fixtures/pkg/pkg.go
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
package pkg
|
||||||
|
|
||||||
|
import "git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
|
||||||
|
type SubTranslate struct {
|
||||||
|
L gotext.Locale
|
||||||
|
}
|
||||||
|
|
||||||
|
type Translate struct {
|
||||||
|
L gotext.Locale
|
||||||
|
S SubTranslate
|
||||||
|
}
|
||||||
|
|
||||||
|
func test() {
|
||||||
|
gotext.Get("inside sub package")
|
||||||
|
}
|
||||||
45
cli/xgotext/main.go
Normal file
45
cli/xgotext/main.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext/cli/xgotext/parser"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dirName = flag.String("in", "", "input dir: /path/to/go/pkg")
|
||||||
|
outputDir = flag.String("out", "", "output dir: /path/to/i18n/files")
|
||||||
|
defaultDomain = flag.String("default", "default", "Name of default domain")
|
||||||
|
excludeDirs = flag.String("exclude", ".git", "Comma separated list of directories to exclude")
|
||||||
|
verbose = flag.Bool("v", false, "print currently handled directory")
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
|
|
||||||
|
// Init logger
|
||||||
|
log.SetFlags(0)
|
||||||
|
|
||||||
|
if *dirName == "" {
|
||||||
|
log.Fatal("No input directory given")
|
||||||
|
}
|
||||||
|
if *outputDir == "" {
|
||||||
|
log.Fatal("No output directory given")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := &parser.DomainMap{
|
||||||
|
Default: *defaultDomain,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := parser.ParseDirRec(*dirName, strings.Split(*excludeDirs, ","), data, *verbose)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = data.Save(*outputDir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
195
cli/xgotext/parser/domain.go
Normal file
195
cli/xgotext/parser/domain.go
Normal file
@@ -0,0 +1,195 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Translation for a text to translate
|
||||||
|
type Translation struct {
|
||||||
|
MsgId string
|
||||||
|
MsgIdPlural string
|
||||||
|
Context string
|
||||||
|
SourceLocations []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddLocations to translation
|
||||||
|
func (t *Translation) AddLocations(locations []string) {
|
||||||
|
if t.SourceLocations == nil {
|
||||||
|
t.SourceLocations = locations
|
||||||
|
} else {
|
||||||
|
t.SourceLocations = append(t.SourceLocations, locations...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump translation as string
|
||||||
|
func (t *Translation) Dump() string {
|
||||||
|
data := make([]string, 0, len(t.SourceLocations)+5)
|
||||||
|
|
||||||
|
for _, location := range t.SourceLocations {
|
||||||
|
data = append(data, "#: "+location)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t.Context != "" {
|
||||||
|
data = append(data, "msgctxt "+t.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, "msgid "+t.MsgId)
|
||||||
|
|
||||||
|
if t.MsgIdPlural == "" {
|
||||||
|
data = append(data, "msgstr \"\"")
|
||||||
|
} else {
|
||||||
|
data = append(data,
|
||||||
|
"msgid_plural "+t.MsgIdPlural,
|
||||||
|
"msgstr[0] \"\"",
|
||||||
|
"msgstr[1] \"\"")
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.Join(data, "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslationMap contains a map of translations with the ID as key
|
||||||
|
type TranslationMap map[string]*Translation
|
||||||
|
|
||||||
|
// Dump the translation map as string
|
||||||
|
func (m TranslationMap) Dump() string {
|
||||||
|
// sort by translation id for consistence output
|
||||||
|
keys := make([]string, 0, len(m))
|
||||||
|
for k := range m {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
data := make([]string, 0, len(m))
|
||||||
|
for _, key := range keys {
|
||||||
|
data = append(data, (m)[key].Dump())
|
||||||
|
}
|
||||||
|
return strings.Join(data, "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Domain holds all translations of one domain
|
||||||
|
type Domain struct {
|
||||||
|
Translations TranslationMap
|
||||||
|
ContextTranslations map[string]TranslationMap
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTranslation to the domain
|
||||||
|
func (d *Domain) AddTranslation(translation *Translation) {
|
||||||
|
if d.Translations == nil {
|
||||||
|
d.Translations = make(TranslationMap)
|
||||||
|
d.ContextTranslations = make(map[string]TranslationMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if translation.Context == "" {
|
||||||
|
if t, ok := d.Translations[translation.MsgId]; ok {
|
||||||
|
t.AddLocations(translation.SourceLocations)
|
||||||
|
} else {
|
||||||
|
d.Translations[translation.MsgId] = translation
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if _, ok := d.ContextTranslations[translation.Context]; !ok {
|
||||||
|
d.ContextTranslations[translation.Context] = make(TranslationMap)
|
||||||
|
}
|
||||||
|
|
||||||
|
if t, ok := d.ContextTranslations[translation.Context][translation.MsgId]; ok {
|
||||||
|
t.AddLocations(translation.SourceLocations)
|
||||||
|
} else {
|
||||||
|
d.ContextTranslations[translation.Context][translation.MsgId] = translation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dump the domain as string
|
||||||
|
func (d *Domain) Dump() string {
|
||||||
|
data := make([]string, 0, len(d.ContextTranslations)+1)
|
||||||
|
data = append(data, d.Translations.Dump())
|
||||||
|
|
||||||
|
// sort context translations by context for consistence output
|
||||||
|
keys := make([]string, 0, len(d.ContextTranslations))
|
||||||
|
for k := range d.ContextTranslations {
|
||||||
|
keys = append(keys, k)
|
||||||
|
}
|
||||||
|
sort.Strings(keys)
|
||||||
|
|
||||||
|
for _, key := range keys {
|
||||||
|
data = append(data, d.ContextTranslations[key].Dump())
|
||||||
|
}
|
||||||
|
return strings.Join(data, "\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save domain to file
|
||||||
|
func (d *Domain) Save(path string) error {
|
||||||
|
file, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to domain: %v", err)
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// write header
|
||||||
|
_, err = file.WriteString(`msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
"MIME-Version: 1.0\n"
|
||||||
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
|
"Language: \n"
|
||||||
|
"X-Generator: xgotext\n"
|
||||||
|
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// write domain content
|
||||||
|
_, err = file.WriteString(d.Dump())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DomainMap contains multiple domains as map with name as key
|
||||||
|
type DomainMap struct {
|
||||||
|
Domains map[string]*Domain
|
||||||
|
Default string
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddTranslation to domain map
|
||||||
|
func (m *DomainMap) AddTranslation(domain string, translation *Translation) {
|
||||||
|
if m.Domains == nil {
|
||||||
|
m.Domains = make(map[string]*Domain, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// use "default" as default domain if not set
|
||||||
|
if m.Default == "" {
|
||||||
|
m.Default = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
// no domain given -> use default
|
||||||
|
if domain == "" {
|
||||||
|
domain = m.Default
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := m.Domains[domain]; !ok {
|
||||||
|
m.Domains[domain] = new(Domain)
|
||||||
|
}
|
||||||
|
m.Domains[domain].AddTranslation(translation)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save domains to directory
|
||||||
|
func (m *DomainMap) Save(directory string) error {
|
||||||
|
// ensure output directory exist
|
||||||
|
err := os.MkdirAll(directory, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create output dir: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// save each domain in a separate po file
|
||||||
|
for name, domain := range m.Domains {
|
||||||
|
err := domain.Save(filepath.Join(directory, name+".pot"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to save domain %s: %v", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
271
cli/xgotext/parser/golang.go
Normal file
271
cli/xgotext/parser/golang.go
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"go/ast"
|
||||||
|
"go/token"
|
||||||
|
"go/types"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"golang.org/x/tools/go/packages"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetterDef describes a getter
|
||||||
|
type GetterDef struct {
|
||||||
|
Id int
|
||||||
|
Plural int
|
||||||
|
Context int
|
||||||
|
Domain int
|
||||||
|
}
|
||||||
|
|
||||||
|
// maxArgIndex returns the largest argument index
|
||||||
|
func (d *GetterDef) maxArgIndex() int {
|
||||||
|
m := d.Id
|
||||||
|
if d.Plural > m {
|
||||||
|
m = d.Plural
|
||||||
|
}
|
||||||
|
if d.Context > m {
|
||||||
|
m = d.Context
|
||||||
|
}
|
||||||
|
if d.Domain > m {
|
||||||
|
m = d.Domain
|
||||||
|
}
|
||||||
|
return m
|
||||||
|
}
|
||||||
|
|
||||||
|
// list of supported getter
|
||||||
|
var gotextGetter = map[string]GetterDef{
|
||||||
|
"Get": {0, -1, -1, -1},
|
||||||
|
"GetN": {0, 1, -1, -1},
|
||||||
|
"GetD": {1, -1, -1, 0},
|
||||||
|
"GetND": {1, 2, -1, 0},
|
||||||
|
"GetC": {0, -1, 1, -1},
|
||||||
|
"GetNC": {0, 1, 3, -1},
|
||||||
|
"GetDC": {1, -1, 2, 0},
|
||||||
|
"GetNDC": {1, 2, 4, 0},
|
||||||
|
}
|
||||||
|
|
||||||
|
// register go parser
|
||||||
|
func init() {
|
||||||
|
AddParser(goParser)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parse go package
|
||||||
|
func goParser(dirPath, basePath string, data *DomainMap) error {
|
||||||
|
fileSet := token.NewFileSet()
|
||||||
|
|
||||||
|
conf := packages.Config{
|
||||||
|
Mode: packages.NeedName |
|
||||||
|
packages.NeedFiles |
|
||||||
|
packages.NeedSyntax |
|
||||||
|
packages.NeedTypes |
|
||||||
|
packages.NeedTypesInfo,
|
||||||
|
Fset: fileSet,
|
||||||
|
Dir: basePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
// load package from path
|
||||||
|
pkgs, err := packages.Load(&packages.Config{
|
||||||
|
Mode: conf.Mode,
|
||||||
|
Fset: fileSet,
|
||||||
|
Dir: dirPath,
|
||||||
|
})
|
||||||
|
if err != nil || len(pkgs) == 0 {
|
||||||
|
// not a go package
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// handle each file
|
||||||
|
for _, node := range pkgs[0].Syntax {
|
||||||
|
file := GoFile{
|
||||||
|
pkgConf: &conf,
|
||||||
|
filePath: fileSet.Position(node.Package).Filename,
|
||||||
|
basePath: basePath,
|
||||||
|
data: data,
|
||||||
|
fileSet: fileSet,
|
||||||
|
|
||||||
|
importedPackages: map[string]*packages.Package{
|
||||||
|
pkgs[0].Name: pkgs[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ast.Inspect(node, file.inspectFile)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GoFile handles the parsing of one go file
|
||||||
|
type GoFile struct {
|
||||||
|
filePath string
|
||||||
|
basePath string
|
||||||
|
data *DomainMap
|
||||||
|
|
||||||
|
fileSet *token.FileSet
|
||||||
|
pkgConf *packages.Config
|
||||||
|
|
||||||
|
importedPackages map[string]*packages.Package
|
||||||
|
}
|
||||||
|
|
||||||
|
// getPackage loads module by name
|
||||||
|
func (g *GoFile) getPackage(name string) (*packages.Package, error) {
|
||||||
|
pkgs, err := packages.Load(g.pkgConf, name)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(pkgs) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return pkgs[0], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getType from ident object
|
||||||
|
func (g *GoFile) getType(ident *ast.Ident) types.Object {
|
||||||
|
for _, pkg := range g.importedPackages {
|
||||||
|
if obj, ok := pkg.TypesInfo.Uses[ident]; ok {
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GoFile) inspectFile(n ast.Node) bool {
|
||||||
|
switch x := n.(type) {
|
||||||
|
// get names of imported packages
|
||||||
|
case *ast.ImportSpec:
|
||||||
|
packageName, _ := strconv.Unquote(x.Path.Value)
|
||||||
|
|
||||||
|
pkg, err := g.getPackage(packageName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("failed to load package %s: %s", packageName, err)
|
||||||
|
} else {
|
||||||
|
if x.Name == nil {
|
||||||
|
g.importedPackages[pkg.Name] = pkg
|
||||||
|
} else {
|
||||||
|
g.importedPackages[x.Name.Name] = pkg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check each function call
|
||||||
|
case *ast.CallExpr:
|
||||||
|
g.inspectCallExpr(x)
|
||||||
|
|
||||||
|
default:
|
||||||
|
print()
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkType for gotext object
|
||||||
|
func (g *GoFile) checkType(rawType types.Type) bool {
|
||||||
|
switch t := rawType.(type) {
|
||||||
|
case *types.Pointer:
|
||||||
|
return g.checkType(t.Elem())
|
||||||
|
|
||||||
|
case *types.Named:
|
||||||
|
if t.Obj().Pkg() == nil || t.Obj().Pkg().Path() != "git.deineagentur.com/DeineAgenturUG/gotext" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GoFile) inspectCallExpr(n *ast.CallExpr) {
|
||||||
|
// must be a selector expression otherwise it is a local function call
|
||||||
|
expr, ok := n.Fun.(*ast.SelectorExpr)
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e := expr.X.(type) {
|
||||||
|
// direct call
|
||||||
|
case *ast.Ident:
|
||||||
|
// object is a package if the Obj is not set
|
||||||
|
if e.Obj == nil {
|
||||||
|
pkg, ok := g.importedPackages[e.Name]
|
||||||
|
if !ok || pkg.PkgPath != "git.deineagentur.com/DeineAgenturUG/gotext" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// validate type of object
|
||||||
|
t := g.getType(e)
|
||||||
|
if t == nil || !g.checkType(t.Type()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// call to attribute
|
||||||
|
case *ast.SelectorExpr:
|
||||||
|
// validate type of object
|
||||||
|
t := g.getType(e.Sel)
|
||||||
|
if t == nil || !g.checkType(t.Type()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// convert args
|
||||||
|
args := make([]*ast.BasicLit, len(n.Args))
|
||||||
|
for idx, arg := range n.Args {
|
||||||
|
args[idx], _ = arg.(*ast.BasicLit)
|
||||||
|
}
|
||||||
|
|
||||||
|
// get position
|
||||||
|
path, _ := filepath.Rel(g.basePath, g.filePath)
|
||||||
|
position := fmt.Sprintf("%s:%d", path, g.fileSet.Position(n.Lparen).Line)
|
||||||
|
|
||||||
|
// handle getters
|
||||||
|
if def, ok := gotextGetter[expr.Sel.String()]; ok {
|
||||||
|
g.parseGetter(def, args, position)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *GoFile) parseGetter(def GetterDef, args []*ast.BasicLit, pos string) {
|
||||||
|
// check if enough arguments are given
|
||||||
|
if len(args) < def.maxArgIndex() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// get domain
|
||||||
|
var domain string
|
||||||
|
if def.Domain != -1 {
|
||||||
|
domain, _ = strconv.Unquote(args[def.Domain].Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// only handle function calls with strings as ID
|
||||||
|
if args[def.Id] == nil || args[def.Id].Kind != token.STRING {
|
||||||
|
log.Printf("ERR: Unsupported call at %s (ID not a string)", pos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
trans := Translation{
|
||||||
|
MsgId: args[def.Id].Value,
|
||||||
|
SourceLocations: []string{pos},
|
||||||
|
}
|
||||||
|
if def.Plural > 0 {
|
||||||
|
// plural ID must be a string
|
||||||
|
if args[def.Plural] == nil || args[def.Plural].Kind != token.STRING {
|
||||||
|
log.Printf("ERR: Unsupported call at %s (Plural not a string)", pos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trans.MsgIdPlural = args[def.Plural].Value
|
||||||
|
}
|
||||||
|
if def.Context > 0 {
|
||||||
|
// Context must be a string
|
||||||
|
if args[def.Context] == nil || args[def.Context].Kind != token.STRING {
|
||||||
|
log.Printf("ERR: Unsupported call at %s (Context not a string)", pos)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trans.Context = args[def.Context].Value
|
||||||
|
}
|
||||||
|
|
||||||
|
g.data.AddTranslation(domain, &trans)
|
||||||
|
}
|
||||||
67
cli/xgotext/parser/parser.go
Normal file
67
cli/xgotext/parser/parser.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package parser
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ParseDirFunc parses one directory
|
||||||
|
type ParseDirFunc func(filePath, basePath string, data *DomainMap) error
|
||||||
|
|
||||||
|
var knownParser []ParseDirFunc
|
||||||
|
|
||||||
|
// AddParser to the known parser list
|
||||||
|
func AddParser(parser ParseDirFunc) {
|
||||||
|
if knownParser == nil {
|
||||||
|
knownParser = []ParseDirFunc{parser}
|
||||||
|
} else {
|
||||||
|
knownParser = append(knownParser, parser)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDir calls all known parser for each directory
|
||||||
|
func ParseDir(dirPath, basePath string, data *DomainMap) error {
|
||||||
|
dirPath, _ = filepath.Abs(dirPath)
|
||||||
|
basePath, _ = filepath.Abs(basePath)
|
||||||
|
|
||||||
|
for _, parser := range knownParser {
|
||||||
|
err := parser(dirPath, basePath, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseDirRec calls all known parser for each directory
|
||||||
|
func ParseDirRec(dirPath string, exclude []string, data *DomainMap, verbose bool) error {
|
||||||
|
dirPath, _ = filepath.Abs(dirPath)
|
||||||
|
|
||||||
|
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if info.IsDir() {
|
||||||
|
// skip directory if in exclude list
|
||||||
|
subDir, _ := filepath.Rel(dirPath, path)
|
||||||
|
for _, d := range exclude {
|
||||||
|
if strings.HasPrefix(subDir, d) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if verbose {
|
||||||
|
log.Print(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := ParseDir(path, dirPath, data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
19
fixtures/ar/categories.po
Normal file
19
fixtures/ar/categories.po
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Plural-Forms: nplurals=6; plural=(n==0 ? 0 : n==1 ? 1 : n==2 ? 2 : n%100>=3 && n%100<=10 ? 3 : n%100>=11 && n%100<=99 ? 4 : 5);\n"
|
||||||
|
|
||||||
|
msgid "Alcohol & Tobacco"
|
||||||
|
msgstr "الكحول والتبغ"
|
||||||
|
|
||||||
|
# this test data is purposely missing msgstr
|
||||||
|
msgid "%d selected"
|
||||||
|
msgid_plural "%d selected"
|
||||||
|
|
||||||
|
msgid "Load %d more document"
|
||||||
|
msgid_plural "Load %d more documents"
|
||||||
|
msgstr[0] "حمّل %d مستندات إضافيّة"
|
||||||
|
msgstr[1] "حمّل مستند واحد إضافي"
|
||||||
|
msgstr[2] "حمّل مستندين إضافيين"
|
||||||
|
msgstr[3] "حمّل %d مستندات إضافيّة"
|
||||||
|
msgstr[4] "حمّل %d مستندا إضافيّا"
|
||||||
|
msgstr[5] "حمّل %d مستند إضافي"
|
||||||
2
fixtures/ar/no_plural_header.po
Normal file
2
fixtures/ar/no_plural_header.po
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
msgid "Alcohol & Tobacco"
|
||||||
|
msgstr "الكحول والتبغ"
|
||||||
BIN
fixtures/de/default.mo
Normal file
BIN
fixtures/de/default.mo
Normal file
Binary file not shown.
77
fixtures/de/default.po
Normal file
77
fixtures/de/default.po
Normal file
@@ -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 <froehle@b1-systems.de>\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"
|
||||||
BIN
fixtures/de_DE/LC_MESSAGES/default.mo
Normal file
BIN
fixtures/de_DE/LC_MESSAGES/default.mo
Normal file
Binary file not shown.
77
fixtures/de_DE/LC_MESSAGES/default.po
Normal file
77
fixtures/de_DE/LC_MESSAGES/default.po
Normal file
@@ -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 <froehle@b1-systems.de>\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"
|
||||||
68
fixtures/en_AU/default.po
Normal file
68
fixtures/en_AU/default.po
Normal file
@@ -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 <froehle@b1-systems.de>\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"
|
||||||
BIN
fixtures/en_GB/default.mo
Normal file
BIN
fixtures/en_GB/default.mo
Normal file
Binary file not shown.
BIN
fixtures/en_US/default.mo
Normal file
BIN
fixtures/en_US/default.mo
Normal file
Binary file not shown.
68
fixtures/en_US/default.po
Normal file
68
fixtures/en_US/default.po
Normal file
@@ -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 <froehle@b1-systems.de>\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"
|
||||||
BIN
fixtures/fr/LC_MESSAGES/default.mo
Normal file
BIN
fixtures/fr/LC_MESSAGES/default.mo
Normal file
Binary file not shown.
77
fixtures/fr/LC_MESSAGES/default.po
Normal file
77
fixtures/fr/LC_MESSAGES/default.po
Normal file
@@ -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 <froehle@b1-systems.de>\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"
|
||||||
7
go.mod
Normal file
7
go.mod
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
module git.deineagentur.com/DeineAgenturUG/gotext
|
||||||
|
|
||||||
|
// go: no requirements found in Gopkg.lock
|
||||||
|
|
||||||
|
require golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd
|
||||||
|
|
||||||
|
go 1.13
|
||||||
14
go.sum
Normal file
14
go.sum
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
|
||||||
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||||
|
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd h1:hHkvGJK23seRCflePJnVa9IMv8fsuavSCWKd11kDQFs=
|
||||||
|
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
202
gotext.go
202
gotext.go
@@ -1,151 +1,247 @@
|
|||||||
/*
|
/*
|
||||||
Package gotext implements GNU gettext utilities.
|
Package gotext implements GNU gettext utilities.
|
||||||
|
|
||||||
For quick/simple translations you can use the package level functions directly.
|
For quick/simple translations you can use the package level functions directly.
|
||||||
|
|
||||||
import "github.com/leonelquinteros/gotext"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Configure package
|
// Configure package
|
||||||
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
|
||||||
|
|
||||||
// Translate text from default domain
|
// Translate text from default domain
|
||||||
println(gotext.Get("My text on 'domain-name' domain"))
|
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
|
||||||
|
|
||||||
// Translate text from a different domain without reconfigure
|
// Translate text from a different domain without reconfigure
|
||||||
println(gotext.GetD("domain2", "Another text on a different domain"))
|
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
package gotext
|
package gotext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
// Global environment variables
|
// Global environment variables
|
||||||
var (
|
type config struct {
|
||||||
|
sync.RWMutex
|
||||||
|
|
||||||
// Default domain to look at when no domain is specified. Used by package level functions.
|
// Default domain to look at when no domain is specified. Used by package level functions.
|
||||||
domain = "default"
|
domain string
|
||||||
|
|
||||||
// Language set.
|
// Language set.
|
||||||
language = "en_US"
|
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 = "/usr/local/share/locale"
|
library string
|
||||||
|
|
||||||
// Storage for package level methods
|
// Storage for package level methods
|
||||||
storage *Locale
|
storage *Locale
|
||||||
)
|
}
|
||||||
|
|
||||||
|
var globalConfig *config
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Init default configuration
|
||||||
|
globalConfig = &config{
|
||||||
|
domain: "default",
|
||||||
|
language: "en_US",
|
||||||
|
library: "/usr/local/share/locale",
|
||||||
|
storage: nil,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register Translator types for gob encoding
|
||||||
|
gob.Register(TranslatorEncoding{})
|
||||||
|
}
|
||||||
|
|
||||||
// loadStorage creates a new Locale object at package level based on the Global variables settings.
|
// 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.
|
// It's called automatically when trying to use Get or GetD methods.
|
||||||
func loadStorage(force bool) {
|
func loadStorage(force bool) {
|
||||||
if storage == nil || force {
|
globalConfig.Lock()
|
||||||
storage = NewLocale(library, language)
|
|
||||||
|
if globalConfig.storage == nil || force {
|
||||||
|
globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _, ok := storage.domains[domain]; !ok || force {
|
if _, ok := globalConfig.storage.Domains[globalConfig.domain]; !ok || force {
|
||||||
storage.AddDomain(domain)
|
globalConfig.storage.AddDomain(globalConfig.domain)
|
||||||
}
|
}
|
||||||
|
globalConfig.storage.SetDomain(globalConfig.domain)
|
||||||
|
|
||||||
|
globalConfig.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetDomain is the domain getter for the package configuration
|
// GetDomain is the domain getter for the package configuration
|
||||||
func GetDomain() string {
|
func GetDomain() string {
|
||||||
return domain
|
var dom string
|
||||||
|
globalConfig.RLock()
|
||||||
|
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.
|
// 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) {
|
func SetDomain(dom string) {
|
||||||
domain = dom
|
globalConfig.Lock()
|
||||||
|
globalConfig.domain = dom
|
||||||
|
if globalConfig.storage != nil {
|
||||||
|
globalConfig.storage.SetDomain(dom)
|
||||||
|
}
|
||||||
|
globalConfig.Unlock()
|
||||||
|
|
||||||
loadStorage(true)
|
loadStorage(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLanguage is the language getter for the package configuration
|
// GetLanguage is the language getter for the package configuration
|
||||||
func GetLanguage() string {
|
func GetLanguage() string {
|
||||||
return language
|
globalConfig.RLock()
|
||||||
|
lang := globalConfig.language
|
||||||
|
globalConfig.RUnlock()
|
||||||
|
|
||||||
|
return lang
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLanguage sets the language code to be used at package level.
|
// 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) {
|
func SetLanguage(lang string) {
|
||||||
language = lang
|
globalConfig.Lock()
|
||||||
|
globalConfig.language = SimplifiedLocale(lang)
|
||||||
|
globalConfig.Unlock()
|
||||||
|
|
||||||
loadStorage(true)
|
loadStorage(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetLibrary is the library getter for the package configuration
|
// GetLibrary is the library getter for the package configuration
|
||||||
func GetLibrary() string {
|
func GetLibrary() string {
|
||||||
return library
|
globalConfig.RLock()
|
||||||
|
lib := globalConfig.library
|
||||||
|
globalConfig.RUnlock()
|
||||||
|
|
||||||
|
return lib
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetLibrary sets the root path for the loale directories and files to be used at package level.
|
// 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) {
|
func SetLibrary(lib string) {
|
||||||
library = lib
|
globalConfig.Lock()
|
||||||
|
globalConfig.library = lib
|
||||||
|
globalConfig.Unlock()
|
||||||
|
|
||||||
loadStorage(true)
|
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.
|
// It receives the library path, language code and domain name.
|
||||||
// This function is recommended to be used when changing more than one setting,
|
// 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) {
|
func Configure(lib, lang, dom string) {
|
||||||
library = lib
|
globalConfig.Lock()
|
||||||
language = lang
|
globalConfig.library = lib
|
||||||
domain = dom
|
globalConfig.language = SimplifiedLocale(lang)
|
||||||
|
globalConfig.domain = dom
|
||||||
|
globalConfig.Unlock()
|
||||||
|
|
||||||
loadStorage(true)
|
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.
|
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
|
||||||
func Get(str string, vars ...interface{}) string {
|
func Get(str string, vars ...interface{}) string {
|
||||||
return GetD(domain, str, vars...)
|
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.
|
// 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 {
|
func GetN(str, plural string, n int, vars ...interface{}) string {
|
||||||
return GetND("default", str, plural, n, vars...)
|
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.
|
// 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 {
|
func GetD(dom, str string, vars ...interface{}) string {
|
||||||
return GetND(dom, str, str, 1, vars...)
|
// Try to load default package Locale storage
|
||||||
|
loadStorage(false)
|
||||||
|
|
||||||
|
// Return Translation
|
||||||
|
globalConfig.RLock()
|
||||||
|
|
||||||
|
if _, ok := globalConfig.storage.Domains[dom]; !ok {
|
||||||
|
globalConfig.storage.AddDomain(dom)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := globalConfig.storage.GetD(dom, str, vars...)
|
||||||
|
globalConfig.RUnlock()
|
||||||
|
|
||||||
|
return tr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// 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 {
|
func GetND(dom, str, plural string, n int, vars ...interface{}) string {
|
||||||
// Try to load default package Locale storage
|
// Try to load default package Locale storage
|
||||||
loadStorage(false)
|
loadStorage(false)
|
||||||
|
|
||||||
// Return translation
|
// Return Translation
|
||||||
return storage.GetND(dom, str, plural, n, vars...)
|
globalConfig.RLock()
|
||||||
|
|
||||||
|
if _, ok := globalConfig.storage.Domains[dom]; !ok {
|
||||||
|
globalConfig.storage.AddDomain(dom)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr := globalConfig.storage.GetND(dom, str, plural, n, vars...)
|
||||||
|
globalConfig.RUnlock()
|
||||||
|
|
||||||
|
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.
|
// 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 {
|
func GetC(str, ctx string, vars ...interface{}) string {
|
||||||
return GetDC(domain, str, ctx, vars...)
|
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.
|
// 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 {
|
func GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||||
return GetNDC("default", str, plural, n, ctx, vars...)
|
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.
|
// 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 {
|
func GetDC(dom, str, ctx string, vars ...interface{}) string {
|
||||||
return GetNDC(dom, str, str, 1, ctx, vars...)
|
// Try to load default package Locale storage
|
||||||
|
loadStorage(false)
|
||||||
|
|
||||||
|
// Return Translation
|
||||||
|
globalConfig.RLock()
|
||||||
|
tr := globalConfig.storage.GetDC(dom, str, ctx, vars...)
|
||||||
|
globalConfig.RUnlock()
|
||||||
|
|
||||||
|
return tr
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// 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 {
|
func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||||
// Try to load default package Locale storage
|
// Try to load default package Locale storage
|
||||||
loadStorage(false)
|
loadStorage(false)
|
||||||
|
|
||||||
// Return translation
|
// Return Translation
|
||||||
return storage.GetNDC(dom, str, plural, n, ctx, vars...)
|
globalConfig.RLock()
|
||||||
|
tr := globalConfig.storage.GetNDC(dom, str, plural, n, ctx, vars...)
|
||||||
|
globalConfig.RUnlock()
|
||||||
|
|
||||||
|
return tr
|
||||||
}
|
}
|
||||||
|
|||||||
385
gotext_test.go
385
gotext_test.go
@@ -2,7 +2,8 @@ package gotext
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,10 +33,12 @@ func TestGettersSetters(t *testing.T) {
|
|||||||
func TestPackageFunctions(t *testing.T) {
|
func TestPackageFunctions(t *testing.T) {
|
||||||
// Set PO content
|
// Set PO content
|
||||||
str := `
|
str := `
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr "Project-Id-Version: %s\n"
|
||||||
|
"Report-Msgid-Bugs-To: %s\n"
|
||||||
|
|
||||||
# Initial comment
|
# Initial comment
|
||||||
# Headers below
|
# More Headers below
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
"Content-Type: text/plain; charset=UTF-8\n"
|
"Content-Type: text/plain; charset=UTF-8\n"
|
||||||
"Content-Transfer-Encoding: 8bit\n"
|
"Content-Transfer-Encoding: 8bit\n"
|
||||||
@@ -55,14 +58,6 @@ msgstr[0] "This one is the singular: %s"
|
|||||||
msgstr[1] "This one is the plural: %s"
|
msgstr[1] "This one is the plural: %s"
|
||||||
msgstr[2] "And this is the second plural form: %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"
|
msgctxt "Ctx"
|
||||||
msgid "One with var: %s"
|
msgid "One with var: %s"
|
||||||
msgid_plural "Several with vars: %s"
|
msgid_plural "Several with vars: %s"
|
||||||
@@ -70,38 +65,74 @@ msgstr[0] "This one is the singular in a Ctx context: %s"
|
|||||||
msgstr[1] "This one is the plural in a Ctx context: %s"
|
msgstr[1] "This one is the plural in a Ctx context: %s"
|
||||||
|
|
||||||
msgid "Some random"
|
msgid "Some random"
|
||||||
msgstr "Some random translation"
|
msgstr "Some random Translation"
|
||||||
|
|
||||||
msgctxt "Ctx"
|
msgctxt "Ctx"
|
||||||
msgid "Some random in a context"
|
msgid "Some random in a context"
|
||||||
msgstr "Some random translation in a context"
|
msgstr "Some random Translation in a context"
|
||||||
|
|
||||||
msgid "More"
|
msgid "More"
|
||||||
msgstr "More translation"
|
msgstr "More Translation"
|
||||||
|
|
||||||
`
|
msgid "Untranslated"
|
||||||
|
msgid_plural "Several untranslated"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
anotherStr := `
|
||||||
|
msgid ""
|
||||||
|
msgstr "Project-Id-Version: %s\n"
|
||||||
|
"Report-Msgid-Bugs-To: %s\n"
|
||||||
|
|
||||||
|
# Initial comment
|
||||||
|
# More 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"
|
||||||
|
|
||||||
|
msgid "Another text on a different domain"
|
||||||
|
msgstr "Another text on another domain"
|
||||||
|
`
|
||||||
|
|
||||||
// Create Locales directory on default location
|
// Create Locales directory on default location
|
||||||
dirname := path.Clean("/tmp" + string(os.PathSeparator) + "en_US")
|
dirname := filepath.Join("/tmp", "en_US")
|
||||||
err := os.MkdirAll(dirname, os.ModePerm)
|
err := os.MkdirAll(dirname, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Can't create test directory: %s", err.Error())
|
t.Fatalf("Can't create test directory: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write PO content to default domain file
|
// Write PO content to default domain file
|
||||||
filename := path.Clean(dirname + string(os.PathSeparator) + "default.po")
|
filename := filepath.Join(dirname, "default.po")
|
||||||
|
|
||||||
f, err := os.Create(filename)
|
f, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Can't create test file: %s", err.Error())
|
t.Fatalf("Can't create test file: %s", err.Error())
|
||||||
}
|
}
|
||||||
defer f.Close()
|
|
||||||
|
|
||||||
_, err = f.WriteString(str)
|
_, err = f.WriteString(str)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Can't write to test file: %s", err.Error())
|
t.Fatalf("Can't write to test file: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
anotherFilename := filepath.Join(dirname, "another.po")
|
||||||
|
|
||||||
|
af, err := os.Create(anotherFilename)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Can't create test file: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = af.WriteString(anotherStr)
|
||||||
|
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()
|
||||||
|
af.Close()
|
||||||
|
|
||||||
// Set package configuration
|
// Set package configuration
|
||||||
Configure("/tmp", "en_US", "default")
|
Configure("/tmp", "en_US", "default")
|
||||||
|
|
||||||
@@ -125,8 +156,8 @@ msgstr "More translation"
|
|||||||
|
|
||||||
// Test context translations
|
// Test context translations
|
||||||
tr = GetC("Some random in a context", "Ctx")
|
tr = GetC("Some random in a context", "Ctx")
|
||||||
if tr != "Some random translation in a context" {
|
if tr != "Some random Translation in a context" {
|
||||||
t.Errorf("Expected 'Some random translation in a context' but got '%s'", tr)
|
t.Errorf("Expected 'Some random Translation in a context' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
v = "Variable"
|
v = "Variable"
|
||||||
@@ -139,6 +170,218 @@ msgstr "More translation"
|
|||||||
if tr != "This one is the plural in a Ctx context: Variable" {
|
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)
|
t.Errorf("Expected 'This one is the plural in a Ctx context: Variable' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tr = GetD("another", "Another text on a different domain")
|
||||||
|
if tr != "Another text on another domain" {
|
||||||
|
t.Errorf("Expected 'Another text on another domain' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUntranslated(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"
|
||||||
|
|
||||||
|
msgid "Untranslated"
|
||||||
|
msgid_plural "Several untranslated"
|
||||||
|
msgstr[0] ""
|
||||||
|
msgstr[1] ""
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
// Create Locales directory on default location
|
||||||
|
dirname := filepath.Join("/tmp", "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 := filepath.Join(dirname, "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 untranslated
|
||||||
|
tr := Get("Untranslated")
|
||||||
|
if tr != "Untranslated" {
|
||||||
|
t.Errorf("Expected 'Untranslated' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
tr = GetN("Untranslated", "Several untranslated", 1)
|
||||||
|
if tr != "Untranslated" {
|
||||||
|
t.Errorf("Expected 'Untranslated' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = GetN("Untranslated", "Several untranslated", 2)
|
||||||
|
if tr != "Several untranslated" {
|
||||||
|
t.Errorf("Expected 'Several untranslated' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = GetD("default", "Untranslated")
|
||||||
|
if tr != "Untranslated" {
|
||||||
|
t.Errorf("Expected 'Untranslated' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
tr = GetND("default", "Untranslated", "Several untranslated", 1)
|
||||||
|
if tr != "Untranslated" {
|
||||||
|
t.Errorf("Expected 'Untranslated' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = GetND("default", "Untranslated", "Several untranslated", 2)
|
||||||
|
if tr != "Several untranslated" {
|
||||||
|
t.Errorf("Expected 'Several untranslated' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 := `
|
||||||
|
msgid ""
|
||||||
|
msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
msgid "Default text"
|
||||||
|
msgid_plural "Default texts"
|
||||||
|
msgstr[0] "Default Translation"
|
||||||
|
msgstr[1] "Default translations"
|
||||||
|
|
||||||
|
msgctxt "Ctx"
|
||||||
|
msgid "Default context"
|
||||||
|
msgid_plural "Default contexts"
|
||||||
|
msgstr[0] "Default ctx Translation"
|
||||||
|
msgstr[1] "Default ctx translations"
|
||||||
|
`
|
||||||
|
|
||||||
|
strCustom := `
|
||||||
|
msgid ""
|
||||||
|
msgstr "Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
msgid "Custom text"
|
||||||
|
msgid_plural "Custom texts"
|
||||||
|
msgstr[0] "Custom Translation"
|
||||||
|
msgstr[1] "Custom translations"
|
||||||
|
|
||||||
|
msgctxt "Ctx"
|
||||||
|
msgid "Custom context"
|
||||||
|
msgid_plural "Custom contexts"
|
||||||
|
msgstr[0] "Custom ctx Translation"
|
||||||
|
msgstr[1] "Custom ctx translations"
|
||||||
|
`
|
||||||
|
|
||||||
|
// Create Locales directory and files on temp location
|
||||||
|
dirname := filepath.Join("/tmp", "en_US")
|
||||||
|
err := os.MkdirAll(dirname, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Can't create test directory: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
fDefault, err := os.Create(filepath.Join(dirname, "default.po"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Can't create test file: %s", err.Error())
|
||||||
|
}
|
||||||
|
defer fDefault.Close()
|
||||||
|
|
||||||
|
fCustom, err := os.Create(filepath.Join(dirname, "custom.po"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Can't create test file: %s", err.Error())
|
||||||
|
}
|
||||||
|
defer fCustom.Close()
|
||||||
|
|
||||||
|
_, err = fDefault.WriteString(strDefault)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Can't write to test file: %s", err.Error())
|
||||||
|
}
|
||||||
|
_, err = fCustom.WriteString(strCustom)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Can't write to test file: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
Configure("/tmp", "en_US", "default")
|
||||||
|
|
||||||
|
// Check default domain Translation
|
||||||
|
SetDomain("default")
|
||||||
|
tr := Get("Default text")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
tr = GetNC("Default context", "Default contexts", 23, "Ctx")
|
||||||
|
if tr != "Default ctx translations" {
|
||||||
|
t.Errorf("Expected 'Default ctx translations'. Got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
SetDomain("custom")
|
||||||
|
tr = Get("Custom text")
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
tr = GetNC("Custom context", "Custom contexts", 23, "Ctx")
|
||||||
|
if tr != "Custom ctx translations" {
|
||||||
|
t.Errorf("Expected 'Custom ctx translations'. Got '%s'", tr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPackageRace(t *testing.T) {
|
func TestPackageRace(t *testing.T) {
|
||||||
@@ -157,17 +400,21 @@ msgstr[0] "This one is the singular: %s"
|
|||||||
msgstr[1] "This one is the plural: %s"
|
msgstr[1] "This one is the plural: %s"
|
||||||
msgstr[2] "And this is the second plural form: %s"
|
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"
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
// Create Locales directory on default location
|
// Create Locales directory on default location
|
||||||
dirname := path.Clean(library + string(os.PathSeparator) + "en_US")
|
dirname := filepath.Join("/tmp", "en_US")
|
||||||
err := os.MkdirAll(dirname, os.ModePerm)
|
err := os.MkdirAll(dirname, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Can't create test directory: %s", err.Error())
|
t.Fatalf("Can't create test directory: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write PO content to default domain file
|
// Write PO content to default domain file
|
||||||
filename := path.Clean(dirname + string(os.PathSeparator) + domain + ".po")
|
filename := filepath.Join("/tmp", GetDomain()+".po")
|
||||||
|
|
||||||
f, err := os.Create(filename)
|
f, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -180,20 +427,84 @@ msgstr[2] "And this is the second plural form: %s"
|
|||||||
t.Fatalf("Can't write to test file: %s", err.Error())
|
t.Fatalf("Can't write to test file: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init sync channels
|
var wg sync.WaitGroup
|
||||||
c1 := make(chan bool)
|
|
||||||
c2 := make(chan bool)
|
|
||||||
|
|
||||||
// Test translations
|
for i := 0; i < 1000; i++ {
|
||||||
go func(done chan bool) {
|
wg.Add(1)
|
||||||
Get("My text")
|
// Test translations
|
||||||
done <- true
|
go func() {
|
||||||
}(c1)
|
defer wg.Done()
|
||||||
|
|
||||||
go func(done chan bool) {
|
GetLibrary()
|
||||||
Get("My text")
|
SetLibrary(filepath.Join("/tmp", "gotextlib"))
|
||||||
done <- true
|
GetDomain()
|
||||||
}(c2)
|
SetDomain("default")
|
||||||
|
GetLanguage()
|
||||||
|
SetLanguage("en_US")
|
||||||
|
Configure("/tmp", "en_US", "default")
|
||||||
|
|
||||||
Get("My text")
|
Get("My text")
|
||||||
|
GetN("One with var: %s", "Several with vars: %s", 0, "test")
|
||||||
|
GetC("Some random in a context", "Ctx")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageArabicTranslation(t *testing.T) {
|
||||||
|
Configure("fixtures/", "ar", "categories")
|
||||||
|
|
||||||
|
// Plurals formula missing + Plural translation string missing
|
||||||
|
tr := GetD("categories", "Alcohol & Tobacco")
|
||||||
|
if tr != "الكحول والتبغ" {
|
||||||
|
t.Errorf("Expected to get 'الكحول والتبغ', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plural translation string present without translations, should get the msgid_plural
|
||||||
|
tr = GetND("categories", "%d selected", "%d selected", 10)
|
||||||
|
if tr != "%d selected" {
|
||||||
|
t.Errorf("Expected to get '%%d selected', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Plurals formula present + Plural translation string present and complete
|
||||||
|
tr = GetND("categories", "Load %d more document", "Load %d more documents", 0)
|
||||||
|
if tr != "حمّل %d مستندات إضافيّة" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[0]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = GetND("categories", "Load %d more document", "Load %d more documents", 1)
|
||||||
|
if tr != "حمّل مستند واحد إضافي" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[1]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = GetND("categories", "Load %d more document", "Load %d more documents", 2)
|
||||||
|
if tr != "حمّل مستندين إضافيين" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[2]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = GetND("categories", "Load %d more document", "Load %d more documents", 6)
|
||||||
|
if tr != "حمّل %d مستندات إضافيّة" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[3]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = GetND("categories", "Load %d more document", "Load %d more documents", 116)
|
||||||
|
if tr != "حمّل %d مستندا إضافيّا" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[4]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = GetND("categories", "Load %d more document", "Load %d more documents", 102)
|
||||||
|
if tr != "حمّل %d مستند إضافي" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[5]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPackageArabicMissingPluralForm(t *testing.T) {
|
||||||
|
Configure("fixtures/", "ar", "no_plural_header")
|
||||||
|
|
||||||
|
// Get translation
|
||||||
|
tr := GetD("no_plural_header", "Alcohol & Tobacco")
|
||||||
|
if tr != "الكحول والتبغ" {
|
||||||
|
t.Errorf("Expected to get 'الكحول والتبغ', but got '%s'", tr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
86
helper.go
Normal file
86
helper.go
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* 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]*[svTtbcdoqXxUeEfFgGp]`)
|
||||||
|
|
||||||
|
// SimplifiedLocale simplified locale like " en_US"/"de_DE "/en_US.UTF-8/zh_CN/zh_TW/el_GR@euro/... to en_US, de_DE, zh_CN, el_GR...
|
||||||
|
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
|
||||||
|
// NPrintf("%(name)s is Type %(type)s", map[string]interface{}{"name": "Gotext", "type": "struct"})
|
||||||
|
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
|
||||||
|
}
|
||||||
112
helper_test.go
Normal file
112
helper_test.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
383
locale.go
383
locale.go
@@ -1,9 +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
|
package gotext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"bytes"
|
||||||
|
"encoding/gob"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,25 +20,29 @@ multiple languages at the same time by working with this object.
|
|||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
import "github.com/leonelquinteros/gotext"
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Create Locale with library path and language code
|
// Create Locale with library path and language code
|
||||||
l := gotext.NewLocale("/path/to/i18n/dir", "en_US")
|
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")
|
l.AddDomain("default")
|
||||||
|
|
||||||
// Translate text from default domain
|
// Translate text from default domain
|
||||||
println(l.Get("Translate this"))
|
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")
|
l.AddDomain("extras")
|
||||||
|
|
||||||
// Translate text from domain
|
// Translate text from domain
|
||||||
println(l.GetD("extras", "Translate this"))
|
fmt.Println(l.GetD("extras", "Translate this"))
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
type Locale struct {
|
type Locale struct {
|
||||||
// Path to locale files.
|
// Path to locale files.
|
||||||
@@ -41,136 +51,403 @@ type Locale struct {
|
|||||||
// Language for this Locale
|
// Language for this Locale
|
||||||
lang string
|
lang string
|
||||||
|
|
||||||
// List of available domains for this locale.
|
// List of available Domains for this locale.
|
||||||
domains map[string]*Po
|
Domains map[string]Translator
|
||||||
|
|
||||||
|
// First AddDomain is default Domain
|
||||||
|
defaultDomain string
|
||||||
|
|
||||||
// Sync Mutex
|
// Sync Mutex
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLocale creates and initializes a new Locale object for a given language.
|
// 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).
|
// It receives a path for the i18n .po/.mo files directory (p) and a language code to use (l).
|
||||||
func NewLocale(p, l string) *Locale {
|
func NewLocale(p, l string) *Locale {
|
||||||
return &Locale{
|
return &Locale{
|
||||||
path: p,
|
path: p,
|
||||||
lang: l,
|
lang: SimplifiedLocale(l),
|
||||||
domains: make(map[string]*Po),
|
Domains: make(map[string]Translator),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Locale) findPO(dom string) string {
|
//SetLang ...
|
||||||
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+".po")
|
func (l *Locale) SetLang(lang string) {
|
||||||
|
l.lang = lang
|
||||||
|
}
|
||||||
|
|
||||||
|
//GetLang ...
|
||||||
|
func (l *Locale) GetLang() string {
|
||||||
|
return l.lang
|
||||||
|
}
|
||||||
|
|
||||||
|
//SetPath ...
|
||||||
|
func (l *Locale) SetPath(path string) {
|
||||||
|
l.path = path
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Locale) findExt(dom, ext string) string {
|
||||||
|
filename := filepath.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
|
||||||
if _, err := os.Stat(filename); err == nil {
|
if _, err := os.Stat(filename); err == nil {
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(l.lang) > 2 {
|
if len(l.lang) > 2 {
|
||||||
filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+".po")
|
filename = filepath.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
|
||||||
if _, err := os.Stat(filename); err == nil {
|
if _, err := os.Stat(filename); err == nil {
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
filename = path.Join(l.path, l.lang, dom+".po")
|
filename = filepath.Join(l.path, l.lang, dom+"."+ext)
|
||||||
if _, err := os.Stat(filename); err == nil {
|
if _, err := os.Stat(filename); err == nil {
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(l.lang) > 2 {
|
if len(l.lang) > 2 {
|
||||||
filename = path.Join(l.path, l.lang[:2], dom+".po")
|
filename = filepath.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.
|
// AddDomain creates a new domain for a given locale object and initializes the Po object.
|
||||||
// If the domain exists, it gets reloaded.
|
// If the domain exists, it gets reloaded.
|
||||||
func (l *Locale) AddDomain(dom string) {
|
func (l *Locale) AddDomain(dom string) {
|
||||||
po := new(Po)
|
var poObj Translator
|
||||||
|
|
||||||
// Parse file.
|
file := l.findExt(dom, "po")
|
||||||
po.ParseFile(l.findPO(dom))
|
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
|
// Save new domain
|
||||||
l.Lock()
|
l.Lock()
|
||||||
defer l.Unlock()
|
|
||||||
|
|
||||||
if l.domains == nil {
|
if l.Domains == nil {
|
||||||
l.domains = make(map[string]*Po)
|
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.
|
// AddTranslator takes a domain name and a Translator object to make it available in the Locale object.
|
||||||
|
func (l *Locale) AddTranslator(dom string, tr Translator) {
|
||||||
|
l.Lock()
|
||||||
|
|
||||||
|
if l.Domains == nil {
|
||||||
|
l.Domains = make(map[string]Translator)
|
||||||
|
}
|
||||||
|
if l.defaultDomain == "" {
|
||||||
|
l.defaultDomain = dom
|
||||||
|
}
|
||||||
|
l.Domains[dom] = tr
|
||||||
|
|
||||||
|
l.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDomain is the domain getter for Locale 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.
|
||||||
|
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.
|
// 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 {
|
func (l *Locale) Get(str string, vars ...interface{}) string {
|
||||||
return l.GetD("default", str, vars...)
|
return l.GetD(l.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.
|
// 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 {
|
func (l *Locale) GetN(str, plural string, n int, vars ...interface{}) string {
|
||||||
return l.GetND("default", str, plural, n, vars...)
|
return l.GetND(l.GetDomain(), 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.
|
// 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 {
|
func (l *Locale) GetD(dom, str string, vars ...interface{}) string {
|
||||||
return l.GetND(dom, str, str, 1, vars...)
|
// 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].Get(str, vars...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Printf(str, 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.
|
// 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 {
|
func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
|
||||||
// Sync read
|
// Sync read
|
||||||
l.RLock()
|
l.RLock()
|
||||||
defer l.RUnlock()
|
defer l.RUnlock()
|
||||||
|
|
||||||
if l.domains != nil {
|
if l.Domains != nil {
|
||||||
if _, ok := l.domains[dom]; ok {
|
if _, ok := l.Domains[dom]; ok {
|
||||||
if l.domains[dom] != nil {
|
if l.Domains[dom] != nil {
|
||||||
return l.domains[dom].GetN(str, plural, n, vars...)
|
return l.Domains[dom].GetN(str, plural, n, vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the same we received by default
|
// Use western default rule (plural > 1) to handle missing domain default result.
|
||||||
return fmt.Sprintf(plural, vars...)
|
if n == 1 {
|
||||||
|
return Printf(str, 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.
|
// 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 {
|
func (l *Locale) GetC(str, ctx string, vars ...interface{}) string {
|
||||||
return l.GetDC("default", str, ctx, vars...)
|
return l.GetDC(l.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.
|
// 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 {
|
func (l *Locale) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||||
return l.GetNDC("default", str, plural, n, ctx, vars...)
|
return l.GetNDC(l.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.
|
// 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 {
|
func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string {
|
||||||
return l.GetNDC(dom, str, str, 1, ctx, vars...)
|
// 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].GetC(str, ctx, vars...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Printf(str, 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.
|
// 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 {
|
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||||
// Sync read
|
// Sync read
|
||||||
l.RLock()
|
l.RLock()
|
||||||
defer l.RUnlock()
|
defer l.RUnlock()
|
||||||
|
|
||||||
if l.domains != nil {
|
if l.Domains != nil {
|
||||||
if _, ok := l.domains[dom]; ok {
|
if _, ok := l.Domains[dom]; ok {
|
||||||
if l.domains[dom] != nil {
|
if l.Domains[dom] != nil {
|
||||||
return l.domains[dom].GetNC(str, plural, n, ctx, vars...)
|
return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the same we received by default
|
// Use western default rule (plural > 1) to handle missing domain default result.
|
||||||
return fmt.Sprintf(plural, vars...)
|
if n == 1 {
|
||||||
|
return Printf(str, vars...)
|
||||||
|
}
|
||||||
|
return Printf(plural, vars...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (l *Locale) GetE(str string, vars ...interface{}) (string, bool) {
|
||||||
|
return l.GetDE(l.GetDomain(), str, vars...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (l *Locale) GetNE(str, plural string, n int, vars ...interface{}) (string, bool) {
|
||||||
|
return l.GetNDE(l.GetDomain(), str, plural, n, vars...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (l *Locale) GetDE(dom, str string, vars ...interface{}) (string, bool) {
|
||||||
|
// 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].GetE(str, vars...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNDE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (l *Locale) GetNDE(dom, str, plural string, n int, vars ...interface{}) (string, bool) {
|
||||||
|
// 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].GetNE(str, plural, n, vars...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use western default rule (plural > 1) to handle missing domain default result.
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (l *Locale) GetCE(str, ctx string, vars ...interface{}) (string, bool) {
|
||||||
|
return l.GetDCE(l.GetDomain(), str, ctx, vars...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNCE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (l *Locale) GetNCE(str, plural string, n int, ctx string, vars ...interface{}) (string, bool) {
|
||||||
|
return l.GetNDCE(l.GetDomain(), str, plural, n, ctx, vars...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetDCE 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) GetDCE(dom, str, ctx string, vars ...interface{}) (string, bool) {
|
||||||
|
// 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].GetCE(str, ctx, vars...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNDCE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (l *Locale) GetNDCE(dom, str, plural string, n int, ctx string, vars ...interface{}) (string, bool) {
|
||||||
|
// 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].GetNCE(str, plural, n, ctx, vars...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use western default rule (plural > 1) to handle missing domain default result.
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// LocaleEncoding is used as intermediary storage to encode Locale objects to Gob.
|
||||||
|
type LocaleEncoding struct {
|
||||||
|
Path string
|
||||||
|
Lang string
|
||||||
|
Domains map[string][]byte
|
||||||
|
DefaultDomain string
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary implements encoding BinaryMarshaler interface
|
||||||
|
func (l *Locale) MarshalBinary() ([]byte, error) {
|
||||||
|
obj := new(LocaleEncoding)
|
||||||
|
obj.DefaultDomain = l.defaultDomain
|
||||||
|
obj.Domains = make(map[string][]byte)
|
||||||
|
for k, v := range l.Domains {
|
||||||
|
var err error
|
||||||
|
obj.Domains[k], err = v.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
obj.Lang = l.lang
|
||||||
|
obj.Path = l.path
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
encoder := gob.NewEncoder(&buff)
|
||||||
|
err := encoder.Encode(obj)
|
||||||
|
|
||||||
|
return buff.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements encoding BinaryUnmarshaler interface
|
||||||
|
func (l *Locale) UnmarshalBinary(data []byte) error {
|
||||||
|
buff := bytes.NewBuffer(data)
|
||||||
|
obj := new(LocaleEncoding)
|
||||||
|
|
||||||
|
decoder := gob.NewDecoder(buff)
|
||||||
|
err := decoder.Decode(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.defaultDomain = obj.DefaultDomain
|
||||||
|
l.lang = obj.Lang
|
||||||
|
l.path = obj.Path
|
||||||
|
|
||||||
|
// Decode Domains
|
||||||
|
l.Domains = make(map[string]Translator)
|
||||||
|
for k, v := range obj.Domains {
|
||||||
|
var tr TranslatorEncoding
|
||||||
|
buff := bytes.NewBuffer(v)
|
||||||
|
trDecoder := gob.NewDecoder(buff)
|
||||||
|
err := trDecoder.Decode(&tr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
l.Domains[k] = tr.GetTranslator()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
459
locale_test.go
459
locale_test.go
@@ -1,8 +1,13 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
package gotext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -38,6 +43,134 @@ msgstr[abc] "Wrong index"
|
|||||||
msgstr[1 "Forgot to close brackets"
|
msgstr[1 "Forgot to close brackets"
|
||||||
msgstr[0] "Badly formatted string'
|
msgstr[0] "Badly formatted string'
|
||||||
|
|
||||||
|
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 := filepath.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 := filepath.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 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
v = "Test"
|
||||||
|
tr = l.GetNC("One with var: %s", "Several with vars: %s", 23, "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'. Got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 TestLocaleFails(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
|
msgid "Invalid formatted id[] with no translations
|
||||||
|
|
||||||
msgctxt "Ctx"
|
msgctxt "Ctx"
|
||||||
@@ -47,26 +180,26 @@ msgstr[0] "This one is the singular in a Ctx context: %s"
|
|||||||
msgstr[1] "This one is the plural in a Ctx context: %s"
|
msgstr[1] "This one is the plural in a Ctx context: %s"
|
||||||
|
|
||||||
msgid "Some random"
|
msgid "Some random"
|
||||||
msgstr "Some random translation"
|
msgstr "Some random Translation"
|
||||||
|
|
||||||
msgctxt "Ctx"
|
msgctxt "Ctx"
|
||||||
msgid "Some random in a context"
|
msgid "Some random in a context"
|
||||||
msgstr "Some random translation in a context"
|
msgstr "Some random Translation in a context"
|
||||||
|
|
||||||
msgid "More"
|
msgid "More"
|
||||||
msgstr "More translation"
|
msgstr "More Translation"
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create Locales directory with simplified language code
|
// Create Locales directory with simplified language code
|
||||||
dirname := path.Join("/tmp", "en", "LC_MESSAGES")
|
dirname := filepath.Join("/tmp", "en", "LC_MESSAGES")
|
||||||
err := os.MkdirAll(dirname, os.ModePerm)
|
err := os.MkdirAll(dirname, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Can't create test directory: %s", err.Error())
|
t.Fatalf("Can't create test directory: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write PO content to file
|
// Write PO content to file
|
||||||
filename := path.Join(dirname, "my_domain.po")
|
filename := filepath.Join(dirname, "my_domain.po")
|
||||||
|
|
||||||
f, err := os.Create(filename)
|
f, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -83,35 +216,33 @@ msgstr "More translation"
|
|||||||
l := NewLocale("/tmp", "en_US")
|
l := NewLocale("/tmp", "en_US")
|
||||||
|
|
||||||
// Force nil domain storage
|
// Force nil domain storage
|
||||||
l.domains = nil
|
l.Domains = nil
|
||||||
|
|
||||||
// Add domain
|
// Add domain
|
||||||
l.AddDomain("my_domain")
|
l.AddDomain("my_domain")
|
||||||
|
|
||||||
// Test translations
|
// Test non-existent "default" domain responses
|
||||||
tr := l.GetD("my_domain", "My text")
|
tr := l.GetDomain()
|
||||||
if tr != "Translated text" {
|
if tr != "my_domain" {
|
||||||
t.Errorf("Expected 'Translated text' but got '%s'", tr)
|
t.Errorf("Expected 'my_domain' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
v := "Variable"
|
// Set default domain to make it fail
|
||||||
tr = l.GetD("my_domain", "One with var: %s", v)
|
l.SetDomain("default")
|
||||||
if tr != "This one is the singular: Variable" {
|
|
||||||
t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr)
|
// Test non-existent "default" domain responses
|
||||||
|
tr = l.GetDomain()
|
||||||
|
if tr != "default" {
|
||||||
|
t.Errorf("Expected 'default' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test plural
|
// Test non-existent "default" domain responses
|
||||||
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")
|
tr = l.Get("My text")
|
||||||
if tr != "My text" {
|
if tr != "My text" {
|
||||||
t.Errorf("Expected 'My text' but got '%s'", tr)
|
t.Errorf("Expected 'My text' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v := "Variable"
|
||||||
tr = l.GetN("One with var: %s", "Several with vars: %s", 2, v)
|
tr = l.GetN("One with var: %s", "Several with vars: %s", 2, v)
|
||||||
if tr != "Several with vars: Variable" {
|
if tr != "Several with vars: Variable" {
|
||||||
t.Errorf("Expected 'Several with vars: Variable' but got '%s'", tr)
|
t.Errorf("Expected 'Several with vars: Variable' but got '%s'", tr)
|
||||||
@@ -124,6 +255,11 @@ msgstr "More translation"
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr = l.GetN("This is a test", "This are tests", 1)
|
tr = l.GetN("This is a test", "This are tests", 1)
|
||||||
|
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", 7)
|
||||||
if tr != "This are tests" {
|
if tr != "This are tests" {
|
||||||
t.Errorf("Expected 'This are tests' but got '%s'", tr)
|
t.Errorf("Expected 'This are tests' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
@@ -135,27 +271,138 @@ msgstr "More translation"
|
|||||||
}
|
}
|
||||||
|
|
||||||
tr = l.GetN("This one has invalid syntax translations", "This are tests", 1)
|
tr = l.GetN("This one has invalid syntax translations", "This are tests", 1)
|
||||||
|
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", 2)
|
||||||
if tr != "This are tests" {
|
if tr != "This are tests" {
|
||||||
t.Errorf("Expected 'Plural index' but got '%s'", tr)
|
t.Errorf("Expected 'This are tests' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test context translations
|
// Create Locale with full language code
|
||||||
v = "Test"
|
l = NewLocale("/tmp", "golem")
|
||||||
tr = l.GetDC("my_domain", "One with var: %s", "Ctx", v)
|
|
||||||
if tr != "This one is the singular in a Ctx context: Test" {
|
// Force nil domain storage
|
||||||
t.Errorf("Expected 'This one is the singular in a Ctx context: Test' but got '%s'", tr)
|
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 plural
|
// Test syntax error parsed translations
|
||||||
tr = l.GetNDC("my_domain", "One with var: %s", "Several with vars: %s", 3, "Ctx", v)
|
tr = l.Get("This one has invalid syntax translations")
|
||||||
if tr != "This one is the plural in a Ctx context: Test" {
|
if tr != "This one has invalid syntax translations" {
|
||||||
t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr)
|
t.Errorf("Expected 'This one has invalid syntax translations' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test last translation
|
tr = l.GetN("This one has invalid syntax translations", "This are tests", 1)
|
||||||
tr = l.GetD("my_domain", "More")
|
if tr != "This one has invalid syntax translations" {
|
||||||
if tr != "More translation" {
|
t.Errorf("Expected 'This one has invalid syntax translations' but got '%s'", tr)
|
||||||
t.Errorf("Expected 'More translation' but got '%s'", tr)
|
}
|
||||||
|
tr = l.GetN("This one has invalid syntax translations", "This are tests", 111)
|
||||||
|
if tr != "This are tests" {
|
||||||
|
t.Errorf("Expected 'This are tests' 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 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", 21)
|
||||||
|
if tr != "This are tests" {
|
||||||
|
t.Errorf("Expected 'This are tests' 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 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", 2)
|
||||||
|
if tr != "This are tests" {
|
||||||
|
t.Errorf("Expected 'This are tests' 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 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", 14)
|
||||||
|
if tr != "This are tests" {
|
||||||
|
t.Errorf("Expected 'This are tests' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,17 +422,17 @@ msgstr[0] "This one is the singular: %s"
|
|||||||
msgstr[1] "This one is the plural: %s"
|
msgstr[1] "This one is the plural: %s"
|
||||||
msgstr[2] "And this is the second plural form: %s"
|
msgstr[2] "And this is the second plural form: %s"
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create Locales directory with simplified language code
|
// Create Locales directory with simplified language code
|
||||||
dirname := path.Join("/tmp", "es")
|
dirname := filepath.Join("/tmp", "es")
|
||||||
err := os.MkdirAll(dirname, os.ModePerm)
|
err := os.MkdirAll(dirname, os.ModePerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Can't create test directory: %s", err.Error())
|
t.Fatalf("Can't create test directory: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write PO content to file
|
// Write PO content to file
|
||||||
filename := path.Join(dirname, "race.po")
|
filename := filepath.Join(dirname, "race.po")
|
||||||
|
|
||||||
f, err := os.Create(filename)
|
f, err := os.Create(filename)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -198,7 +445,7 @@ msgstr[2] "And this is the second plural form: %s"
|
|||||||
t.Fatalf("Can't write to test file: %s", err.Error())
|
t.Fatalf("Can't write to test file: %s", err.Error())
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create Locale with full language code
|
// Create Locale
|
||||||
l := NewLocale("/tmp", "es")
|
l := NewLocale("/tmp", "es")
|
||||||
|
|
||||||
// Init sync channels
|
// Init sync channels
|
||||||
@@ -224,3 +471,133 @@ msgstr[2] "And this is the second plural form: %s"
|
|||||||
<-ac
|
<-ac
|
||||||
<-rc
|
<-rc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAddTranslator(t *testing.T) {
|
||||||
|
// Create po object
|
||||||
|
po := new(Po)
|
||||||
|
|
||||||
|
// Parse file
|
||||||
|
po.ParseFile("fixtures/en_US/default.po")
|
||||||
|
|
||||||
|
// Create Locale
|
||||||
|
l := NewLocale("", "en")
|
||||||
|
|
||||||
|
// Add PO Translator to Locale object
|
||||||
|
l.AddTranslator("default", po)
|
||||||
|
|
||||||
|
// Test translations
|
||||||
|
tr := l.Get("My text")
|
||||||
|
if tr != "Translated text" {
|
||||||
|
t.Errorf("Expected 'Translated text' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
// Test translations
|
||||||
|
tr = l.Get("language")
|
||||||
|
if tr != "en_US" {
|
||||||
|
t.Errorf("Expected 'en_US' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArabicTranslation(t *testing.T) {
|
||||||
|
// Create Locale
|
||||||
|
l := NewLocale("fixtures/", "ar")
|
||||||
|
|
||||||
|
// Add domain
|
||||||
|
l.AddDomain("categories")
|
||||||
|
|
||||||
|
// Plurals formula missing + Plural translation string missing
|
||||||
|
tr := l.GetD("categories", "Alcohol & Tobacco")
|
||||||
|
if tr != "الكحول والتبغ" {
|
||||||
|
t.Errorf("Expected to get 'الكحول والتبغ', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Plural translation string present without translations, should get the msgid_plural
|
||||||
|
tr = l.GetND("categories", "%d selected", "%d selected", 10)
|
||||||
|
if tr != "%d selected" {
|
||||||
|
t.Errorf("Expected to get '%%d selected', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Plurals formula present + Plural translation string present and complete
|
||||||
|
tr = l.GetND("categories", "Load %d more document", "Load %d more documents", 0)
|
||||||
|
if tr != "حمّل %d مستندات إضافيّة" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[0]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = l.GetND("categories", "Load %d more document", "Load %d more documents", 1)
|
||||||
|
if tr != "حمّل مستند واحد إضافي" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[1]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = l.GetND("categories", "Load %d more document", "Load %d more documents", 2)
|
||||||
|
if tr != "حمّل مستندين إضافيين" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[2]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = l.GetND("categories", "Load %d more document", "Load %d more documents", 6)
|
||||||
|
if tr != "حمّل %d مستندات إضافيّة" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[3]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = l.GetND("categories", "Load %d more document", "Load %d more documents", 116)
|
||||||
|
if tr != "حمّل %d مستندا إضافيّا" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[4]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = l.GetND("categories", "Load %d more document", "Load %d more documents", 102)
|
||||||
|
if tr != "حمّل %d مستند إضافي" {
|
||||||
|
t.Errorf("Expected to get 'msgstr[5]', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArabicMissingPluralForm(t *testing.T) {
|
||||||
|
// Create Locale
|
||||||
|
l := NewLocale("fixtures/", "ar")
|
||||||
|
|
||||||
|
// Add domain
|
||||||
|
l.AddDomain("no_plural_header")
|
||||||
|
|
||||||
|
// Get translation
|
||||||
|
tr := l.GetD("no_plural_header", "Alcohol & Tobacco")
|
||||||
|
if tr != "الكحول والتبغ" {
|
||||||
|
t.Errorf("Expected to get 'الكحول والتبغ', but got '%s'", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLocaleBinaryEncoding(t *testing.T) {
|
||||||
|
// Create Locale
|
||||||
|
l := NewLocale("fixtures/", "en_US")
|
||||||
|
l.AddDomain("default")
|
||||||
|
|
||||||
|
buff, err := l.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
l2 := new(Locale)
|
||||||
|
err = l2.UnmarshalBinary(buff)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check object properties
|
||||||
|
if l.path != l2.path {
|
||||||
|
t.Fatalf("path doesn't match: '%s' vs '%s'", l.path, l2.path)
|
||||||
|
}
|
||||||
|
if l.lang != l2.lang {
|
||||||
|
t.Fatalf("lang doesn't match: '%s' vs '%s'", l.lang, l2.lang)
|
||||||
|
}
|
||||||
|
if l.defaultDomain != l2.defaultDomain {
|
||||||
|
t.Fatalf("defaultDomain doesn't match: '%s' vs '%s'", l.defaultDomain, l2.defaultDomain)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check translations
|
||||||
|
if l.Get("My text") != l2.Get("My text") {
|
||||||
|
t.Errorf("'%s' is different from '%s", l.Get("My text"), l2.Get("My text"))
|
||||||
|
}
|
||||||
|
if l.Get("More") != l2.Get("More") {
|
||||||
|
t.Errorf("'%s' is different from '%s", l.Get("More"), l2.Get("More"))
|
||||||
|
}
|
||||||
|
if l.GetN("One with var: %s", "Several with vars: %s", 3, "VALUE") != l2.GetN("One with var: %s", "Several with vars: %s", 3, "VALUE") {
|
||||||
|
t.Errorf("'%s' is different from '%s", l.GetN("One with var: %s", "Several with vars: %s", 3, "VALUE"), l2.GetN("One with var: %s", "Several with vars: %s", 3, "VALUE"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
556
mo.go
Normal file
556
mo.go
Normal file
@@ -0,0 +1,556 @@
|
|||||||
|
/*
|
||||||
|
* 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"
|
||||||
|
"encoding/gob"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/textproto"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext/plurals"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// MoMagicLittleEndian encoding
|
||||||
|
MoMagicLittleEndian = 0x950412de
|
||||||
|
// MoMagicBigEndian encoding
|
||||||
|
MoMagicBigEndian = 0xde120495
|
||||||
|
|
||||||
|
// EotSeparator msgctxt and msgid separator
|
||||||
|
EotSeparator = "\x04"
|
||||||
|
// NulSeparator msgid and msgstr separator
|
||||||
|
NulSeparator = "\x00"
|
||||||
|
)
|
||||||
|
|
||||||
|
/*
|
||||||
|
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"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/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
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewMoTranslator creates a new Mo object with the Translator interface
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (mo *Mo) GetE(str string, vars ...interface{}) (string, bool) {
|
||||||
|
// Sync read
|
||||||
|
mo.RLock()
|
||||||
|
defer mo.RUnlock()
|
||||||
|
|
||||||
|
if mo.translations != nil {
|
||||||
|
if _, ok := mo.translations[str]; ok {
|
||||||
|
if fmt, ok := mo.translations[str].GetE(); ok {
|
||||||
|
return Printf(fmt, vars...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (mo *Mo) GetNE(str, plural string, n int, vars ...interface{}) (string, bool) {
|
||||||
|
// Sync read
|
||||||
|
mo.RLock()
|
||||||
|
defer mo.RUnlock()
|
||||||
|
|
||||||
|
if mo.translations != nil {
|
||||||
|
if _, ok := mo.translations[str]; ok {
|
||||||
|
if fmt, ok := mo.translations[str].GetNE(mo.pluralForm(n)); ok {
|
||||||
|
return Printf(fmt, vars...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (mo *Mo) GetCE(str, ctx string, vars ...interface{}) (string, bool) {
|
||||||
|
// 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 {
|
||||||
|
if fmt, ok := mo.contexts[ctx][str].GetE(); ok {
|
||||||
|
return Printf(fmt, vars...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNCE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (mo *Mo) GetNCE(str, plural string, n int, ctx string, vars ...interface{}) (string, bool) {
|
||||||
|
// 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 {
|
||||||
|
if fmt, ok := mo.contexts[ctx][str].GetNE(mo.pluralForm(n)); ok {
|
||||||
|
return Printf(fmt, vars...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary implements encoding.BinaryMarshaler interface
|
||||||
|
func (mo *Mo) MarshalBinary() ([]byte, error) {
|
||||||
|
obj := new(TranslatorEncoding)
|
||||||
|
obj.Headers = mo.Headers
|
||||||
|
obj.Language = mo.Language
|
||||||
|
obj.PluralForms = mo.PluralForms
|
||||||
|
obj.Nplurals = mo.nplurals
|
||||||
|
obj.Plural = mo.plural
|
||||||
|
obj.Translations = mo.translations
|
||||||
|
obj.Contexts = mo.contexts
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
encoder := gob.NewEncoder(&buff)
|
||||||
|
err := encoder.Encode(obj)
|
||||||
|
|
||||||
|
return buff.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements encoding.BinaryUnmarshaler interface
|
||||||
|
func (mo *Mo) UnmarshalBinary(data []byte) error {
|
||||||
|
buff := bytes.NewBuffer(data)
|
||||||
|
obj := new(TranslatorEncoding)
|
||||||
|
|
||||||
|
decoder := gob.NewDecoder(buff)
|
||||||
|
err := decoder.Decode(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mo.Headers = obj.Headers
|
||||||
|
mo.Language = obj.Language
|
||||||
|
mo.PluralForms = obj.PluralForms
|
||||||
|
mo.nplurals = obj.Nplurals
|
||||||
|
mo.plural = obj.Plural
|
||||||
|
mo.translations = obj.Translations
|
||||||
|
mo.contexts = obj.Contexts
|
||||||
|
|
||||||
|
if expr, err := plurals.Compile(mo.plural); err == nil {
|
||||||
|
mo.pluralforms = expr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
234
mo_test.go
Normal file
234
mo_test.go
Normal file
@@ -0,0 +1,234 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMoBinaryEncoding(t *testing.T) {
|
||||||
|
// Create mo objects
|
||||||
|
mo := new(Mo)
|
||||||
|
mo2 := new(Mo)
|
||||||
|
|
||||||
|
// Parse file
|
||||||
|
mo.ParseFile("fixtures/en_US/default.mo")
|
||||||
|
|
||||||
|
buff, err := mo.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = mo2.UnmarshalBinary(buff)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test translations
|
||||||
|
tr := mo2.Get("My text")
|
||||||
|
if tr != "Translated text" {
|
||||||
|
t.Errorf("Expected 'Translated text' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
// Test translations
|
||||||
|
tr = mo2.Get("language")
|
||||||
|
if tr != "en_US" {
|
||||||
|
t.Errorf("Expected 'en_US' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
429
plurals/compiler.go
Normal file
429
plurals/compiler.go
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
/*
|
||||||
|
* 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 plurals is the pluralform compiler to get the correct translation id of the plural string
|
||||||
|
*/
|
||||||
|
package plurals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type match struct {
|
||||||
|
openPos int
|
||||||
|
closePos int
|
||||||
|
}
|
||||||
|
|
||||||
|
var pat = regexp.MustCompile(`(\?|:|\|\||&&|==|!=|>=|>|<=|<|%|\d+|n)`)
|
||||||
|
|
||||||
|
type testToken interface {
|
||||||
|
compile(tokens []string) (test test, err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type cmpTestBuilder func(val uint32, flipped bool) test
|
||||||
|
type logicTestBuild func(left test, right test) test
|
||||||
|
|
||||||
|
var ternaryToken ternaryStruct
|
||||||
|
|
||||||
|
type ternaryStruct struct{}
|
||||||
|
|
||||||
|
func (ternaryStruct) compile(tokens []string) (expr Expression, err error) {
|
||||||
|
main, err := splitTokens(tokens, "?")
|
||||||
|
if err != nil {
|
||||||
|
return expr, err
|
||||||
|
}
|
||||||
|
test, err := compileTest(strings.Join(main.Left, ""))
|
||||||
|
if err != nil {
|
||||||
|
return expr, err
|
||||||
|
}
|
||||||
|
actions, err := splitTokens(main.Right, ":")
|
||||||
|
if err != nil {
|
||||||
|
return expr, err
|
||||||
|
}
|
||||||
|
trueAction, err := compileExpression(strings.Join(actions.Left, ""))
|
||||||
|
if err != nil {
|
||||||
|
return expr, err
|
||||||
|
}
|
||||||
|
falseAction, err := compileExpression(strings.Join(actions.Right, ""))
|
||||||
|
if err != nil {
|
||||||
|
return expr, nil
|
||||||
|
}
|
||||||
|
return ternary{
|
||||||
|
test: test,
|
||||||
|
trueExpr: trueAction,
|
||||||
|
falseExpr: falseAction,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var constToken constValStruct
|
||||||
|
|
||||||
|
type constValStruct struct{}
|
||||||
|
|
||||||
|
func (constValStruct) compile(tokens []string) (expr Expression, err error) {
|
||||||
|
if len(tokens) == 0 {
|
||||||
|
return expr, errors.New("got nothing instead of constant")
|
||||||
|
}
|
||||||
|
if len(tokens) != 1 {
|
||||||
|
return expr, fmt.Errorf("invalid constant: %s", strings.Join(tokens, ""))
|
||||||
|
}
|
||||||
|
i, err := strconv.Atoi(tokens[0])
|
||||||
|
if err != nil {
|
||||||
|
return expr, err
|
||||||
|
}
|
||||||
|
return constValue{value: i}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileLogicTest(tokens []string, sep string, builder logicTestBuild) (test test, err error) {
|
||||||
|
split, err := splitTokens(tokens, sep)
|
||||||
|
if err != nil {
|
||||||
|
return test, err
|
||||||
|
}
|
||||||
|
left, err := compileTest(strings.Join(split.Left, ""))
|
||||||
|
if err != nil {
|
||||||
|
return test, err
|
||||||
|
}
|
||||||
|
right, err := compileTest(strings.Join(split.Right, ""))
|
||||||
|
if err != nil {
|
||||||
|
return test, err
|
||||||
|
}
|
||||||
|
return builder(left, right), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var orToken orStruct
|
||||||
|
|
||||||
|
type orStruct struct{}
|
||||||
|
|
||||||
|
func (orStruct) compile(tokens []string) (test test, err error) {
|
||||||
|
return compileLogicTest(tokens, "||", buildOr)
|
||||||
|
}
|
||||||
|
func buildOr(left test, right test) test {
|
||||||
|
return or{left: left, right: right}
|
||||||
|
}
|
||||||
|
|
||||||
|
var andToken andStruct
|
||||||
|
|
||||||
|
type andStruct struct{}
|
||||||
|
|
||||||
|
func (andStruct) compile(tokens []string) (test test, err error) {
|
||||||
|
return compileLogicTest(tokens, "&&", buildAnd)
|
||||||
|
}
|
||||||
|
func buildAnd(left test, right test) test {
|
||||||
|
return and{left: left, right: right}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileMod(tokens []string) (math math, err error) {
|
||||||
|
split, err := splitTokens(tokens, "%")
|
||||||
|
if err != nil {
|
||||||
|
return math, err
|
||||||
|
}
|
||||||
|
if len(split.Left) != 1 || split.Left[0] != "n" {
|
||||||
|
return math, errors.New("Modulus operation requires 'n' as left operand")
|
||||||
|
}
|
||||||
|
if len(split.Right) != 1 {
|
||||||
|
return math, errors.New("Modulus operation requires simple integer as right operand")
|
||||||
|
}
|
||||||
|
i, err := parseUint32(split.Right[0])
|
||||||
|
if err != nil {
|
||||||
|
return math, err
|
||||||
|
}
|
||||||
|
return mod{value: uint32(i)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func subPipe(modTokens []string, actionTokens []string, builder cmpTestBuilder, flipped bool) (test test, err error) {
|
||||||
|
modifier, err := compileMod(modTokens)
|
||||||
|
if err != nil {
|
||||||
|
return test, err
|
||||||
|
}
|
||||||
|
if len(actionTokens) != 1 {
|
||||||
|
return test, errors.New("can only get modulus of integer")
|
||||||
|
}
|
||||||
|
i, err := parseUint32(actionTokens[0])
|
||||||
|
if err != nil {
|
||||||
|
return test, err
|
||||||
|
}
|
||||||
|
action := builder(uint32(i), flipped)
|
||||||
|
return pipe{
|
||||||
|
modifier: modifier,
|
||||||
|
action: action,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileEquality(tokens []string, sep string, builder cmpTestBuilder) (test test, err error) {
|
||||||
|
split, err := splitTokens(tokens, sep)
|
||||||
|
if err != nil {
|
||||||
|
return test, err
|
||||||
|
}
|
||||||
|
if len(split.Left) == 1 && split.Left[0] == "n" {
|
||||||
|
if len(split.Right) != 1 {
|
||||||
|
return test, errors.New("test can only compare n to integers")
|
||||||
|
}
|
||||||
|
i, err := parseUint32(split.Right[0])
|
||||||
|
if err != nil {
|
||||||
|
return test, err
|
||||||
|
}
|
||||||
|
return builder(i, false), nil
|
||||||
|
} else if len(split.Right) == 1 && split.Right[0] == "n" {
|
||||||
|
if len(split.Left) != 1 {
|
||||||
|
return test, errors.New("test can only compare n to integers")
|
||||||
|
}
|
||||||
|
i, err := parseUint32(split.Left[0])
|
||||||
|
if err != nil {
|
||||||
|
return test, err
|
||||||
|
}
|
||||||
|
return builder(i, true), nil
|
||||||
|
} else if contains(split.Left, "n") && contains(split.Left, "%") {
|
||||||
|
return subPipe(split.Left, split.Right, builder, false)
|
||||||
|
}
|
||||||
|
return test, errors.New("equality test must have 'n' as one of the two tests")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var eqToken eqStruct
|
||||||
|
|
||||||
|
type eqStruct struct{}
|
||||||
|
|
||||||
|
func (eqStruct) compile(tokens []string) (test test, err error) {
|
||||||
|
return compileEquality(tokens, "==", buildEq)
|
||||||
|
}
|
||||||
|
func buildEq(val uint32, flipped bool) test {
|
||||||
|
return equal{value: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
var neqToken neqStruct
|
||||||
|
|
||||||
|
type neqStruct struct{}
|
||||||
|
|
||||||
|
func (neqStruct) compile(tokens []string) (test test, err error) {
|
||||||
|
return compileEquality(tokens, "!=", buildNeq)
|
||||||
|
}
|
||||||
|
func buildNeq(val uint32, flipped bool) test {
|
||||||
|
return notequal{value: val}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gtToken gtStruct
|
||||||
|
|
||||||
|
type gtStruct struct{}
|
||||||
|
|
||||||
|
func (gtStruct) compile(tokens []string) (test test, err error) {
|
||||||
|
return compileEquality(tokens, ">", buildGt)
|
||||||
|
}
|
||||||
|
func buildGt(val uint32, flipped bool) test {
|
||||||
|
return gt{value: val, flipped: flipped}
|
||||||
|
}
|
||||||
|
|
||||||
|
var gteToken gteStruct
|
||||||
|
|
||||||
|
type gteStruct struct{}
|
||||||
|
|
||||||
|
func (gteStruct) compile(tokens []string) (test test, err error) {
|
||||||
|
return compileEquality(tokens, ">=", buildGte)
|
||||||
|
}
|
||||||
|
func buildGte(val uint32, flipped bool) test {
|
||||||
|
return gte{value: val, flipped: flipped}
|
||||||
|
}
|
||||||
|
|
||||||
|
var ltToken ltStruct
|
||||||
|
|
||||||
|
type ltStruct struct{}
|
||||||
|
|
||||||
|
func (ltStruct) compile(tokens []string) (test test, err error) {
|
||||||
|
return compileEquality(tokens, "<", buildLt)
|
||||||
|
}
|
||||||
|
func buildLt(val uint32, flipped bool) test {
|
||||||
|
return lt{value: val, flipped: flipped}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lteToken lteStruct
|
||||||
|
|
||||||
|
type lteStruct struct{}
|
||||||
|
|
||||||
|
func (lteStruct) compile(tokens []string) (test test, err error) {
|
||||||
|
return compileEquality(tokens, "<=", buildLte)
|
||||||
|
}
|
||||||
|
func buildLte(val uint32, flipped bool) test {
|
||||||
|
return lte{value: val, flipped: flipped}
|
||||||
|
}
|
||||||
|
|
||||||
|
type testTokenDef struct {
|
||||||
|
op string
|
||||||
|
token testToken
|
||||||
|
}
|
||||||
|
|
||||||
|
var precedence = []testTokenDef{
|
||||||
|
{op: "||", token: orToken},
|
||||||
|
{op: "&&", token: andToken},
|
||||||
|
{op: "==", token: eqToken},
|
||||||
|
{op: "!=", token: neqToken},
|
||||||
|
{op: ">=", token: gteToken},
|
||||||
|
{op: ">", token: gtToken},
|
||||||
|
{op: "<=", token: lteToken},
|
||||||
|
{op: "<", token: ltToken},
|
||||||
|
}
|
||||||
|
|
||||||
|
type splitted struct {
|
||||||
|
Left []string
|
||||||
|
Right []string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find index of token in list of tokens
|
||||||
|
func index(tokens []string, sep string) int {
|
||||||
|
for index, token := range tokens {
|
||||||
|
if token == sep {
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split a list of tokens by a token into a splitted struct holding the tokens
|
||||||
|
// before and after the token to be split by.
|
||||||
|
func splitTokens(tokens []string, sep string) (s splitted, err error) {
|
||||||
|
index := index(tokens, sep)
|
||||||
|
if index == -1 {
|
||||||
|
return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','"))
|
||||||
|
}
|
||||||
|
return splitted{
|
||||||
|
Left: tokens[:index],
|
||||||
|
Right: tokens[index+1:],
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scan a string for parenthesis
|
||||||
|
func scan(s string) <-chan match {
|
||||||
|
ch := make(chan match)
|
||||||
|
go func() {
|
||||||
|
depth := 0
|
||||||
|
opener := 0
|
||||||
|
for index, char := range s {
|
||||||
|
switch char {
|
||||||
|
case '(':
|
||||||
|
if depth == 0 {
|
||||||
|
opener = index
|
||||||
|
}
|
||||||
|
depth++
|
||||||
|
case ')':
|
||||||
|
depth--
|
||||||
|
if depth == 0 {
|
||||||
|
ch <- match{
|
||||||
|
openPos: opener,
|
||||||
|
closePos: index + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Split the string into tokens
|
||||||
|
func split(s string) <-chan string {
|
||||||
|
ch := make(chan string)
|
||||||
|
go func() {
|
||||||
|
s = strings.Replace(s, " ", "", -1)
|
||||||
|
if !strings.Contains(s, "(") {
|
||||||
|
ch <- s
|
||||||
|
} else {
|
||||||
|
last := 0
|
||||||
|
end := len(s)
|
||||||
|
for info := range scan(s) {
|
||||||
|
if last != info.openPos {
|
||||||
|
ch <- s[last:info.openPos]
|
||||||
|
}
|
||||||
|
ch <- s[info.openPos:info.closePos]
|
||||||
|
last = info.closePos
|
||||||
|
}
|
||||||
|
if last != end {
|
||||||
|
ch <- s[last:]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tokenizes a string into a list of strings, tokens grouped by parenthesis are
|
||||||
|
// not split! If the string starts with ( and ends in ), those are stripped.
|
||||||
|
func tokenize(s string) []string {
|
||||||
|
/*
|
||||||
|
TODO: Properly detect if the string starts with a ( and ends with a )
|
||||||
|
and that those two form a matching pair.
|
||||||
|
|
||||||
|
Eg: (foo) -> true; (foo)(bar) -> false;
|
||||||
|
*/
|
||||||
|
if s[0] == '(' && s[len(s)-1] == ')' {
|
||||||
|
s = s[1 : len(s)-1]
|
||||||
|
}
|
||||||
|
ret := []string{}
|
||||||
|
for chunk := range split(s) {
|
||||||
|
if len(chunk) != 0 {
|
||||||
|
if chunk[0] == '(' && chunk[len(chunk)-1] == ')' {
|
||||||
|
ret = append(ret, chunk)
|
||||||
|
} else {
|
||||||
|
for _, token := range pat.FindAllStringSubmatch(chunk, -1) {
|
||||||
|
ret = append(ret, token[0])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fmt.Printf("Empty chunk in string '%s'\n", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ret
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile a string containing a plural form expression to a Expression object.
|
||||||
|
func Compile(s string) (expr Expression, err error) {
|
||||||
|
if s == "0" {
|
||||||
|
return constValue{value: 0}, nil
|
||||||
|
}
|
||||||
|
if !strings.Contains(s, "?") {
|
||||||
|
s += "?1:0"
|
||||||
|
}
|
||||||
|
return compileExpression(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a token is in a slice of strings
|
||||||
|
func contains(haystack []string, needle string) bool {
|
||||||
|
for _, s := range haystack {
|
||||||
|
if s == needle {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compiles an expression (ternary or constant)
|
||||||
|
func compileExpression(s string) (expr Expression, err error) {
|
||||||
|
tokens := tokenize(s)
|
||||||
|
if contains(tokens, "?") {
|
||||||
|
return ternaryToken.compile(tokens)
|
||||||
|
}
|
||||||
|
return constToken.compile(tokens)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compiles a test (comparison)
|
||||||
|
func compileTest(s string) (test test, err error) {
|
||||||
|
tokens := tokenize(s)
|
||||||
|
for _, tokenDef := range precedence {
|
||||||
|
if contains(tokens, tokenDef.op) {
|
||||||
|
return tokenDef.token.compile(tokens)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return test, errors.New("cannot compile")
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUint32(s string) (ui uint32, err error) {
|
||||||
|
i, err := strconv.ParseUint(s, 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return ui, err
|
||||||
|
}
|
||||||
|
return uint32(i), nil
|
||||||
|
}
|
||||||
50
plurals/compiler_test.go
Normal file
50
plurals/compiler_test.go
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/*
|
||||||
|
* 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 plurals
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fixture struct {
|
||||||
|
PluralForm string
|
||||||
|
Fixture []int
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCompiler(t *testing.T) {
|
||||||
|
f, err := os.Open("testdata/pluralforms.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
dec := json.NewDecoder(f)
|
||||||
|
var fixtures []fixture
|
||||||
|
err = dec.Decode(&fixtures)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
for _, data := range fixtures {
|
||||||
|
expr, err := Compile(data.PluralForm)
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("'%s' triggered error: %s", data.PluralForm, err)
|
||||||
|
} else if expr == nil {
|
||||||
|
t.Logf("'%s' compiled to nil", data.PluralForm)
|
||||||
|
t.Fail()
|
||||||
|
} else {
|
||||||
|
for n, e := range data.Fixture {
|
||||||
|
i := expr.Eval(uint32(n))
|
||||||
|
if i != e {
|
||||||
|
t.Logf("'%s' with n = %d, expected %d, got %d, compiled to %s", data.PluralForm, n, e, i, expr)
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if i == -1 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
43
plurals/expression.go
Normal file
43
plurals/expression.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
/*
|
||||||
|
* 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 plurals
|
||||||
|
|
||||||
|
// Expression is a plurals expression. Eval evaluates the expression for
|
||||||
|
// a given n value. Use plurals.Compile to generate Expression instances.
|
||||||
|
type Expression interface {
|
||||||
|
Eval(n uint32) int
|
||||||
|
}
|
||||||
|
|
||||||
|
type constValue struct {
|
||||||
|
value int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c constValue) Eval(n uint32) int {
|
||||||
|
return c.value
|
||||||
|
}
|
||||||
|
|
||||||
|
type test interface {
|
||||||
|
test(n uint32) bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type ternary struct {
|
||||||
|
test test
|
||||||
|
trueExpr Expression
|
||||||
|
falseExpr Expression
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t ternary) Eval(n uint32) int {
|
||||||
|
if t.test.test(n) {
|
||||||
|
if t.trueExpr == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return t.trueExpr.Eval(n)
|
||||||
|
}
|
||||||
|
if t.falseExpr == nil {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
return t.falseExpr.Eval(n)
|
||||||
|
}
|
||||||
18
plurals/math.go
Normal file
18
plurals/math.go
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
/*
|
||||||
|
* 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 plurals
|
||||||
|
|
||||||
|
type math interface {
|
||||||
|
calc(n uint32) uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type mod struct {
|
||||||
|
value uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m mod) calc(n uint32) uint32 {
|
||||||
|
return n % m.value
|
||||||
|
}
|
||||||
1
plurals/testdata/pluralforms.json
vendored
Normal file
1
plurals/testdata/pluralforms.json
vendored
Normal file
File diff suppressed because one or more lines are too long
104
plurals/tests.go
Normal file
104
plurals/tests.go
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
/*
|
||||||
|
* 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 plurals
|
||||||
|
|
||||||
|
type equal struct {
|
||||||
|
value uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e equal) test(n uint32) bool {
|
||||||
|
return n == e.value
|
||||||
|
}
|
||||||
|
|
||||||
|
type notequal struct {
|
||||||
|
value uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e notequal) test(n uint32) bool {
|
||||||
|
return n != e.value
|
||||||
|
}
|
||||||
|
|
||||||
|
type gt struct {
|
||||||
|
value uint32
|
||||||
|
flipped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e gt) test(n uint32) bool {
|
||||||
|
if e.flipped {
|
||||||
|
return e.value > n
|
||||||
|
} else {
|
||||||
|
return n > e.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type lt struct {
|
||||||
|
value uint32
|
||||||
|
flipped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e lt) test(n uint32) bool {
|
||||||
|
if e.flipped {
|
||||||
|
return e.value < n
|
||||||
|
}
|
||||||
|
return n < e.value
|
||||||
|
}
|
||||||
|
|
||||||
|
type gte struct {
|
||||||
|
value uint32
|
||||||
|
flipped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e gte) test(n uint32) bool {
|
||||||
|
if e.flipped {
|
||||||
|
return e.value >= n
|
||||||
|
}
|
||||||
|
return n >= e.value
|
||||||
|
}
|
||||||
|
|
||||||
|
type lte struct {
|
||||||
|
value uint32
|
||||||
|
flipped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e lte) test(n uint32) bool {
|
||||||
|
if e.flipped {
|
||||||
|
return e.value <= n
|
||||||
|
}
|
||||||
|
return n <= e.value
|
||||||
|
}
|
||||||
|
|
||||||
|
type and struct {
|
||||||
|
left test
|
||||||
|
right test
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e and) test(n uint32) bool {
|
||||||
|
if !e.left.test(n) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return e.right.test(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
type or struct {
|
||||||
|
left test
|
||||||
|
right test
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e or) test(n uint32) bool {
|
||||||
|
if e.left.test(n) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return e.right.test(n)
|
||||||
|
}
|
||||||
|
|
||||||
|
type pipe struct {
|
||||||
|
modifier math
|
||||||
|
action test
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e pipe) test(n uint32) bool {
|
||||||
|
return e.action.test(e.modifier.calc(n))
|
||||||
|
}
|
||||||
589
po.go
589
po.go
@@ -1,74 +1,51 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
package gotext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
"fmt"
|
"bytes"
|
||||||
"github.com/leonelquinteros/anko/vm"
|
"encoding/gob"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/textproto"
|
"net/textproto"
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/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 {
|
|
||||||
return t.trs[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return unstranlated id by default
|
|
||||||
return t.id
|
|
||||||
}
|
|
||||||
|
|
||||||
func (t *translation) getN(n int) string {
|
|
||||||
// Look for translation index
|
|
||||||
if _, ok := t.trs[n]; ok {
|
|
||||||
return t.trs[n]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return unstranlated 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.
|
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.
|
And it's safe for concurrent use by multiple goroutines by using the sync package for locking.
|
||||||
|
|
||||||
Example:
|
Example:
|
||||||
|
|
||||||
import "github.com/leonelquinteros/gotext"
|
import (
|
||||||
|
"fmt"
|
||||||
|
"git.deineagentur.com/DeineAgenturUG/gotext"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// Create po object
|
// Create po object
|
||||||
po := new(gotext.Po)
|
po := gotext.NewPoTranslator()
|
||||||
|
|
||||||
// Parse .po file
|
// Parse .po file
|
||||||
po.ParseFile("/path/to/po/file/translations.po")
|
po.ParseFile("/path/to/po/file/translations.po")
|
||||||
|
|
||||||
// Get translation
|
// Get Translation
|
||||||
println(po.Get("Translate this"))
|
fmt.Println(po.Get("Translate this"))
|
||||||
}
|
}
|
||||||
|
|
||||||
*/
|
*/
|
||||||
type Po struct {
|
type Po struct {
|
||||||
// Headers
|
// Headers storage
|
||||||
RawHeaders string
|
Headers textproto.MIMEHeader
|
||||||
|
|
||||||
// Language header
|
// Language header
|
||||||
Language string
|
Language string
|
||||||
@@ -77,15 +54,35 @@ type Po struct {
|
|||||||
PluralForms string
|
PluralForms string
|
||||||
|
|
||||||
// Parsed Plural-Forms header values
|
// Parsed Plural-Forms header values
|
||||||
nplurals int
|
nplurals int
|
||||||
plural string
|
plural string
|
||||||
|
pluralforms plurals.Expression
|
||||||
|
|
||||||
// Storage
|
// Storage
|
||||||
translations map[string]*translation
|
translations map[string]*Translation
|
||||||
contexts map[string]map[string]*translation
|
contexts map[string]map[string]*Translation
|
||||||
|
|
||||||
// Sync Mutex
|
// Sync Mutex
|
||||||
sync.RWMutex
|
sync.RWMutex
|
||||||
|
|
||||||
|
// Parsing buffers
|
||||||
|
trBuffer *Translation
|
||||||
|
ctxBuffer string
|
||||||
|
}
|
||||||
|
|
||||||
|
type parseState int
|
||||||
|
|
||||||
|
const (
|
||||||
|
head parseState = iota
|
||||||
|
msgCtxt
|
||||||
|
msgID
|
||||||
|
msgIDPlural
|
||||||
|
msgStr
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPoTranslator creates a new Po object with the Translator interface
|
||||||
|
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.
|
// ParseFile tries to read the file by its provided path (f) and parse its content as a .po file.
|
||||||
@@ -107,188 +104,228 @@ func (po *Po) ParseFile(f string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
po.Parse(string(data))
|
po.Parse(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse loads the translations specified in the provided string (str)
|
// 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
|
// Lock while parsing
|
||||||
po.Lock()
|
po.Lock()
|
||||||
defer po.Unlock()
|
|
||||||
|
|
||||||
// Init storage
|
// Init storage
|
||||||
if po.translations == nil {
|
if po.translations == nil {
|
||||||
po.translations = make(map[string]*translation)
|
po.translations = make(map[string]*Translation)
|
||||||
po.contexts = make(map[string]map[string]*translation)
|
po.contexts = make(map[string]map[string]*Translation)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get lines
|
// Get lines
|
||||||
lines := strings.Split(str, "\n")
|
lines := strings.Split(string(buf), "\n")
|
||||||
|
|
||||||
// Translation buffer
|
// Init buffer
|
||||||
tr := newTranslation()
|
po.trBuffer = NewTranslation()
|
||||||
|
po.ctxBuffer = ""
|
||||||
// Context buffer
|
|
||||||
ctx := ""
|
|
||||||
|
|
||||||
|
state := head
|
||||||
for _, l := range lines {
|
for _, l := range lines {
|
||||||
// Trim spaces
|
// Trim spaces
|
||||||
l = strings.TrimSpace(l)
|
l = strings.TrimSpace(l)
|
||||||
|
|
||||||
// Skip empty lines
|
|
||||||
if l == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip invalid lines
|
// Skip invalid lines
|
||||||
if !strings.HasPrefix(l, "\"") && !strings.HasPrefix(l, "msgctxt") && !strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") && !strings.HasPrefix(l, "msgstr") {
|
if !po.isValidLine(l) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer context and continue
|
// Buffer context and continue
|
||||||
if strings.HasPrefix(l, "msgctxt") {
|
if strings.HasPrefix(l, "msgctxt") {
|
||||||
// Save current translation buffer.
|
po.parseContext(l)
|
||||||
// No context
|
state = msgCtxt
|
||||||
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
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Buffer msgid and continue
|
// Buffer msgid and continue
|
||||||
if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") {
|
if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") {
|
||||||
// Save current translation buffer if not inside a context.
|
po.parseID(l)
|
||||||
if ctx == "" {
|
state = msgID
|
||||||
po.translations[tr.id] = tr
|
|
||||||
|
|
||||||
// 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")))
|
|
||||||
|
|
||||||
// Loop
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for plural form
|
// Check for plural form
|
||||||
if strings.HasPrefix(l, "msgid_plural") {
|
if strings.HasPrefix(l, "msgid_plural") {
|
||||||
tr.pluralId, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural")))
|
po.parsePluralID(l)
|
||||||
|
state = msgIDPlural
|
||||||
// Loop
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save translation
|
// Save Translation
|
||||||
if strings.HasPrefix(l, "msgstr") {
|
if strings.HasPrefix(l, "msgstr") {
|
||||||
l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr"))
|
po.parseMessage(l)
|
||||||
|
state = msgStr
|
||||||
// Check for indexed translation forms
|
|
||||||
if strings.HasPrefix(l, "[") {
|
|
||||||
idx := strings.Index(l, "]")
|
|
||||||
if idx == -1 {
|
|
||||||
// Skip wrong index formatting
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse index
|
|
||||||
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[idx+1:]))
|
|
||||||
|
|
||||||
// Loop
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Save single translation form under 0 index
|
|
||||||
tr.trs[0], _ = strconv.Unquote(l)
|
|
||||||
|
|
||||||
// Loop
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Multi line strings and headers
|
// Multi line strings and headers
|
||||||
if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") {
|
if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") {
|
||||||
// Check for multiline from previously set msgid
|
po.parseString(l, state)
|
||||||
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
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save last translation buffer.
|
// Save last Translation buffer.
|
||||||
if tr.id != "" {
|
po.saveBuffer()
|
||||||
if ctx == "" {
|
|
||||||
po.translations[tr.id] = tr
|
// Unlock to parse headers
|
||||||
} else {
|
po.Unlock()
|
||||||
// Save context
|
|
||||||
if _, ok := po.contexts[ctx]; !ok {
|
// Parse headers
|
||||||
po.contexts[ctx] = make(map[string]*translation)
|
po.parseHeaders()
|
||||||
}
|
}
|
||||||
po.contexts[ctx][tr.id] = tr
|
|
||||||
|
// 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
|
||||||
|
} else {
|
||||||
|
// With context...
|
||||||
|
if _, ok := po.contexts[po.ctxBuffer]; !ok {
|
||||||
|
po.contexts[po.ctxBuffer] = make(map[string]*Translation)
|
||||||
|
}
|
||||||
|
po.contexts[po.ctxBuffer][po.trBuffer.ID] = po.trBuffer
|
||||||
|
|
||||||
|
// Cleanup current context buffer if needed
|
||||||
|
if po.trBuffer.ID != "" {
|
||||||
|
po.ctxBuffer = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse headers
|
// Flush Translation buffer
|
||||||
po.RawHeaders += "\n\n"
|
po.trBuffer = NewTranslation()
|
||||||
|
}
|
||||||
|
|
||||||
reader := bufio.NewReader(strings.NewReader(po.RawHeaders))
|
// parseContext takes a line starting with "msgctxt",
|
||||||
|
// saves the current Translation buffer and creates a new context.
|
||||||
|
func (po *Po) parseContext(l string) {
|
||||||
|
// Save current Translation buffer.
|
||||||
|
po.saveBuffer()
|
||||||
|
|
||||||
|
// Buffer context
|
||||||
|
po.ctxBuffer, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseID takes a line starting with "msgid",
|
||||||
|
// saves the current Translation and creates a new msgid buffer.
|
||||||
|
func (po *Po) parseID(l string) {
|
||||||
|
// Save current Translation buffer.
|
||||||
|
po.saveBuffer()
|
||||||
|
|
||||||
|
// Set id
|
||||||
|
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")))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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
|
||||||
|
if strings.HasPrefix(l, "[") {
|
||||||
|
idx := strings.Index(l, "]")
|
||||||
|
if idx == -1 {
|
||||||
|
// Skip wrong index formatting
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse index
|
||||||
|
i, err := strconv.Atoi(l[1:idx])
|
||||||
|
if err != nil {
|
||||||
|
// Skip wrong index formatting
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseString takes a well formatted string without prefix
|
||||||
|
// and creates headers or attach multi-line strings when corresponding
|
||||||
|
func (po *Po) parseString(l string, state parseState) {
|
||||||
|
clean, _ := strconv.Unquote(l)
|
||||||
|
|
||||||
|
switch state {
|
||||||
|
case msgStr:
|
||||||
|
// 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
|
||||||
|
|
||||||
|
case msgIDPlural:
|
||||||
|
// Multiline msgid - Append to current id
|
||||||
|
po.trBuffer.PluralID += clean
|
||||||
|
|
||||||
|
case msgCtxt:
|
||||||
|
// Multiline context - Append to current context
|
||||||
|
po.ctxBuffer += clean
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidLine checks for line prefixes to detect valid syntax.
|
||||||
|
func (po *Po) isValidLine(l string) bool {
|
||||||
|
// Check prefix
|
||||||
|
valid := []string{
|
||||||
|
"\"",
|
||||||
|
"msgctxt",
|
||||||
|
"msgid",
|
||||||
|
"msgid_plural",
|
||||||
|
"msgstr",
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, v := range valid {
|
||||||
|
if strings.HasPrefix(l, v) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseHeaders retrieves data from previously parsed headers
|
||||||
|
func (po *Po) parseHeaders() {
|
||||||
|
// Make sure we end with 2 carriage returns.
|
||||||
|
raw := po.Get("") + "\n\n"
|
||||||
|
|
||||||
|
// Read
|
||||||
|
reader := bufio.NewReader(strings.NewReader(raw))
|
||||||
tp := textproto.NewReader(reader)
|
tp := textproto.NewReader(reader)
|
||||||
|
|
||||||
mimeHeader, err := tp.ReadMIMEHeader()
|
var err error
|
||||||
|
|
||||||
|
// Sync Headers write.
|
||||||
|
po.Lock()
|
||||||
|
defer po.Unlock()
|
||||||
|
|
||||||
|
po.Headers, err = tp.ReadMIMEHeader()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get/save needed headers
|
// Get/save needed headers
|
||||||
po.Language = mimeHeader.Get("Language")
|
po.Language = po.Headers.Get("Language")
|
||||||
po.PluralForms = mimeHeader.Get("Plural-Forms")
|
po.PluralForms = po.Headers.Get("Plural-Forms")
|
||||||
|
|
||||||
// Parse Plural-Forms formula
|
// Parse Plural-Forms formula
|
||||||
if po.PluralForms == "" {
|
if po.PluralForms == "" {
|
||||||
@@ -311,6 +348,11 @@ func (po *Po) Parse(str string) {
|
|||||||
|
|
||||||
case "plural":
|
case "plural":
|
||||||
po.plural = vs[1]
|
po.plural = vs[1]
|
||||||
|
|
||||||
|
if expr, err := plurals.Compile(po.plural); err == nil {
|
||||||
|
po.pluralforms = expr
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -321,38 +363,18 @@ func (po *Po) pluralForm(n int) int {
|
|||||||
po.RLock()
|
po.RLock()
|
||||||
defer po.RUnlock()
|
defer po.RUnlock()
|
||||||
|
|
||||||
// Failsafe
|
// Failure fallback
|
||||||
if po.nplurals < 1 {
|
if po.pluralforms == nil {
|
||||||
return 0
|
/* Use Western plural rule. */
|
||||||
}
|
if n == 1 {
|
||||||
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
|
return 0
|
||||||
}
|
}
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
return po.pluralforms.Eval(uint32(n))
|
||||||
if int(plural.Int()) > po.nplurals {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
return int(plural.Int())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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.
|
// 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 {
|
func (po *Po) Get(str string, vars ...interface{}) string {
|
||||||
// Sync read
|
// Sync read
|
||||||
@@ -361,15 +383,15 @@ func (po *Po) Get(str string, vars ...interface{}) string {
|
|||||||
|
|
||||||
if po.translations != nil {
|
if po.translations != nil {
|
||||||
if _, ok := po.translations[str]; ok {
|
if _, ok := po.translations[str]; ok {
|
||||||
return fmt.Sprintf(po.translations[str].get(), vars...)
|
return Printf(po.translations[str].Get(), vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the same we received by default
|
// Return the same we received by default
|
||||||
return fmt.Sprintf(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.
|
// 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 {
|
func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
|
||||||
// Sync read
|
// Sync read
|
||||||
@@ -378,15 +400,18 @@ func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
|
|||||||
|
|
||||||
if po.translations != nil {
|
if po.translations != nil {
|
||||||
if _, ok := po.translations[str]; ok {
|
if _, ok := po.translations[str]; ok {
|
||||||
return fmt.Sprintf(po.translations[str].getN(po.pluralForm(n)), vars...)
|
return Printf(po.translations[str].GetN(po.pluralForm(n)), vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the plural string we received by default
|
// Parse plural forms to distinguish between plural and singular
|
||||||
return fmt.Sprintf(plural, vars...)
|
if po.pluralForm(n) == 0 {
|
||||||
|
return Printf(str, 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.
|
// 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 {
|
func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
|
||||||
// Sync read
|
// Sync read
|
||||||
@@ -397,17 +422,17 @@ func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
|
|||||||
if _, ok := po.contexts[ctx]; ok {
|
if _, ok := po.contexts[ctx]; ok {
|
||||||
if po.contexts[ctx] != nil {
|
if po.contexts[ctx] != nil {
|
||||||
if _, ok := po.contexts[ctx][str]; ok {
|
if _, ok := po.contexts[ctx][str]; ok {
|
||||||
return fmt.Sprintf(po.contexts[ctx][str].get(), vars...)
|
return Printf(po.contexts[ctx][str].Get(), vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the string we received by default
|
// Return the string we received by default
|
||||||
return fmt.Sprintf(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.
|
// 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 {
|
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
|
||||||
// Sync read
|
// Sync read
|
||||||
@@ -418,12 +443,144 @@ func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{})
|
|||||||
if _, ok := po.contexts[ctx]; ok {
|
if _, ok := po.contexts[ctx]; ok {
|
||||||
if po.contexts[ctx] != nil {
|
if po.contexts[ctx] != nil {
|
||||||
if _, ok := po.contexts[ctx][str]; ok {
|
if _, ok := po.contexts[ctx][str]; ok {
|
||||||
return fmt.Sprintf(po.contexts[ctx][str].getN(po.pluralForm(n)), vars...)
|
return Printf(po.contexts[ctx][str].GetN(po.pluralForm(n)), vars...)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return the plural string we received by default
|
// Parse plural forms to distinguish between plural and singular
|
||||||
return fmt.Sprintf(plural, vars...)
|
if po.pluralForm(n) == 0 {
|
||||||
|
return Printf(str, vars...)
|
||||||
|
}
|
||||||
|
return Printf(plural, vars...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (po *Po) GetE(str string, vars ...interface{}) (string, bool) {
|
||||||
|
// Sync read
|
||||||
|
po.RLock()
|
||||||
|
defer po.RUnlock()
|
||||||
|
|
||||||
|
if po.translations != nil {
|
||||||
|
if _, ok := po.translations[str]; ok {
|
||||||
|
if fmt, ok := po.translations[str].GetE(); ok {
|
||||||
|
return Printf(fmt, vars...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (po *Po) GetNE(str, plural string, n int, vars ...interface{}) (string, bool) {
|
||||||
|
// Sync read
|
||||||
|
po.RLock()
|
||||||
|
defer po.RUnlock()
|
||||||
|
|
||||||
|
if po.translations != nil {
|
||||||
|
if _, ok := po.translations[str]; ok {
|
||||||
|
if fmt, ok := po.translations[str].GetNE(po.pluralForm(n)); ok {
|
||||||
|
return Printf(fmt, vars...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (po *Po) GetCE(str, ctx string, vars ...interface{}) (string, bool) {
|
||||||
|
// 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 {
|
||||||
|
if fmt, ok := po.contexts[ctx][str].GetE(); ok {
|
||||||
|
return Printf(fmt, vars...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNCE 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.
|
||||||
|
// The second return value is true iff the string was found.
|
||||||
|
func (po *Po) GetNCE(str, plural string, n int, ctx string, vars ...interface{}) (string, bool) {
|
||||||
|
// 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 {
|
||||||
|
if fmt, ok := po.contexts[ctx][str].GetNE(po.pluralForm(n)); ok {
|
||||||
|
return Printf(fmt, vars...), true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse plural forms to distinguish between plural and singular
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalBinary implements encoding.BinaryMarshaler interface
|
||||||
|
func (po *Po) MarshalBinary() ([]byte, error) {
|
||||||
|
obj := new(TranslatorEncoding)
|
||||||
|
obj.Headers = po.Headers
|
||||||
|
obj.Language = po.Language
|
||||||
|
obj.PluralForms = po.PluralForms
|
||||||
|
obj.Nplurals = po.nplurals
|
||||||
|
obj.Plural = po.plural
|
||||||
|
obj.Translations = po.translations
|
||||||
|
obj.Contexts = po.contexts
|
||||||
|
|
||||||
|
var buff bytes.Buffer
|
||||||
|
encoder := gob.NewEncoder(&buff)
|
||||||
|
err := encoder.Encode(obj)
|
||||||
|
|
||||||
|
return buff.Bytes(), err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalBinary implements encoding.BinaryUnmarshaler interface
|
||||||
|
func (po *Po) UnmarshalBinary(data []byte) error {
|
||||||
|
buff := bytes.NewBuffer(data)
|
||||||
|
obj := new(TranslatorEncoding)
|
||||||
|
|
||||||
|
decoder := gob.NewDecoder(buff)
|
||||||
|
err := decoder.Decode(obj)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
po.Headers = obj.Headers
|
||||||
|
po.Language = obj.Language
|
||||||
|
po.PluralForms = obj.PluralForms
|
||||||
|
po.nplurals = obj.Nplurals
|
||||||
|
po.plural = obj.Plural
|
||||||
|
po.translations = obj.Translations
|
||||||
|
po.contexts = obj.Contexts
|
||||||
|
|
||||||
|
if expr, err := plurals.Compile(po.plural); err == nil {
|
||||||
|
po.pluralforms = expr
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
363
po_test.go
363
po_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
|
package gotext
|
||||||
|
|
||||||
import (
|
import (
|
||||||
@@ -6,11 +11,34 @@ import (
|
|||||||
"testing"
|
"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) {
|
func TestPo(t *testing.T) {
|
||||||
// Set PO content
|
// Set PO content
|
||||||
str := `
|
str := `
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
# Initial comment
|
# Initial comment
|
||||||
# Headers below
|
# Headers below
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
@@ -26,9 +54,25 @@ msgstr "Translated text"
|
|||||||
msgid "Another string"
|
msgid "Another string"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
|
# Multi-line msgid
|
||||||
|
msgid ""
|
||||||
|
"multi"
|
||||||
|
"line"
|
||||||
|
"id"
|
||||||
|
msgstr "id with multiline content"
|
||||||
|
|
||||||
|
# Multi-line msgid_plural
|
||||||
|
msgid ""
|
||||||
|
"multi"
|
||||||
|
"line"
|
||||||
|
"plural"
|
||||||
|
"id"
|
||||||
|
msgstr "plural id with multiline content"
|
||||||
|
|
||||||
#Multi-line string
|
#Multi-line string
|
||||||
msgid "Multi-line"
|
msgid "Multi-line"
|
||||||
msgstr "Multi "
|
msgstr ""
|
||||||
|
"Multi "
|
||||||
"line"
|
"line"
|
||||||
|
|
||||||
msgid "One with var: %s"
|
msgid "One with var: %s"
|
||||||
@@ -37,14 +81,6 @@ msgstr[0] "This one is the singular: %s"
|
|||||||
msgstr[1] "This one is the plural: %s"
|
msgstr[1] "This one is the plural: %s"
|
||||||
msgstr[2] "And this is the second plural form: %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"
|
msgctxt "Ctx"
|
||||||
msgid "One with var: %s"
|
msgid "One with var: %s"
|
||||||
msgid_plural "Several with vars: %s"
|
msgid_plural "Several with vars: %s"
|
||||||
@@ -52,15 +88,24 @@ msgstr[0] "This one is the singular in a Ctx context: %s"
|
|||||||
msgstr[1] "This one is the plural in a Ctx context: %s"
|
msgstr[1] "This one is the plural in a Ctx context: %s"
|
||||||
|
|
||||||
msgid "Some random"
|
msgid "Some random"
|
||||||
msgstr "Some random translation"
|
msgstr "Some random Translation"
|
||||||
|
|
||||||
msgctxt "Ctx"
|
msgctxt "Ctx"
|
||||||
msgid "Some random in a context"
|
msgid "Some random in a context"
|
||||||
msgstr "Some random translation 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"
|
msgid "More"
|
||||||
msgstr "More translation"
|
msgstr "More Translation"
|
||||||
`
|
|
||||||
|
`
|
||||||
|
|
||||||
// Write PO content to file
|
// Write PO content to file
|
||||||
filename := path.Clean(os.TempDir() + string(os.PathSeparator) + "default.po")
|
filename := path.Clean(os.TempDir() + string(os.PathSeparator) + "default.po")
|
||||||
@@ -97,6 +142,18 @@ msgstr "More translation"
|
|||||||
t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr)
|
t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test multi-line id
|
||||||
|
tr = po.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 = po.Get("multilinepluralid")
|
||||||
|
if tr != "plural id with multiline content" {
|
||||||
|
t.Errorf("Expected 'plural id with multiline content' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
// Test multi-line
|
// Test multi-line
|
||||||
tr = po.Get("Multi-line")
|
tr = po.Get("Multi-line")
|
||||||
if tr != "Multi line" {
|
if tr != "Multi line" {
|
||||||
@@ -109,7 +166,7 @@ msgstr "More translation"
|
|||||||
t.Errorf("Expected 'This one is the plural: Variable' but got '%s'", tr)
|
t.Errorf("Expected 'This one is the plural: Variable' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test inexistent translations
|
// Test not existent translations
|
||||||
tr = po.Get("This is a test")
|
tr = po.Get("This is a test")
|
||||||
if tr != "This is a test" {
|
if tr != "This is a test" {
|
||||||
t.Errorf("Expected 'This is a test' but got '%s'", tr)
|
t.Errorf("Expected 'This is a test' but got '%s'", tr)
|
||||||
@@ -120,17 +177,22 @@ msgstr "More translation"
|
|||||||
t.Errorf("Expected 'This are tests' but got '%s'", tr)
|
t.Errorf("Expected 'This are tests' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test syntax error parsed translations
|
// Test translations with existence check
|
||||||
tr = po.Get("This one has invalid syntax translations")
|
tr, exists := po.GetE("My text")
|
||||||
if tr != "" {
|
if (tr != "Translated text") || (!exists) {
|
||||||
t.Errorf("Expected '' but got '%s'", tr)
|
t.Errorf("Expected 'Translated text', true but got '%s', %v", tr, exists)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr, exists = po.GetE("I don't exist")
|
||||||
|
if exists {
|
||||||
|
t.Errorf("Expected 'I don't exist' not to exist but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
tr = po.GetN("This one has invalid syntax translations", "This are tests", 4)
|
tr = po.GetN("I don't exist", "We don't exist", 100)
|
||||||
if tr != "Plural index" {
|
if exists {
|
||||||
t.Errorf("Expected 'Plural index' but got '%s'", tr)
|
t.Errorf("Expected 'I/We don't exist' not to exist but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test context translations
|
// Test context translations
|
||||||
v = "Test"
|
v = "Test"
|
||||||
tr = po.GetC("One with var: %s", "Ctx", v)
|
tr = po.GetC("One with var: %s", "Ctx", v)
|
||||||
@@ -144,19 +206,110 @@ msgstr "More translation"
|
|||||||
t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr)
|
t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test last translation
|
// Test default plural vs singular return responses
|
||||||
tr = po.Get("More")
|
tr = po.GetN("Original", "Original plural", 4)
|
||||||
if tr != "More translation" {
|
if tr != "Original plural" {
|
||||||
t.Errorf("Expected 'More translation' but got '%s'", tr)
|
t.Errorf("Expected 'Original plural' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
tr = po.GetN("Original", "Original plural", 1)
|
||||||
|
if tr != "Original" {
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = po.Get("Empty plural form singular")
|
||||||
|
if tr != "Singular translated" {
|
||||||
|
t.Errorf("Expected 'Singular translated' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = po.GetN("Empty plural form singular", "Empty plural form", 1)
|
||||||
|
if tr != "Singular translated" {
|
||||||
|
t.Errorf("Expected 'Singular translated' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = po.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 = po.Get("More")
|
||||||
|
if tr != "More Translation" {
|
||||||
|
t.Errorf("Expected 'More Translation' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPlural(t *testing.T) {
|
||||||
|
// Set PO content
|
||||||
|
str := `
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
|
|
||||||
|
msgid "Singular: %s"
|
||||||
|
msgid_plural "Plural: %s"
|
||||||
|
msgstr[0] "TR Singular: %s"
|
||||||
|
msgstr[1] "TR Plural: %s"
|
||||||
|
msgstr[2] "TR Plural 2: %s"
|
||||||
|
|
||||||
|
|
||||||
|
`
|
||||||
|
// Create po object
|
||||||
|
po := new(Po)
|
||||||
|
po.Parse([]byte(str))
|
||||||
|
|
||||||
|
v := "Var"
|
||||||
|
tr := po.GetN("Singular: %s", "Plural: %s", 2, v)
|
||||||
|
if tr != "TR Plural: Var" {
|
||||||
|
t.Errorf("Expected 'TR Plural: Var' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = po.GetN("Singular: %s", "Plural: %s", 1, v)
|
||||||
|
if tr != "TR Singular: Var" {
|
||||||
|
t.Errorf("Expected 'TR Singular: Var' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluralNoHeaderInformation(t *testing.T) {
|
||||||
|
// Set PO content
|
||||||
|
str := `
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
msgid "Singular: %s"
|
||||||
|
msgid_plural "Plural: %s"
|
||||||
|
msgstr[0] "TR Singular: %s"
|
||||||
|
msgstr[1] "TR Plural: %s"
|
||||||
|
msgstr[2] "TR Plural 2: %s"
|
||||||
|
|
||||||
|
|
||||||
|
`
|
||||||
|
// Create po object
|
||||||
|
po := new(Po)
|
||||||
|
po.Parse([]byte(str))
|
||||||
|
|
||||||
|
v := "Var"
|
||||||
|
tr := po.GetN("Singular: %s", "Plural: %s", 2, v)
|
||||||
|
if tr != "TR Plural: Var" {
|
||||||
|
t.Errorf("Expected 'TR Plural: Var' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
|
||||||
|
tr = po.GetN("Singular: %s", "Plural: %s", 1, v)
|
||||||
|
if tr != "TR Singular: Var" {
|
||||||
|
t.Errorf("Expected 'TR Singular: Var' but got '%s'", tr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPoHeaders(t *testing.T) {
|
func TestPoHeaders(t *testing.T) {
|
||||||
// Set PO content
|
// Set PO content
|
||||||
str := `
|
str := `
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
# Initial comment
|
# Initial comment
|
||||||
# Headers below
|
# Headers below
|
||||||
"Language: en\n"
|
"Language: en\n"
|
||||||
@@ -167,13 +320,13 @@ msgstr ""
|
|||||||
# Some comment
|
# Some comment
|
||||||
msgid "Example"
|
msgid "Example"
|
||||||
msgstr "Translated example"
|
msgstr "Translated example"
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create po object
|
// Create po object
|
||||||
po := new(Po)
|
po := new(Po)
|
||||||
|
|
||||||
// Parse
|
// Parse
|
||||||
po.Parse(str)
|
po.Parse([]byte(str))
|
||||||
|
|
||||||
// Check headers expected
|
// Check headers expected
|
||||||
if po.Language != "en" {
|
if po.Language != "en" {
|
||||||
@@ -186,9 +339,30 @@ msgstr "Translated example"
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPluralForms(t *testing.T) {
|
func TestMissingPoHeadersSupport(t *testing.T) {
|
||||||
|
// Set PO content
|
||||||
|
str := `
|
||||||
|
msgid "Example"
|
||||||
|
msgstr "Translated example"
|
||||||
|
`
|
||||||
|
|
||||||
|
// Create po object
|
||||||
|
po := new(Po)
|
||||||
|
|
||||||
|
// Parse
|
||||||
|
po.Parse([]byte(str))
|
||||||
|
|
||||||
|
// Check Translation expected
|
||||||
|
if po.Get("Example") != "Translated example" {
|
||||||
|
t.Errorf("Expected 'Translated example' but got '%s'", po.Get("Example"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPluralFormsSingle(t *testing.T) {
|
||||||
// Single form
|
// Single form
|
||||||
str := `
|
str := `
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
"Plural-Forms: nplurals=1; plural=0;"
|
"Plural-Forms: nplurals=1; plural=0;"
|
||||||
|
|
||||||
# Some comment
|
# Some comment
|
||||||
@@ -198,13 +372,13 @@ msgstr[0] "Singular form"
|
|||||||
msgstr[1] "Plural form 1"
|
msgstr[1] "Plural form 1"
|
||||||
msgstr[2] "Plural form 2"
|
msgstr[2] "Plural form 2"
|
||||||
msgstr[3] "Plural form 3"
|
msgstr[3] "Plural form 3"
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create po object
|
// Create po object
|
||||||
po := new(Po)
|
po := new(Po)
|
||||||
|
|
||||||
// Parse
|
// Parse
|
||||||
po.Parse(str)
|
po.Parse([]byte(str))
|
||||||
|
|
||||||
// Check plural form
|
// Check plural form
|
||||||
n := po.pluralForm(0)
|
n := po.pluralForm(0)
|
||||||
@@ -227,10 +401,13 @@ msgstr[3] "Plural form 3"
|
|||||||
if n != 0 {
|
if n != 0 {
|
||||||
t.Errorf("Expected 0 for pluralForm(50), got %d", n)
|
t.Errorf("Expected 0 for pluralForm(50), got %d", n)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
func TestPluralForms2(t *testing.T) {
|
||||||
// 2 forms
|
// 2 forms
|
||||||
str = `
|
str := `
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
"Plural-Forms: nplurals=2; plural=n != 1;"
|
"Plural-Forms: nplurals=2; plural=n != 1;"
|
||||||
|
|
||||||
# Some comment
|
# Some comment
|
||||||
@@ -240,16 +417,16 @@ msgstr[0] "Singular form"
|
|||||||
msgstr[1] "Plural form 1"
|
msgstr[1] "Plural form 1"
|
||||||
msgstr[2] "Plural form 2"
|
msgstr[2] "Plural form 2"
|
||||||
msgstr[3] "Plural form 3"
|
msgstr[3] "Plural form 3"
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create po object
|
// Create po object
|
||||||
po = new(Po)
|
po := new(Po)
|
||||||
|
|
||||||
// Parse
|
// Parse
|
||||||
po.Parse(str)
|
po.Parse([]byte(str))
|
||||||
|
|
||||||
// Check plural form
|
// Check plural form
|
||||||
n = po.pluralForm(0)
|
n := po.pluralForm(0)
|
||||||
if n != 1 {
|
if n != 1 {
|
||||||
t.Errorf("Expected 1 for pluralForm(0), got %d", n)
|
t.Errorf("Expected 1 for pluralForm(0), got %d", n)
|
||||||
}
|
}
|
||||||
@@ -265,10 +442,13 @@ msgstr[3] "Plural form 3"
|
|||||||
if n != 1 {
|
if n != 1 {
|
||||||
t.Errorf("Expected 1 for pluralForm(3), got %d", n)
|
t.Errorf("Expected 1 for pluralForm(3), got %d", n)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
func TestPluralForms3(t *testing.T) {
|
||||||
// 3 forms
|
// 3 forms
|
||||||
str = `
|
str := `
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;"
|
"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;"
|
||||||
|
|
||||||
# Some comment
|
# Some comment
|
||||||
@@ -278,16 +458,16 @@ msgstr[0] "Singular form"
|
|||||||
msgstr[1] "Plural form 1"
|
msgstr[1] "Plural form 1"
|
||||||
msgstr[2] "Plural form 2"
|
msgstr[2] "Plural form 2"
|
||||||
msgstr[3] "Plural form 3"
|
msgstr[3] "Plural form 3"
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create po object
|
// Create po object
|
||||||
po = new(Po)
|
po := new(Po)
|
||||||
|
|
||||||
// Parse
|
// Parse
|
||||||
po.Parse(str)
|
po.Parse([]byte(str))
|
||||||
|
|
||||||
// Check plural form
|
// Check plural form
|
||||||
n = po.pluralForm(0)
|
n := po.pluralForm(0)
|
||||||
if n != 2 {
|
if n != 2 {
|
||||||
t.Errorf("Expected 2 for pluralForm(0), got %d", n)
|
t.Errorf("Expected 2 for pluralForm(0), got %d", n)
|
||||||
}
|
}
|
||||||
@@ -311,10 +491,13 @@ msgstr[3] "Plural form 3"
|
|||||||
if n != 1 {
|
if n != 1 {
|
||||||
t.Errorf("Expected 1 for pluralForm(3), got %d", n)
|
t.Errorf("Expected 1 for pluralForm(3), got %d", n)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ------------------------------------------------------------------------
|
func TestPluralFormsSpecial(t *testing.T) {
|
||||||
// 3 forms special
|
// 3 forms special
|
||||||
str = `
|
str := `
|
||||||
|
msgid ""
|
||||||
|
msgstr ""
|
||||||
"Plural-Forms: nplurals=3;"
|
"Plural-Forms: nplurals=3;"
|
||||||
"plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
|
"plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"
|
||||||
|
|
||||||
@@ -325,16 +508,16 @@ msgstr[0] "Singular form"
|
|||||||
msgstr[1] "Plural form 1"
|
msgstr[1] "Plural form 1"
|
||||||
msgstr[2] "Plural form 2"
|
msgstr[2] "Plural form 2"
|
||||||
msgstr[3] "Plural form 3"
|
msgstr[3] "Plural form 3"
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create po object
|
// Create po object
|
||||||
po = new(Po)
|
po := new(Po)
|
||||||
|
|
||||||
// Parse
|
// Parse
|
||||||
po.Parse(str)
|
po.Parse([]byte(str))
|
||||||
|
|
||||||
// Check plural form
|
// Check plural form
|
||||||
n = po.pluralForm(1)
|
n := po.pluralForm(1)
|
||||||
if n != 0 {
|
if n != 0 {
|
||||||
t.Errorf("Expected 0 for pluralForm(1), got %d", n)
|
t.Errorf("Expected 0 for pluralForm(1), got %d", n)
|
||||||
}
|
}
|
||||||
@@ -357,19 +540,18 @@ msgstr[3] "Plural form 3"
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTranslationObject(t *testing.T) {
|
func TestTranslationObject(t *testing.T) {
|
||||||
tr := newTranslation()
|
tr := NewTranslation()
|
||||||
str := tr.get()
|
str := tr.Get()
|
||||||
|
|
||||||
if str != "" {
|
if str != "" {
|
||||||
t.Errorf("Expected '' but got '%s'", str)
|
t.Errorf("Expected '' but got '%s'", str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set id
|
// Set id
|
||||||
tr.id = "Text"
|
tr.ID = "Text"
|
||||||
|
str = tr.Get()
|
||||||
|
|
||||||
// Get again
|
// Get again
|
||||||
str = tr.get()
|
|
||||||
|
|
||||||
if str != "Text" {
|
if str != "Text" {
|
||||||
t.Errorf("Expected 'Text' but got '%s'", str)
|
t.Errorf("Expected 'Text' but got '%s'", str)
|
||||||
}
|
}
|
||||||
@@ -391,7 +573,7 @@ msgstr[0] "This one is the singular: %s"
|
|||||||
msgstr[1] "This one is the plural: %s"
|
msgstr[1] "This one is the plural: %s"
|
||||||
msgstr[2] "And this is the second plural form: %s"
|
msgstr[2] "And this is the second plural form: %s"
|
||||||
|
|
||||||
`
|
`
|
||||||
|
|
||||||
// Create Po object
|
// Create Po object
|
||||||
po := new(Po)
|
po := new(Po)
|
||||||
@@ -402,11 +584,11 @@ msgstr[2] "And this is the second plural form: %s"
|
|||||||
|
|
||||||
// Parse po content in a goroutine
|
// Parse po content in a goroutine
|
||||||
go func(po *Po, done chan bool) {
|
go func(po *Po, done chan bool) {
|
||||||
po.Parse(str)
|
po.Parse([]byte(str))
|
||||||
done <- true
|
done <- true
|
||||||
}(po, pc)
|
}(po, pc)
|
||||||
|
|
||||||
// Read some translation on a goroutine
|
// Read some Translation on a goroutine
|
||||||
go func(po *Po, done chan bool) {
|
go func(po *Po, done chan bool) {
|
||||||
po.Get("My text")
|
po.Get("My text")
|
||||||
done <- true
|
done <- true
|
||||||
@@ -419,3 +601,62 @@ msgstr[2] "And this is the second plural form: %s"
|
|||||||
<-pc
|
<-pc
|
||||||
<-rc
|
<-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
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPoBinaryEncoding(t *testing.T) {
|
||||||
|
// Create po objects
|
||||||
|
po := new(Po)
|
||||||
|
po2 := new(Po)
|
||||||
|
|
||||||
|
// Parse file
|
||||||
|
po.ParseFile("fixtures/en_US/default.po")
|
||||||
|
|
||||||
|
buff, err := po.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = po2.UnmarshalBinary(buff)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test translations
|
||||||
|
tr := po2.Get("My text")
|
||||||
|
if tr != "Translated text" {
|
||||||
|
t.Errorf("Expected 'Translated text' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
// Test translations
|
||||||
|
tr = po2.Get("language")
|
||||||
|
if tr != "en_US" {
|
||||||
|
t.Errorf("Expected 'en_US' but got '%s'", tr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
78
translation.go
Normal file
78
translation.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
|
||||||
|
// Translation is the struct for the Translations parsed via Po or Mo files and all coming parsers
|
||||||
|
type Translation struct {
|
||||||
|
ID string
|
||||||
|
PluralID string
|
||||||
|
Trs map[int]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTranslation returns the Translation object and initialized it.
|
||||||
|
func NewTranslation() *Translation {
|
||||||
|
tr := new(Translation)
|
||||||
|
tr.Trs = make(map[int]string)
|
||||||
|
|
||||||
|
return tr
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get returns the string of the translation
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetN returns the string of the plural translation
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetE returns the string of the translation. The second return value is true
|
||||||
|
// iff the string was found.
|
||||||
|
func (t *Translation) GetE() (string, bool) {
|
||||||
|
// Look for Translation index 0
|
||||||
|
if _, ok := t.Trs[0]; ok {
|
||||||
|
if t.Trs[0] != "" {
|
||||||
|
return t.Trs[0], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetNE returns the string of the plural translation. The second return value
|
||||||
|
// is true iff the string was found.
|
||||||
|
func (t *Translation) GetNE(n int) (string, bool) {
|
||||||
|
// Look for Translation index
|
||||||
|
if _, ok := t.Trs[n]; ok {
|
||||||
|
if t.Trs[n] != "" {
|
||||||
|
return t.Trs[n], true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
65
translator.go
Normal file
65
translator.go
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
/*
|
||||||
|
* 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 "net/textproto"
|
||||||
|
|
||||||
|
// Translator interface is used by Locale and Po objects.Translator
|
||||||
|
// It contains all methods needed to parse translation sources and obtain corresponding translations.
|
||||||
|
// Also implements gob.GobEncoder/gob.DobDecoder interfaces to allow serialization of Locale objects.
|
||||||
|
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
|
||||||
|
|
||||||
|
GetE(str string, vars ...interface{}) (string, bool)
|
||||||
|
GetNE(str, plural string, n int, vars ...interface{}) (string, bool)
|
||||||
|
GetCE(str, ctx string, vars ...interface{}) (string, bool)
|
||||||
|
GetNCE(str, plural string, n int, ctx string, vars ...interface{}) (string, bool)
|
||||||
|
|
||||||
|
MarshalBinary() ([]byte, error)
|
||||||
|
UnmarshalBinary([]byte) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// TranslatorEncoding is used as intermediary storage to encode Translator objects to Gob.
|
||||||
|
type TranslatorEncoding struct {
|
||||||
|
// Headers storage
|
||||||
|
Headers textproto.MIMEHeader
|
||||||
|
|
||||||
|
// 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTranslator is used to recover a Translator object after unmarshaling the TranslatorEncoding object.
|
||||||
|
// Internally uses a Po object as it should be switcheable with Mo objects without problem.
|
||||||
|
// External Translator implementations should be able to serialize into a TranslatorEncoding object in order to unserialize into a Po-compatible object.
|
||||||
|
func (te *TranslatorEncoding) GetTranslator() Translator {
|
||||||
|
po := new(Po)
|
||||||
|
po.Headers = te.Headers
|
||||||
|
po.Language = te.Language
|
||||||
|
po.PluralForms = te.PluralForms
|
||||||
|
po.nplurals = te.Nplurals
|
||||||
|
po.plural = te.Plural
|
||||||
|
po.translations = te.Translations
|
||||||
|
po.contexts = te.Contexts
|
||||||
|
|
||||||
|
return po
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user