66 Commits

Author SHA1 Message Date
c8ac87a60c update module to own version 2020-07-29 18:59:14 +02:00
67d7b9ca60 update module to own version 2020-07-29 18:09:14 +02:00
bb9b8c2585 fix path on different OS 2020-07-29 16:47:49 +02:00
b4ab6497b5 update module to own version 2020-07-27 18:17:50 +02:00
2441e55972 „go.mod“ ändern 2020-07-27 16:06:38 +00:00
Ben Sarah Golightly
75a1fb9b04 Add variants of Get functions with existence checks
Currently, a translation that doesn't exist just defaults to the
passed message ID.

It can be helpful to be able to catch these missing cases e.g. to
save to a log.

This PR implements variants ending in 'E' like GetE, GetNE.

This wasn't implemented at the package-level scope, because for
quick translations like that the extra flexibility probably isn't
needed.
2020-07-23 00:28:56 +01:00
Leonel Quinteros
3bb41424ff Merge pull request #40 from chrisvaughn/cv/ar_bug
call Translation.Get & GetC directly
2020-04-22 12:11:35 -03:00
Chris Vaughn
d791a97f01 call Get & GetC directly instead of using plural=1 for package methods 2020-04-19 14:50:56 -05:00
Chris Vaughn
192fa01364 call Translation.Get & GetC directly instead of calling Translation.GetN with default of plural=1
* add correct Arabic pluralform rules to test
2020-04-16 17:29:45 -05:00
Leonel Quinteros Carbano
362a1d6915 Add xgotext CLI tool installation to build process 2020-04-15 22:16:10 -03:00
Leonel Quinteros Carbano
60d22914a5 Remove old Build configuration. 2020-04-15 22:12:20 -03:00
Leonel Quinteros Carbano
f7464201e6 Add Github Build action badge 2020-04-15 22:08:55 -03:00
Leonel Quinteros
d26d707c73 Create build.yml 2020-04-15 22:03:51 -03:00
Leonel Quinteros
b6df672e9a Merge pull request #37 from bboehmke/xgotext_verbose
verbose flag for xgotext
2020-03-06 10:52:42 -03:00
Benjamin Böhmke
c40a6e9dd5 xgotext: added verbose flag 2020-03-05 19:40:31 +01:00
Leonel Quinteros
be1a13b346 Merge pull request #36 from bboehmke/xgotext_fixes
xgotext fixes
2020-02-29 16:38:15 -03:00
Benjamin Böhmke
36588ae653 xgotext: updated README.md 2020-02-29 11:42:35 +01:00
Benjamin Böhmke
20f50a5bba xgotext: name generated files .pot 2020-02-28 16:56:38 +01:00
Benjamin Böhmke
5b0ba55f37 xgotext: added option to exclude directories 2020-02-27 19:59:03 +01:00
Benjamin Böhmke
df996c3ae1 xgotext: catch unknown variable type 2020-02-26 21:43:19 +01:00
Benjamin Böhmke
f46758caed xgotext: fixed parsing of error types 2020-02-26 21:08:59 +01:00
Leonel Quinteros
4aa838b8c3 Merge pull request #35 from bboehmke/xgotext_rework
rework of xgotext
2020-02-25 11:15:17 -03:00
Benjamin Böhmke
478f4d29b7 added option to set default domain 2020-02-24 17:52:39 +01:00
Benjamin Böhmke
9091772533 fixed support for older go versions 2020-02-24 15:33:30 +01:00
Benjamin Böhmke
3b92b162e1 moved PO file writing to domain object 2020-02-23 10:08:48 +01:00
Benjamin Böhmke
b8c9a57c39 catch unsupported function calls 2020-02-23 10:00:43 +01:00
Benjamin Böhmke
1891633250 updated CLI README.md 2020-02-23 09:51:48 +01:00
Benjamin Böhmke
75a3d22c53 initial rework of xgotext 2020-02-22 22:37:17 +01:00
Leonel Quinteros
2b59b30398 Update README.md 2019-11-13 14:59:02 -03:00
Leonel Quinteros
208807f5ca Create go.yml 2019-11-13 14:46:21 -03:00
Leonel Quinteros
823ca32c7a Fallback Po's missing plural translation using plural forms when available. Use western rule n==1 convention only on Locale object without Domain. Fixes #34 2019-10-21 14:43:48 -03:00
Leonel Quinteros
99a9166ded Remove code coverage tool 2019-07-10 18:23:45 -03:00
Leonel Quinteros
6431cb3aea Parse SetDomain methods 2019-02-18 18:09:32 -03:00
Leonel Quinteros
0e382cfe26 First implementation of CLI tool 2019-02-15 15:20:42 -03:00
Leonel Quinteros
ff3209d159 Remove dep files 2018-12-21 14:53:49 -03:00
Leonel Quinteros
0216b71049 Merge pull request #30 from draven-archive/master
Fix gotext.GetD with unloaded domain
2018-12-17 10:02:03 -03:00
draveness
37bac2fe57 Fix gotext.GetD with unloaded domain 2018-12-07 12:32:39 +08:00
Leonel Quinteros
7b73c0d36b Update README 2018-09-12 15:31:27 -03:00
Leonel Quinteros
477ce49ddf Update Travis configuration. 2018-09-12 15:07:17 -03:00
Leonel Quinteros
f28243c54b Add support for Go Modules. Closes #27 2018-09-12 15:03:50 -03:00
Leonel Quinteros
4cbf30d337 Make Locale and Po objects serializable. Closes #23 2018-09-07 18:14:38 -03:00
Leonel Quinteros
302c88af99 Add arabic translation test. See #25 2018-08-14 11:01:14 -03:00
Leonel Quinteros
8e9d9df2e2 Allow Locale backends to work with different Translator sources using AddTranslator method for Locale object. Fixes #22 2018-07-05 11:37:25 -03:00
Leonel Quinteros
431a313411 Merge branch 'master' of github.com:leonelquinteros/gotext 2018-05-29 12:46:28 -03:00
Leonel Quinteros
3c52f5c10b Make golint happy. Fixes #21 2018-05-29 12:46:11 -03:00
Leonel Quinteros
7fd6ac336a Update ISSUE_TEMPLATE.md 2018-05-14 16:24:26 -03:00
Leonel Quinteros
9161ab1e62 Update PULL_REQUEST_TEMPLATE.md 2018-05-14 16:22:29 -03:00
Leonel Quinteros
2044aeae11 Update ISSUE_TEMPLATE.md 2018-05-14 16:15:38 -03:00
Leonel Quinteros
7c73cc2975 Fix travis tests 2018-04-08 18:25:47 -03:00
Leonel Quinteros
48726a37dd Update README file. Update Travis config 2018-04-08 17:49:58 -03:00
Leonel Quinteros
c62dae3c6b Fix crash during Configure. Fixes #24 2018-04-08 17:37:29 -03:00
Leonel Quinteros
3adb2eae86 Merge pull request #19 from DeineAgenturUG/leonelquinteros-master
Create MO parser
2018-03-28 11:08:43 -03:00
4172a9c4a7 Update commits
Signed-off-by: Josef Fröhle <froehle@b1-systems.de>
2018-03-26 20:47:41 +02:00
cd46239477 Create MO parser
Refactored a bit too, so we can use interfaces to take Mo and Po files

added fixtures

found that the parser for Po files have a bug... but it works... so not touched
2018-03-24 22:44:37 +01:00
Leonel Quinteros
8c36835ece Merge pull request #16 from DeineAgenturUG/plurals
Create own plurals subpackage
2018-03-23 15:28:40 -03:00
a19c5fd581 Create own plurals subpackage
fix Typo
2018-03-22 22:58:55 +01:00
Leonel Quinteros
92b69ffa4c Honor package domain in Locale methods. Increase test coverage. 2018-02-14 12:04:47 -03:00
Leonel Quinteros
c583d0991b Fix GetN and GetNC to honor package domain. Refactor global package functions to make them all concurrent safe. Fixes #14 2018-02-13 17:35:07 -03:00
Leonel Quinteros
4d0fbfd720 Update PULL_REQUEST_TEMPLATE.md 2018-02-13 15:34:25 -03:00
Leonel Quinteros
d0a0759bca Create ISSUE_TEMPLATE.md 2018-02-13 15:27:02 -03:00
Leonel Quinteros
972f5685e7 Create PULL_REQUEST_TEMPLATE.md 2018-02-13 15:21:25 -03:00
Leonel Quinteros
36b0166de9 Create CONTRIBUTING.md 2017-11-16 18:50:32 -03:00
Leonel Quinteros
b44b8cf3f4 Merge pull request #12 from leonelquinteros/add-code-of-conduct-1
Create CODE_OF_CONDUCT.md
2017-11-16 18:45:56 -03:00
Leonel Quinteros
3f452f14f2 Create CODE_OF_CONDUCT.md 2017-11-16 18:45:37 -03:00
Leonel Quinteros
7d86bb66fe Unify fmt.Sprintf behaviour on Po and Locale 2017-11-02 10:21:20 -03:00
Leonel Quinteros
50cdb4e058 Add Go 1.9 to travis builds 2017-10-26 16:27:26 -03:00
65 changed files with 4640 additions and 4179 deletions

14
.github/ISSUE_TEMPLATE.md vendored Normal file
View 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
View 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
View 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
View File

@@ -3,6 +3,9 @@
.settings
.buildpath
# golang jetbrains shit
.idea
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a

View File

@@ -1,16 +1,8 @@
language: go
go:
- 1.6
- 1.7
- 1.8
- tip
before_install:
- go get -t -v ./...
script: go test -v -race -coverprofile=coverage.txt -covermode=atomic .
after_success:
- bash <(curl -s https://codecov.io/bash)
- "1.9"
- "1.10"
- "1.11"
- "1.12"
- "tip"

46
CODE_OF_CONDUCT.md Normal file
View 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
View 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.

15
Gopkg.lock generated
View File

@@ -1,15 +0,0 @@
# This file is autogenerated, do not edit; changes may be undone by the next 'dep ensure'.
[[projects]]
branch = "master"
name = "github.com/mattn/kinako"
packages = ["ast","parser","vm"]
revision = "332c0a7e205a29536e672337a4bea6c7a96b04c1"
[solve-meta]
analyzer-name = "dep"
analyzer-version = 1
inputs-digest = "d3069fabe2d6f79fe33ad88133e861db84aef0400f6b949c4e64395913b3ae97"
solver-name = "gps-cdcl"
solver-version = 1

View File

@@ -1,26 +0,0 @@
# Gopkg.toml example
#
# Refer to https://github.com/golang/dep/blob/master/docs/Gopkg.toml.md
# for detailed Gopkg.toml documentation.
#
# required = ["github.com/user/thing/cmd/thing"]
# ignored = ["github.com/user/project/pkgX", "bitbucket.org/user/project/pkgA/pkgY"]
#
# [[constraint]]
# name = "github.com/user/project"
# version = "1.0.0"
#
# [[constraint]]
# name = "github.com/user/project2"
# branch = "dev"
# source = "github.com/myfork/project2"
#
# [[override]]
# name = "github.com/x/y"
# version = "2.4.0"
[[constraint]]
branch = "master"
name = "github.com/mattn/kinako"

126
README.md
View File

@@ -1,13 +1,13 @@
[![GitHub release](https://img.shields.io/github/release/leonelquinteros/gotext.svg)](https://github.com/leonelquinteros/gotext)
[![GitHub release](https://img.shields.io/github/release/leonelquinteros/gotext.svg)](https://git.deineagentur.com/DeineAgenturUG/gotext)
[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![GoDoc](https://godoc.org/github.com/leonelquinteros/gotext?status.svg)](https://godoc.org/github.com/leonelquinteros/gotext)
[![GoDoc](https://godoc.org/git.deineagentur.com/DeineAgenturUG/gotext?status.svg)](https://godoc.org/git.deineagentur.com/DeineAgenturUG/gotext)
![Gotext build](https://git.deineagentur.com/DeineAgenturUG/gotext/workflows/Gotext%20build/badge.svg?branch=master)
[![Build Status](https://travis-ci.org/leonelquinteros/gotext.svg?branch=master)](https://travis-ci.org/leonelquinteros/gotext)
[![codecov](https://codecov.io/gh/leonelquinteros/gotext/branch/master/graph/badge.svg)](https://codecov.io/gh/leonelquinteros/gotext)
[![Go Report Card](https://goreportcard.com/badge/github.com/leonelquinteros/gotext)](https://goreportcard.com/report/github.com/leonelquinteros/gotext)
[![Go Report Card](https://goreportcard.com/badge/git.deineagentur.com/DeineAgenturUG/gotext)](https://goreportcard.com/report/git.deineagentur.com/DeineAgenturUG/gotext)
# Gotext
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
[GNU gettext utilities](https://www.gnu.org/software/gettext) for Go.
# Features
@@ -18,11 +18,14 @@
- Support for variables inside translation strings using Go's [fmt syntax](https://golang.org/pkg/fmt/).
- Support for [pluralization rules](https://www.gnu.org/software/gettext/manual/html_node/Translating-plural-forms.html).
- Support for [message contexts](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html).
- Thread-safe: This package is safe for concurrent use across multiple goroutines.
- 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.
- Unit tests available.
- Language codes are automatically simplified from the form `en_UK` to `en` if the first isn't available.
- Ready to use inside Go templates.
- Objects are serializable to []byte to store them in cache.
- Support for Go Modules.
# License
@@ -32,28 +35,55 @@
# 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 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.
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)
@@ -63,7 +93,7 @@ To get the latest v1 package stable release, execute:
go get gopkg.in/leonelquinteros/gotext.v1
```
To import this package, add the following line to your code:
Import as
```go
import "gopkg.in/leonelquinteros/gotext.v1"
@@ -74,19 +104,19 @@ Refer to it as gotext.
# Locales directories structure
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.
The package will assume a directories structure starting with a base path that will be provided to the package configuration
or to object constructors depending on the use, but either will use the same convention to lookup inside the base path.
Inside the base directory where will be the language directories named using the language and country 2-letter codes (en_US, es_AR, ...).
All package functions can lookup after the simplified version for each language in case the full code isn't present but the more general language code exists.
So if the language set is `en_UK`, but there is no directory named after that code and there is a directory named `en`,
all package functions will be able to resolve this generalization and provide translations for the more general library.
Inside the base directory where will be the language directories named using the language and country 2-letter codes (en_US, es_AR, ...).
All package functions can lookup after the simplified version for each language in case the full code isn't present but the more general language code exists.
So if the language set is `en_UK`, but there is no directory named after that code and there is a directory named `en`,
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.
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
A library directory structure can look like:
Then, there can be a `LC_MESSAGES` containing all PO files or the PO files themselves.
A library directory structure can look like:
```
/path/to/locales
@@ -111,7 +141,7 @@ A library directory structure can look like:
/path/to/locales/fr
/path/to/locales/fr/default.po
/path/to/locales/fr/extras.po
```
```
And so on...
@@ -125,16 +155,16 @@ For quick/simple translations you can use the package level functions directly.
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
"git.deineagentur.com/DeineAgenturUG/gotext"
)
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"))
// Translate text from a different domain without reconfigure
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
}
@@ -143,22 +173,22 @@ func main() {
## Using dynamic variables on translations
All translation strings support dynamic variables to be inserted without translate.
Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string.
All translation strings support dynamic variables to be inserted without translate.
Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string.
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
"git.deineagentur.com/DeineAgenturUG/gotext"
)
func main() {
// Configure package
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
// Set variables
name := "John"
// Translate text with variables
fmt.Println(gotext.Get("Hi, my name is %s", name))
}
@@ -168,35 +198,35 @@ func main() {
## 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.
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
"git.deineagentur.com/DeineAgenturUG/gotext"
)
func main() {
// Create Locale with library path and language code
l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY")
// Load domain '/path/to/locales/root/dir/es_UY/default.po'
l.AddDomain("default")
// Translate text from default domain
fmt.Println(l.Get("Translate this"))
// Load different domain
l.AddDomain("translations")
// Translate text from domain
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.
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" }}
@@ -205,13 +235,13 @@ 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
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.
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
"git.deineagentur.com/DeineAgenturUG/gotext"
)
func main() {
@@ -226,11 +256,11 @@ msgstr ""
msgid "One with var: %s"
msgstr "This one sets the var: %s"
`
// Create Po object
po := new(gotext.Po)
po.Parse(str)
fmt.Println(po.Get("Translate this"))
}
```
@@ -247,7 +277,7 @@ Plural formulas are parsed and evaluated using [Kinako](https://github.com/mattn
```go
import (
"fmt"
"github.com/leonelquinteros/gotext"
"git.deineagentur.com/DeineAgenturUG/gotext"
)
func main() {
@@ -270,21 +300,21 @@ msgid_plural "Several with vars: %s"
msgstr[0] "This one is the singular: %s"
msgstr[1] "This one is the plural: %s"
`
// Create Po object
po := new(gotext.Po)
po.Parse(str)
fmt.Println(po.GetN("One with var: %s", "Several with vars: %s", 54, v))
// "This one is the plural: Variable"
}
```
# Contribute
# Contribute
- Please, contribute.
- Use the package on your projects.
- Report issues on Github.
- Report issues on Github.
- Send pull requests for bugfixes and improvements.
- Send proposals on Github issues.

48
cli/xgotext/README.md Normal file
View 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

View 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] ""

View 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 ""

View 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 ""

View 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] ""

View 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")
}

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

View 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
}

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

View 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
View 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 مستند إضافي"

View File

@@ -0,0 +1,2 @@
msgid "Alcohol & Tobacco"
msgstr "الكحول والتبغ"

BIN
fixtures/de/default.mo Normal file

Binary file not shown.

77
fixtures/de/default.po Normal file
View 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"

Binary file not shown.

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

Binary file not shown.

BIN
fixtures/en_US/default.mo Normal file

Binary file not shown.

68
fixtures/en_US/default.po Normal file
View 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"

Binary file not shown.

View 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
View 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
View 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=

205
gotext.go
View File

@@ -1,154 +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 (
"fmt"
"github.com/leonelquinteros/gotext"
)
import (
"fmt"
"git.deineagentur.com/DeineAgenturUG/gotext"
)
func main() {
// Configure package
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
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"))
// Translate text from default domain
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
// Translate text from a different domain without reconfigure
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
}
// Translate text from a different domain without reconfigure
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
}
*/
package gotext
import (
"encoding/gob"
"sync"
)
// Global environment variables
var (
type config struct {
sync.RWMutex
// Default domain to look at when no domain is specified. Used by package level functions.
domain = "default"
domain string
// Language set.
language = "en_US"
language string
// Path to library directory where all locale directories and translation files are.
library = "/usr/local/share/locale"
// Path to library directory where all locale directories and Translation files are.
library string
// Storage for package level methods
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.
// It's called automatically when trying to use Get or GetD methods.
func loadStorage(force bool) {
if storage == nil || force {
storage = NewLocale(library, language)
globalConfig.Lock()
if globalConfig.storage == nil || force {
globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language)
}
if _, ok := storage.domains[domain]; !ok || force {
storage.AddDomain(domain)
if _, ok := globalConfig.storage.Domains[globalConfig.domain]; !ok || force {
globalConfig.storage.AddDomain(globalConfig.domain)
}
globalConfig.storage.SetDomain(globalConfig.domain)
globalConfig.Unlock()
}
// GetDomain is the domain getter for the package configuration
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.
// It reloads the corresponding translation file.
// It reloads the corresponding Translation file.
func SetDomain(dom string) {
domain = dom
globalConfig.Lock()
globalConfig.domain = dom
if globalConfig.storage != nil {
globalConfig.storage.SetDomain(dom)
}
globalConfig.Unlock()
loadStorage(true)
}
// GetLanguage is the language getter for the package configuration
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.
// It reloads the corresponding translation file.
// It reloads the corresponding Translation file.
func SetLanguage(lang string) {
language = lang
globalConfig.Lock()
globalConfig.language = SimplifiedLocale(lang)
globalConfig.Unlock()
loadStorage(true)
}
// GetLibrary is the library getter for the package configuration
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.
// It reloads the corresponding translation file.
// It reloads the corresponding Translation file.
func SetLibrary(lib string) {
library = lib
globalConfig.Lock()
globalConfig.library = lib
globalConfig.Unlock()
loadStorage(true)
}
// Configure sets all configuration variables to be used at package level and reloads the corresponding translation file.
// Configure sets all configuration variables to be used at package level and reloads the corresponding Translation file.
// It receives the library path, language code and domain name.
// This function is recommended to be used when changing more than one setting,
// as using each setter will introduce a I/O overhead because the translation file will be loaded after each set.
// as using each setter will introduce a I/O overhead because the Translation file will be loaded after each set.
func Configure(lib, lang, dom string) {
library = lib
language = lang
domain = dom
globalConfig.Lock()
globalConfig.library = lib
globalConfig.language = SimplifiedLocale(lang)
globalConfig.domain = dom
globalConfig.Unlock()
loadStorage(true)
}
// Get uses the default domain globally set to return the corresponding translation of a given string.
// Get uses the default domain globally set to return the corresponding Translation of a given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func Get(str string, vars ...interface{}) string {
return GetD(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.
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.
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.
func GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Try to load default package Locale storage
loadStorage(false)
// Return translation
return storage.GetND(dom, str, plural, n, vars...)
// Return Translation
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.
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.
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.
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.
func GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Try to load default package Locale storage
loadStorage(false)
// Return translation
return storage.GetNDC(dom, str, plural, n, ctx, vars...)
// Return Translation
globalConfig.RLock()
tr := globalConfig.storage.GetNDC(dom, str, plural, n, ctx, vars...)
globalConfig.RUnlock()
return tr
}

View File

@@ -2,7 +2,7 @@ package gotext
import (
"os"
"path"
"path/filepath"
"sync"
"testing"
)
@@ -65,14 +65,14 @@ msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"
msgid "Some random"
msgstr "Some random translation"
msgstr "Some random Translation"
msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgstr "Some random Translation in a context"
msgid "More"
msgstr "More translation"
msgstr "More Translation"
msgid "Untranslated"
msgid_plural "Several untranslated"
@@ -81,27 +81,58 @@ 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
dirname := path.Clean("/tmp" + string(os.PathSeparator) + "en_US")
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 := path.Clean(dirname + string(os.PathSeparator) + "default.po")
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())
}
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
Configure("/tmp", "en_US", "default")
@@ -125,8 +156,8 @@ msgstr[1] ""
// Test context translations
tr = GetC("Some random in a context", "Ctx")
if tr != "Some random translation in a context" {
t.Errorf("Expected 'Some random translation in a context' but got '%s'", tr)
if tr != "Some random Translation in a context" {
t.Errorf("Expected 'Some random Translation in a context' but got '%s'", tr)
}
v = "Variable"
@@ -139,6 +170,11 @@ msgstr[1] ""
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)
}
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) {
@@ -161,14 +197,14 @@ msgstr[1] ""
`
// 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)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
// Write PO content to default domain file
filename := path.Clean(dirname + string(os.PathSeparator) + "default.po")
filename := filepath.Join(dirname, "default.po")
f, err := os.Create(filename)
if err != nil {
@@ -214,6 +250,140 @@ msgstr[1] ""
}
}
func TestMoAndPoTranslator(t *testing.T) {
fixPath, _ := filepath.Abs("./fixtures/")
Configure(fixPath, "en_GB", "default")
// Check default domain Translation
SetDomain("default")
tr := Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text'. Got '%s'", tr)
}
tr = Get("language")
if tr != "en_GB" {
t.Errorf("Expected 'en_GB'. Got '%s'", tr)
}
// Change Language (locale)
SetLanguage("en_AU")
// Check default domain Translation
SetDomain("default")
tr = Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text'. Got '%s'", tr)
}
tr = Get("language")
if tr != "en_AU" {
t.Errorf("Expected 'en_AU'. Got '%s'", tr)
}
}
func TestDomains(t *testing.T) {
// Set PO content
strDefault := `
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) {
// Set PO content
str := `# Some comment
@@ -230,17 +400,21 @@ 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"
msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random Translation in a context"
`
// 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)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
// Write PO content to default domain file
filename := path.Clean(dirname + string(os.PathSeparator) + domain + ".po")
filename := filepath.Join("/tmp", GetDomain()+".po")
f, err := os.Create(filename)
if err != nil {
@@ -255,27 +429,82 @@ msgstr[2] "And this is the second plural form: %s"
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
for i := 0; i < 1000; i++ {
wg.Add(1)
// Test translations
go func() {
defer wg.Done()
GetLibrary()
SetLibrary(filepath.Join("/tmp", "gotextlib"))
GetDomain()
SetDomain("default")
GetLanguage()
SetLanguage("en_US")
Configure("/tmp", "en_US", "default")
Get("My text")
GetN("One with var: %s", "Several with vars: %s", 0, "test")
GetC("Some random in a context", "Ctx")
}()
wg.Add(1)
go func() {
defer wg.Done()
Get("My text")
GetN("One with var: %s", "Several with vars: %s", 1, "test")
}()
Get("My text")
GetN("One with var: %s", "Several with vars: %s", 2, "test")
}
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
View 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
View 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)
}
}

380
locale.go
View File

@@ -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
import (
"fmt"
"bytes"
"encoding/gob"
"os"
"path"
"path/filepath"
"sync"
)
@@ -15,27 +21,28 @@ multiple languages at the same time by working with this object.
Example:
import (
"fmt"
"github.com/leonelquinteros/gotext"
"encoding/gob"
"bytes"
"fmt"
"git.deineagentur.com/DeineAgenturUG/gotext"
)
func main() {
// Create Locale with library path and language code
l := gotext.NewLocale("/path/to/i18n/dir", "en_US")
// Load domain '/path/to/i18n/dir/en_US/LC_MESSAGES/default.po'
// Load domain '/path/to/i18n/dir/en_US/LC_MESSAGES/default.{po,mo}'
l.AddDomain("default")
// Translate text from default domain
fmt.Println(l.Get("Translate this"))
// Load different domain ('/path/to/i18n/dir/en_US/LC_MESSAGES/extras.po')
// Load different domain ('/path/to/i18n/dir/en_US/LC_MESSAGES/extras.{po,mo}')
l.AddDomain("extras")
// Translate text from domain
fmt.Println(l.GetD("extras", "Translate this"))
}
fmt.Println(l.GetD("extras", "Translate this"))
}
*/
type Locale struct {
// Path to locale files.
@@ -44,136 +51,403 @@ type Locale struct {
// Language for this Locale
lang string
// List of available domains for this locale.
domains map[string]*Po
// List of available Domains for this locale.
Domains map[string]Translator
// First AddDomain is default Domain
defaultDomain string
// Sync Mutex
sync.RWMutex
}
// 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 {
return &Locale{
path: p,
lang: l,
domains: make(map[string]*Po),
lang: SimplifiedLocale(l),
Domains: make(map[string]Translator),
}
}
func (l *Locale) findPO(dom string) string {
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+".po")
//SetLang ...
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 {
return filename
}
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 {
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 {
return filename
}
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.
// If the domain exists, it gets reloaded.
func (l *Locale) AddDomain(dom string) {
po := new(Po)
var poObj Translator
// Parse file.
po.ParseFile(l.findPO(dom))
file := l.findExt(dom, "po")
if file != "" {
poObj = new(Po)
// Parse file.
poObj.ParseFile(file)
} else {
file = l.findExt(dom, "mo")
if file != "" {
poObj = new(Mo)
// Parse file.
poObj.ParseFile(file)
} else {
// fallback return if no file found with
return
}
}
// Save new domain
l.Lock()
defer l.Unlock()
if l.domains == nil {
l.domains = make(map[string]*Po)
if l.Domains == nil {
l.Domains = make(map[string]Translator)
}
l.domains[dom] = po
if l.defaultDomain == "" {
l.defaultDomain = dom
}
l.Domains[dom] = poObj
// Unlock "Save new domain"
l.Unlock()
}
// Get uses a domain "default" to return the corresponding translation of a given string.
// 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.
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.
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.
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.
func (l *Locale) GetND(dom, str, plural string, n int, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.domains != nil {
if _, ok := l.domains[dom]; ok {
if l.domains[dom] != nil {
return l.domains[dom].GetN(str, plural, n, vars...)
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetN(str, plural, n, vars...)
}
}
}
// Return the same we received by default
return fmt.Sprintf(plural, vars...)
// Use western default rule (plural > 1) to handle missing domain default result.
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.
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.
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.
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.
func (l *Locale) GetNDC(dom, str, plural string, n int, ctx string, vars ...interface{}) string {
// Sync read
l.RLock()
defer l.RUnlock()
if l.domains != nil {
if _, ok := l.domains[dom]; ok {
if l.domains[dom] != nil {
return l.domains[dom].GetNC(str, plural, n, ctx, vars...)
if l.Domains != nil {
if _, ok := l.Domains[dom]; ok {
if l.Domains[dom] != nil {
return l.Domains[dom].GetNC(str, plural, n, ctx, vars...)
}
}
}
// Return the same we received by default
return fmt.Sprintf(plural, vars...)
// Use western default rule (plural > 1) to handle missing domain default result.
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
}

View File

@@ -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
import (
"os"
"path"
"path/filepath"
"testing"
)
@@ -45,26 +50,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"
msgid "Some random"
msgstr "Some random translation"
msgstr "Some random Translation"
msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgstr "Some random Translation in a context"
msgid "More"
msgstr "More translation"
msgstr "More Translation"
`
// 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)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
// Write PO content to file
filename := path.Join(dirname, "my_domain.po")
filename := filepath.Join(dirname, "my_domain.po")
f, err := os.Create(filename)
if err != nil {
@@ -81,7 +86,7 @@ msgstr "More translation"
l := NewLocale("/tmp", "en_US")
// Force nil domain storage
l.domains = nil
l.Domains = nil
// Add domain
l.AddDomain("my_domain")
@@ -105,7 +110,17 @@ msgstr "More translation"
}
// Test context translations
tr = l.GetC("Some random in a context", "Ctx")
if tr != "Some random Translation in a context" {
t.Errorf("Expected 'Some random Translation in a context'. Got '%s'", tr)
}
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)
@@ -117,10 +132,10 @@ msgstr "More translation"
t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr)
}
// Test last translation
// Test last Translation
tr = l.GetD("my_domain", "More")
if tr != "More translation" {
t.Errorf("Expected 'More translation' but got '%s'", tr)
if tr != "More Translation" {
t.Errorf("Expected 'More Translation' but got '%s'", tr)
}
}
@@ -165,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"
msgid "Some random"
msgstr "Some random translation"
msgstr "Some random Translation"
msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgstr "Some random Translation in a context"
msgid "More"
msgstr "More translation"
msgstr "More Translation"
`
// 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)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
// Write PO content to file
filename := path.Join(dirname, "my_domain.po")
filename := filepath.Join(dirname, "my_domain.po")
f, err := os.Create(filename)
if err != nil {
@@ -201,13 +216,28 @@ msgstr "More translation"
l := NewLocale("/tmp", "en_US")
// Force nil domain storage
l.domains = nil
l.Domains = nil
// Add domain
l.AddDomain("my_domain")
// Test non-existent "deafult" domain responses
tr := l.Get("My text")
// Test non-existent "default" domain responses
tr := l.GetDomain()
if tr != "my_domain" {
t.Errorf("Expected 'my_domain' but got '%s'", tr)
}
// Set default domain to make it fail
l.SetDomain("default")
// Test non-existent "default" domain responses
tr = l.GetDomain()
if tr != "default" {
t.Errorf("Expected 'default' but got '%s'", tr)
}
// Test non-existent "default" domain responses
tr = l.Get("My text")
if tr != "My text" {
t.Errorf("Expected 'My text' but got '%s'", tr)
}
@@ -225,6 +255,11 @@ msgstr "More translation"
}
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" {
t.Errorf("Expected 'This are tests' but got '%s'", tr)
}
@@ -236,8 +271,138 @@ msgstr "More translation"
}
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 'Plural index' but got '%s'", tr)
t.Errorf("Expected 'This are tests' but got '%s'", tr)
}
// Create Locale with full language code
l = NewLocale("/tmp", "golem")
// Force nil domain storage
l.Domains = nil
// Add domain
l.SetDomain("my_domain")
// Test non-existent "default" domain responses
tr = l.GetDomain()
if tr != "my_domain" {
t.Errorf("Expected 'my_domain' but got '%s'", tr)
}
// Test syntax error parsed translations
tr = l.Get("This one has invalid syntax translations")
if tr != "This one has invalid syntax translations" {
t.Errorf("Expected 'This one has invalid syntax translations' but got '%s'", tr)
}
tr = l.GetN("This one has invalid syntax translations", "This are tests", 1)
if tr != "This 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", 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)
}
}
@@ -260,14 +425,14 @@ msgstr[2] "And this is the second plural form: %s"
`
// Create Locales directory with simplified language code
dirname := path.Join("/tmp", "es")
dirname := filepath.Join("/tmp", "es")
err := os.MkdirAll(dirname, os.ModePerm)
if err != nil {
t.Fatalf("Can't create test directory: %s", err.Error())
}
// Write PO content to file
filename := path.Join(dirname, "race.po")
filename := filepath.Join(dirname, "race.po")
f, err := os.Create(filename)
if err != nil {
@@ -280,7 +445,7 @@ msgstr[2] "And this is the second plural form: %s"
t.Fatalf("Can't write to test file: %s", err.Error())
}
// Create Locale with full language code
// Create Locale
l := NewLocale("/tmp", "es")
// Init sync channels
@@ -306,3 +471,133 @@ msgstr[2] "And this is the second plural form: %s"
<-ac
<-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
View 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
View 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
View 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
View 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
View 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
View 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

File diff suppressed because one or more lines are too long

104
plurals/tests.go Normal file
View 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))
}

340
po.go
View File

@@ -1,61 +1,26 @@
/*
* 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"
"fmt"
"github.com/mattn/kinako/vm"
"bytes"
"encoding/gob"
"io/ioutil"
"net/textproto"
"os"
"strconv"
"strings"
"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 {
if t.trs[0] != "" {
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 {
if t.trs[n] != "" {
return t.trs[n]
}
}
// Return unstranlated singular if corresponding
if n == 0 {
return t.id
}
// Return untranslated plural by default
return t.pluralID
}
/*
Po parses the content of any PO file and provides all the translation functions needed.
Po parses the content of any PO file and provides all the Translation functions needed.
It's the base object used by all package methods.
And it's safe for concurrent use by multiple goroutines by using the sync package for locking.
@@ -63,17 +28,17 @@ Example:
import (
"fmt"
"github.com/leonelquinteros/gotext"
"git.deineagentur.com/DeineAgenturUG/gotext"
)
func main() {
// Create po object
po := new(gotext.Po)
po := gotext.NewPoTranslator()
// Parse .po file
po.ParseFile("/path/to/po/file/translations.po")
// Get translation
// Get Translation
fmt.Println(po.Get("Translate this"))
}
@@ -89,18 +54,19 @@ type Po struct {
PluralForms string
// Parsed Plural-Forms header values
nplurals int
plural string
nplurals int
plural string
pluralforms plurals.Expression
// Storage
translations map[string]*translation
contexts map[string]map[string]*translation
translations map[string]*Translation
contexts map[string]map[string]*Translation
// Sync Mutex
sync.RWMutex
// Parsing buffers
trBuffer *translation
trBuffer *Translation
ctxBuffer string
}
@@ -114,6 +80,11 @@ const (
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.
func (po *Po) ParseFile(f string) {
// Check if file exists
@@ -133,25 +104,25 @@ func (po *Po) ParseFile(f string) {
return
}
po.Parse(string(data))
po.Parse(data)
}
// Parse loads the translations specified in the provided string (str)
func (po *Po) Parse(str string) {
func (po *Po) Parse(buf []byte) {
// Lock while parsing
po.Lock()
// Init storage
if po.translations == nil {
po.translations = make(map[string]*translation)
po.contexts = make(map[string]map[string]*translation)
po.translations = make(map[string]*Translation)
po.contexts = make(map[string]map[string]*Translation)
}
// Get lines
lines := strings.Split(str, "\n")
lines := strings.Split(string(buf), "\n")
// Init buffer
po.trBuffer = newTranslation()
po.trBuffer = NewTranslation()
po.ctxBuffer = ""
state := head
@@ -185,7 +156,7 @@ func (po *Po) Parse(str string) {
continue
}
// Save translation
// Save Translation
if strings.HasPrefix(l, "msgstr") {
po.parseMessage(l)
state = msgStr
@@ -199,7 +170,7 @@ func (po *Po) Parse(str string) {
}
}
// Save last translation buffer.
// Save last Translation buffer.
po.saveBuffer()
// Unlock to parse headers
@@ -209,33 +180,33 @@ func (po *Po) Parse(str string) {
po.parseHeaders()
}
// saveBuffer takes the context and translation buffers
// saveBuffer takes the context and Translation buffers
// and saves it on the translations collection
func (po *Po) saveBuffer() {
// With no context...
if po.ctxBuffer == "" {
po.translations[po.trBuffer.id] = po.trBuffer
po.translations[po.trBuffer.ID] = po.trBuffer
} else {
// With context...
if _, ok := po.contexts[po.ctxBuffer]; !ok {
po.contexts[po.ctxBuffer] = make(map[string]*translation)
po.contexts[po.ctxBuffer] = make(map[string]*Translation)
}
po.contexts[po.ctxBuffer][po.trBuffer.id] = po.trBuffer
po.contexts[po.ctxBuffer][po.trBuffer.ID] = po.trBuffer
// Cleanup current context buffer if needed
if po.trBuffer.id != "" {
if po.trBuffer.ID != "" {
po.ctxBuffer = ""
}
}
// Flush translation buffer
po.trBuffer = newTranslation()
// Flush Translation buffer
po.trBuffer = NewTranslation()
}
// parseContext takes a line starting with "msgctxt",
// saves the current translation buffer and creates a new context.
// saves the current Translation buffer and creates a new context.
func (po *Po) parseContext(l string) {
// Save current translation buffer.
// Save current Translation buffer.
po.saveBuffer()
// Buffer context
@@ -243,25 +214,25 @@ func (po *Po) parseContext(l string) {
}
// parseID takes a line starting with "msgid",
// saves the current translation and creates a new msgid buffer.
// saves the current Translation and creates a new msgid buffer.
func (po *Po) parseID(l string) {
// Save current translation buffer.
// Save current Translation buffer.
po.saveBuffer()
// Set id
po.trBuffer.id, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid")))
po.trBuffer.ID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid")))
}
// parsePluralID saves the plural id buffer from a line starting with "msgid_plural"
func (po *Po) parsePluralID(l string) {
po.trBuffer.pluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural")))
po.trBuffer.PluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural")))
}
// parseMessage takes a line starting with "msgstr" and saves it into the current buffer.
func (po *Po) parseMessage(l string) {
l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr"))
// Check for indexed translation forms
// Check for indexed Translation forms
if strings.HasPrefix(l, "[") {
idx := strings.Index(l, "]")
if idx == -1 {
@@ -276,15 +247,15 @@ func (po *Po) parseMessage(l string) {
return
}
// Parse translation string
po.trBuffer.trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:]))
// Parse Translation string
po.trBuffer.Trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:]))
// Loop
return
}
// Save single translation form under 0 index
po.trBuffer.trs[0], _ = strconv.Unquote(l)
// Save single Translation form under 0 index
po.trBuffer.Trs[0], _ = strconv.Unquote(l)
}
// parseString takes a well formatted string without prefix
@@ -294,16 +265,16 @@ func (po *Po) parseString(l string, state parseState) {
switch state {
case msgStr:
// Append to last translation found
po.trBuffer.trs[len(po.trBuffer.trs)-1] += clean
// Append to last Translation found
po.trBuffer.Trs[len(po.trBuffer.Trs)-1] += clean
case msgID:
// Multiline msgid - Append to current id
po.trBuffer.id += clean
po.trBuffer.ID += clean
case msgIDPlural:
// Multiline msgid - Append to current id
po.trBuffer.pluralID += clean
po.trBuffer.PluralID += clean
case msgCtxt:
// Multiline context - Append to current context
@@ -377,6 +348,11 @@ func (po *Po) parseHeaders() {
case "plural":
po.plural = vs[1]
if expr, err := plurals.Compile(po.plural); err == nil {
po.pluralforms = expr
}
}
}
}
@@ -387,38 +363,18 @@ func (po *Po) pluralForm(n int) int {
po.RLock()
defer po.RUnlock()
// Failsafe
if po.nplurals < 1 {
return 0
}
if po.plural == "" {
return 0
}
// Init compiler
env := vm.NewEnv()
env.Define("n", n)
plural, err := env.Execute(po.plural)
if err != nil {
return 0
}
if plural.Type().Name() == "bool" {
if plural.Bool() {
return 1
// Failure fallback
if po.pluralforms == nil {
/* Use Western plural rule. */
if n == 1 {
return 0
}
// Else
return 0
return 1
}
if int(plural.Int()) > po.nplurals {
return 0
}
return int(plural.Int())
return po.pluralforms.Eval(uint32(n))
}
// Get retrieves the corresponding translation for the given string.
// Get retrieves the corresponding Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) Get(str string, vars ...interface{}) string {
// Sync read
@@ -427,15 +383,15 @@ func (po *Po) Get(str string, vars ...interface{}) string {
if po.translations != nil {
if _, ok := po.translations[str]; ok {
return po.printf(po.translations[str].get(), vars...)
return Printf(po.translations[str].Get(), vars...)
}
}
// Return the same we received by default
return po.printf(str, vars...)
return Printf(str, vars...)
}
// GetN retrieves the (N)th plural form of translation for the given string.
// GetN retrieves the (N)th plural form of Translation for the given string.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
// Sync read
@@ -444,17 +400,18 @@ func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
if po.translations != nil {
if _, ok := po.translations[str]; ok {
return po.printf(po.translations[str].getN(po.pluralForm(n)), vars...)
return Printf(po.translations[str].GetN(po.pluralForm(n)), vars...)
}
}
if n == 1 {
return po.printf(str, vars...)
// Parse plural forms to distinguish between plural and singular
if po.pluralForm(n) == 0 {
return Printf(str, vars...)
}
return po.printf(plural, vars...)
return Printf(plural, vars...)
}
// GetC retrieves the corresponding translation for a given string in the given context.
// GetC retrieves the corresponding Translation for a given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
// Sync read
@@ -465,17 +422,17 @@ func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
if _, ok := po.contexts[ctx]; ok {
if po.contexts[ctx] != nil {
if _, ok := po.contexts[ctx][str]; ok {
return po.printf(po.contexts[ctx][str].get(), vars...)
return Printf(po.contexts[ctx][str].Get(), vars...)
}
}
}
}
// Return the string we received by default
return po.printf(str, vars...)
return Printf(str, vars...)
}
// GetNC retrieves the (N)th plural form of translation for the given string in the given context.
// GetNC retrieves the (N)th plural form of Translation for the given string in the given context.
// Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax.
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
// Sync read
@@ -486,23 +443,144 @@ func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{})
if _, ok := po.contexts[ctx]; ok {
if po.contexts[ctx] != nil {
if _, ok := po.contexts[ctx][str]; ok {
return po.printf(po.contexts[ctx][str].getN(po.pluralForm(n)), vars...)
return Printf(po.contexts[ctx][str].GetN(po.pluralForm(n)), vars...)
}
}
}
}
if n == 1 {
return po.printf(str, vars...)
// Parse plural forms to distinguish between plural and singular
if po.pluralForm(n) == 0 {
return Printf(str, vars...)
}
return po.printf(plural, vars...)
return Printf(plural, vars...)
}
// printf applies text formatting only when needed to parse variables.
func (po *Po) printf(str string, vars ...interface{}) string {
if len(vars) > 0 {
return fmt.Sprintf(str, 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 str
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
}

View File

@@ -1,3 +1,8 @@
/*
* Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com. All rights reserved.
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/
package gotext
import (
@@ -6,6 +11,28 @@ import (
"testing"
)
func TestPo_Get(t *testing.T) {
// Create po object
po := new(Po)
// Try to parse a directory
po.ParseFile(path.Clean(os.TempDir()))
// Parse file
po.ParseFile("fixtures/en_US/default.po")
// Test translations
tr := po.Get("My text")
if tr != "Translated text" {
t.Errorf("Expected 'Translated text' but got '%s'", tr)
}
// Test translations
tr = po.Get("language")
if tr != "en_US" {
t.Errorf("Expected 'en_US' but got '%s'", tr)
}
}
func TestPo(t *testing.T) {
// Set PO content
str := `
@@ -28,13 +55,15 @@ msgid "Another string"
msgstr ""
# Multi-line msgid
msgid "multi"
msgid ""
"multi"
"line"
"id"
msgstr "id with multiline content"
# Multi-line msgid_plural
msgid "multi"
msgid ""
"multi"
"line"
"plural"
"id"
@@ -42,7 +71,8 @@ msgstr "plural id with multiline content"
#Multi-line string
msgid "Multi-line"
msgstr "Multi "
msgstr ""
"Multi "
"line"
msgid "One with var: %s"
@@ -58,13 +88,13 @@ msgstr[0] "This one is the singular in a Ctx context: %s"
msgstr[1] "This one is the plural in a Ctx context: %s"
msgid "Some random"
msgstr "Some random translation"
msgstr "Some random Translation"
msgctxt "Ctx"
msgid "Some random in a context"
msgstr "Some random translation in a context"
msgstr "Some random Translation in a context"
msgid "Empty translation"
msgid "Empty Translation"
msgstr ""
msgid "Empty plural form singular"
@@ -73,7 +103,7 @@ msgstr[0] "Singular translated"
msgstr[1] ""
msgid "More"
msgstr "More translation"
msgstr "More Translation"
`
@@ -136,7 +166,7 @@ msgstr "More translation"
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")
if tr != "This is a test" {
t.Errorf("Expected 'This is a test' but got '%s'", tr)
@@ -147,6 +177,22 @@ msgstr "More translation"
t.Errorf("Expected 'This are tests' but got '%s'", tr)
}
// Test translations with existence check
tr, exists := po.GetE("My text")
if (tr != "Translated text") || (!exists) {
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("I don't exist", "We don't exist", 100)
if exists {
t.Errorf("Expected 'I/We don't exist' not to exist but got '%s'", tr)
}
// Test context translations
v = "Test"
tr = po.GetC("One with var: %s", "Ctx", v)
@@ -170,10 +216,10 @@ msgstr "More translation"
t.Errorf("Expected 'Original' but got '%s'", tr)
}
// Test empty translation strings
tr = po.Get("Empty translation")
if tr != "Empty translation" {
t.Errorf("Expected 'Empty translation' but got '%s'", tr)
// Test empty Translation strings
tr = po.Get("Empty Translation")
if tr != "Empty Translation" {
t.Errorf("Expected 'Empty Translation' but got '%s'", tr)
}
tr = po.Get("Empty plural form singular")
@@ -191,10 +237,10 @@ msgstr "More translation"
t.Errorf("Expected 'Empty plural form' but got '%s'", tr)
}
// Test last translation
// Test last Translation
tr = po.Get("More")
if tr != "More translation" {
t.Errorf("Expected 'More translation' but got '%s'", tr)
if tr != "More Translation" {
t.Errorf("Expected 'More Translation' but got '%s'", tr)
}
}
@@ -211,11 +257,41 @@ msgstr[0] "TR Singular: %s"
msgstr[1] "TR Plural: %s"
msgstr[2] "TR Plural 2: %s"
`
// Create po object
po := new(Po)
po.Parse(str)
po.Parse([]byte(str))
v := "Var"
tr := po.GetN("Singular: %s", "Plural: %s", 2, v)
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)
@@ -250,7 +326,7 @@ msgstr "Translated example"
po := new(Po)
// Parse
po.Parse(str)
po.Parse([]byte(str))
// Check headers expected
if po.Language != "en" {
@@ -274,9 +350,9 @@ msgstr "Translated example"
po := new(Po)
// Parse
po.Parse(str)
po.Parse([]byte(str))
// Check translation expected
// Check Translation expected
if po.Get("Example") != "Translated example" {
t.Errorf("Expected 'Translated example' but got '%s'", po.Get("Example"))
}
@@ -302,7 +378,7 @@ msgstr[3] "Plural form 3"
po := new(Po)
// Parse
po.Parse(str)
po.Parse([]byte(str))
// Check plural form
n := po.pluralForm(0)
@@ -347,7 +423,7 @@ msgstr[3] "Plural form 3"
po := new(Po)
// Parse
po.Parse(str)
po.Parse([]byte(str))
// Check plural form
n := po.pluralForm(0)
@@ -388,7 +464,7 @@ msgstr[3] "Plural form 3"
po := new(Po)
// Parse
po.Parse(str)
po.Parse([]byte(str))
// Check plural form
n := po.pluralForm(0)
@@ -438,7 +514,7 @@ msgstr[3] "Plural form 3"
po := new(Po)
// Parse
po.Parse(str)
po.Parse([]byte(str))
// Check plural form
n := po.pluralForm(1)
@@ -464,19 +540,18 @@ msgstr[3] "Plural form 3"
}
func TestTranslationObject(t *testing.T) {
tr := newTranslation()
str := tr.get()
tr := NewTranslation()
str := tr.Get()
if str != "" {
t.Errorf("Expected '' but got '%s'", str)
}
// Set id
tr.id = "Text"
tr.ID = "Text"
str = tr.Get()
// Get again
str = tr.get()
if str != "Text" {
t.Errorf("Expected 'Text' but got '%s'", str)
}
@@ -509,11 +584,11 @@ msgstr[2] "And this is the second plural form: %s"
// Parse po content in a goroutine
go func(po *Po, done chan bool) {
po.Parse(str)
po.Parse([]byte(str))
done <- true
}(po, pc)
// Read some translation on a goroutine
// Read some Translation on a goroutine
go func(po *Po, done chan bool) {
po.Get("My text")
done <- true
@@ -526,3 +601,62 @@ msgstr[2] "And this is the second plural form: %s"
<-pc
<-rc
}
func TestNewPoTranslatorRace(t *testing.T) {
// Create Po object
mo := NewPoTranslator()
// Create sync channels
pc := make(chan bool)
rc := make(chan bool)
// Parse po content in a goroutine
go func(mo Translator, done chan bool) {
// Parse file
mo.ParseFile("fixtures/en_US/default.po")
done <- true
}(mo, pc)
// Read some Translation on a goroutine
go func(mo Translator, done chan bool) {
mo.Get("My text")
done <- true
}(mo, rc)
// Read something at top level
mo.Get("My text")
// Wait for goroutines to finish
<-pc
<-rc
}
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
View 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
View 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
}

View File

@@ -1,37 +0,0 @@
# kinako
Kinako is small VM written in Go.
![](https://raw.githubusercontent.com/mattn/kinako/master/kinako.png)
(Picture licensed under CC BY-SA 3.0 by wikipedia)
## Installation
Requires Go.
```
$ go get -u github.com/mattn/kinako
```
## Usage
Embedding the interpreter into your own program:
```Go
var env = vm.NewEnv()
env.Define("foo", 1)
val, err := env.Execute(`foo + 3`)
if err != nil {
panic(err)
}
fmt.Println(val)
```
# License
MIT
# Author
Yasuhiro Matsumoto (a.k.a mattn)

View File

@@ -1,18 +0,0 @@
package main
import (
"fmt"
"log"
"github.com/mattn/kinako/vm"
)
func main() {
env := vm.NewEnv()
v, err := env.Execute(`foo=1; foo+3`)
if err != nil {
log.Fatal(err)
}
fmt.Println(v)
}

View File

@@ -1,112 +0,0 @@
package ast
type Token struct {
Tok int
Lit string
}
// Position provides interface to store code locations.
type Position struct {
Line int
Column int
}
// Expr provides all of interfaces for expression.
type Expr interface {
expr()
}
// ExprImpl provide commonly implementations for Expr.
type ExprImpl struct {
}
// expr provide restraint interface.
func (x *ExprImpl) expr() {}
// NumberExpr provide Number expression.
type NumberExpr struct {
ExprImpl
Lit string
}
// UnaryExpr provide unary minus expression. ex: -1, ^1, ~1.
type UnaryExpr struct {
ExprImpl
Operator string
Expr Expr
}
// IdentExpr provide identity expression.
type IdentExpr struct {
ExprImpl
Lit string
}
// Stmt provides all of interfaces for statement.
type Stmt interface {
stmt()
}
// StmtImpl provide commonly implementations for Stmt..
type StmtImpl struct {
}
// stmt provide restraint interface.
func (x *StmtImpl) stmt() {}
// LetsStmt provide multiple statement of let.
type LetsStmt struct {
StmtImpl
Lhss []Expr
Operator string
Rhss []Expr
}
// StringExpr provide String expression.
type StringExpr struct {
ExprImpl
Lit string
}
type TernaryOpExpr struct {
ExprImpl
Expr Expr
Lhs Expr
Rhs Expr
}
// CallExpr provide calling expression.
type CallExpr struct {
ExprImpl
Func interface{}
Name string
SubExprs []Expr
}
// ParenExpr provide parent block expression.
type ParenExpr struct {
ExprImpl
SubExpr Expr
}
// BinOpExpr provide binary operator expression.
type BinOpExpr struct {
ExprImpl
Lhs Expr
Operator string
Rhs Expr
}
// ExprStmt provide expression statement.
type ExprStmt struct {
StmtImpl
Expr Expr
}
// LetStmt provide statement of let.
type LetStmt struct {
StmtImpl
Lhs Expr
Operator string
Rhs Expr
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 450 KiB

View File

@@ -1,4 +0,0 @@
all : parser.go
parser.go : parser.go.y
goyacc -o $@ parser.go.y

View File

@@ -1,427 +0,0 @@
package parser
import (
"errors"
"fmt"
"unicode"
"github.com/mattn/kinako/ast"
)
const (
EOF = -1 // End of file.
EOL = '\n' // End of line.
)
// Error provides a convenient interface for handling runtime error.
// It can be Error inteface with type cast which can call Pos().
type Error struct {
Message string
Filename string
Fatal bool
}
// Error returns the error message.
func (e *Error) Error() string {
return e.Message
}
// Scanner stores informations for lexer.
type Scanner struct {
src []rune
offset int
lineHead int
line int
}
// Init resets code to scan.
func (s *Scanner) Init(src string) {
s.src = []rune(src)
}
// Scan analyses token, and decide identify or literals.
func (s *Scanner) Scan() (tok int, lit string, pos ast.Position, err error) {
retry:
s.skipBlank()
pos = s.pos()
switch ch := s.peek(); {
case isLetter(ch):
tok = IDENT
lit, err = s.scanIdentifier()
if err != nil {
return
}
case isDigit(ch):
tok = NUMBER
lit, err = s.scanNumber()
if err != nil {
return
}
case ch == '"':
tok = STRING
lit, err = s.scanString('"')
if err != nil {
return
}
case ch == '\'':
tok = STRING
lit, err = s.scanString('\'')
if err != nil {
return
}
case ch == '`':
tok = STRING
lit, err = s.scanRawString()
if err != nil {
return
}
default:
switch ch {
case EOF:
tok = EOF
case '#':
for !isEOL(s.peek()) {
s.next()
}
goto retry
case '!':
s.next()
switch s.peek() {
case '=':
tok = NEQ
lit = "!="
default:
s.back()
tok = int(ch)
lit = string(ch)
}
case '=':
s.next()
switch s.peek() {
case '=':
tok = EQEQ
lit = "=="
default:
s.back()
tok = int(ch)
lit = string(ch)
}
case '+':
tok = int(ch)
lit = string(ch)
case '-':
tok = int(ch)
lit = string(ch)
case '*':
tok = int(ch)
lit = string(ch)
case '/':
tok = int(ch)
lit = string(ch)
case '>':
s.next()
switch s.peek() {
case '=':
tok = GE
lit = ">="
case '>':
tok = SHIFTRIGHT
lit = ">>"
default:
s.back()
tok = int(ch)
lit = string(ch)
}
case '<':
s.next()
switch s.peek() {
case '=':
tok = LE
lit = "<="
case '<':
tok = SHIFTLEFT
lit = "<<"
default:
s.back()
tok = int(ch)
lit = string(ch)
}
case '|':
s.next()
switch s.peek() {
case '|':
tok = OROR
lit = "||"
default:
s.back()
tok = int(ch)
lit = string(ch)
}
case '&':
s.next()
switch s.peek() {
case '&':
tok = ANDAND
lit = "&&"
default:
s.back()
tok = int(ch)
lit = string(ch)
}
case '.':
tok = int(ch)
lit = string(ch)
case '\n':
tok = int(ch)
lit = string(ch)
case '(', ')', ':', ';', '%', '?', '{', '}', ',', '[', ']', '^':
tok = int(ch)
lit = string(ch)
default:
err = fmt.Errorf(`syntax error "%s"`, string(ch))
tok = int(ch)
lit = string(ch)
return
}
s.next()
}
return
}
// isLetter returns true if the rune is a letter for identity.
func isLetter(ch rune) bool {
return unicode.IsLetter(ch) || ch == '_'
}
// isDigit returns true if the rune is a number.
func isDigit(ch rune) bool {
return '0' <= ch && ch <= '9'
}
// isHex returns true if the rune is a hex digits.
func isHex(ch rune) bool {
return ('0' <= ch && ch <= '9') || ('a' <= ch && ch <= 'f') || ('A' <= ch && ch <= 'F')
}
// isEOL returns true if the rune is at end-of-line or end-of-file.
func isEOL(ch rune) bool {
return ch == '\n' || ch == -1
}
// isBlank returns true if the rune is empty character..
func isBlank(ch rune) bool {
return ch == ' ' || ch == '\t' || ch == '\r'
}
// peek returns current rune in the code.
func (s *Scanner) peek() rune {
if s.reachEOF() {
return EOF
}
return s.src[s.offset]
}
// next moves offset to next.
func (s *Scanner) next() {
if !s.reachEOF() {
if s.peek() == '\n' {
s.lineHead = s.offset + 1
s.line++
}
s.offset++
}
}
// current returns the current offset.
func (s *Scanner) current() int {
return s.offset
}
// offset sets the offset value.
func (s *Scanner) set(o int) {
s.offset = o
}
// back moves back offset once to top.
func (s *Scanner) back() {
s.offset--
}
// reachEOF returns true if offset is at end-of-file.
func (s *Scanner) reachEOF() bool {
return len(s.src) <= s.offset
}
// pos returns the position of current.
func (s *Scanner) pos() ast.Position {
return ast.Position{Line: s.line + 1, Column: s.offset - s.lineHead + 1}
}
// skipBlank moves position into non-black character.
func (s *Scanner) skipBlank() {
for isBlank(s.peek()) {
s.next()
}
}
// scanIdentifier returns identifier begining at current position.
func (s *Scanner) scanIdentifier() (string, error) {
var ret []rune
for {
if !isLetter(s.peek()) && !isDigit(s.peek()) {
break
}
ret = append(ret, s.peek())
s.next()
}
return string(ret), nil
}
// scanNumber returns number begining at current position.
func (s *Scanner) scanNumber() (string, error) {
var ret []rune
ch := s.peek()
ret = append(ret, ch)
s.next()
if ch == '0' && s.peek() == 'x' {
ret = append(ret, s.peek())
s.next()
for isHex(s.peek()) {
ret = append(ret, s.peek())
s.next()
}
} else {
for isDigit(s.peek()) || s.peek() == '.' {
ret = append(ret, s.peek())
s.next()
}
if s.peek() == 'e' {
ret = append(ret, s.peek())
s.next()
if isDigit(s.peek()) || s.peek() == '+' || s.peek() == '-' {
ret = append(ret, s.peek())
s.next()
for isDigit(s.peek()) || s.peek() == '.' {
ret = append(ret, s.peek())
s.next()
}
}
for isDigit(s.peek()) || s.peek() == '.' {
ret = append(ret, s.peek())
s.next()
}
}
if isLetter(s.peek()) {
return "", errors.New("identifier starts immediately after numeric literal")
}
}
return string(ret), nil
}
// scanRawString returns raw-string starting at current position.
func (s *Scanner) scanRawString() (string, error) {
var ret []rune
for {
s.next()
if s.peek() == EOF {
return "", errors.New("unexpected EOF")
break
}
if s.peek() == '`' {
s.next()
break
}
ret = append(ret, s.peek())
}
return string(ret), nil
}
// scanString returns string starting at current position.
// This handles backslash escaping.
func (s *Scanner) scanString(l rune) (string, error) {
var ret []rune
eos:
for {
s.next()
switch s.peek() {
case EOL:
return "", errors.New("unexpected EOL")
case EOF:
return "", errors.New("unexpected EOF")
case l:
s.next()
break eos
case '\\':
s.next()
switch s.peek() {
case 'b':
ret = append(ret, '\b')
continue
case 'f':
ret = append(ret, '\f')
continue
case 'r':
ret = append(ret, '\r')
continue
case 'n':
ret = append(ret, '\n')
continue
case 't':
ret = append(ret, '\t')
continue
}
ret = append(ret, s.peek())
continue
default:
ret = append(ret, s.peek())
}
}
return string(ret), nil
}
// Lexer provides inteface to parse codes.
type Lexer struct {
s *Scanner
lit string
pos ast.Position
e error
stmts []ast.Stmt
}
// Lex scans the token and literals.
func (l *Lexer) Lex(lval *yySymType) int {
tok, lit, pos, err := l.s.Scan()
if err != nil {
l.e = &Error{Message: fmt.Sprintf("%s", err.Error()), Fatal: true}
}
lval.tok = ast.Token{Tok: tok, Lit: lit}
l.lit = lit
l.pos = pos
return tok
}
// Error sets parse error.
func (l *Lexer) Error(msg string) {
l.e = &Error{Message: msg, Fatal: false}
}
// Parser provides way to parse the code using Scanner.
func Parse(s *Scanner) ([]ast.Stmt, error) {
l := Lexer{s: s}
if yyParse(&l) != 0 {
return nil, l.e
}
return l.stmts, l.e
}
func EnableErrorVerbose() {
yyErrorVerbose = true
}
// ParserSrc provides way to parse the code from source.
func ParseSrc(src string) ([]ast.Stmt, error) {
scanner := &Scanner{
src: []rune(src),
}
return Parse(scanner)
}

View File

@@ -1,778 +0,0 @@
//line parser.go.y:2
package parser
import __yyfmt__ "fmt"
//line parser.go.y:2
import (
"github.com/mattn/kinako/ast"
)
//line parser.go.y:16
type yySymType struct {
yys int
compstmt []ast.Stmt
stmts []ast.Stmt
stmt ast.Stmt
expr ast.Expr
exprs []ast.Expr
tok ast.Token
term ast.Token
terms ast.Token
opt_terms ast.Token
}
const IDENT = 57346
const NUMBER = 57347
const STRING = 57348
const EQEQ = 57349
const NEQ = 57350
const GE = 57351
const LE = 57352
const OROR = 57353
const ANDAND = 57354
const POW = 57355
const SHIFTLEFT = 57356
const SHIFTRIGHT = 57357
const PLUSPLUS = 57358
const MINUSMINUS = 57359
const UNARY = 57360
var yyToknames = [...]string{
"$end",
"error",
"$unk",
"IDENT",
"NUMBER",
"STRING",
"EQEQ",
"NEQ",
"GE",
"LE",
"OROR",
"ANDAND",
"POW",
"'='",
"'?'",
"':'",
"','",
"'>'",
"'<'",
"SHIFTLEFT",
"SHIFTRIGHT",
"'+'",
"'-'",
"PLUSPLUS",
"MINUSMINUS",
"'*'",
"'/'",
"'%'",
"UNARY",
"'!'",
"'^'",
"'('",
"')'",
"'|'",
"'&'",
"';'",
"'\\n'",
}
var yyStatenames = [...]string{}
const yyEofCode = 1
const yyErrCode = 2
const yyInitialStackSize = 16
//line parser.go.y:213
//line yacctab:1
var yyExca = [...]int{
-1, 1,
1, -1,
-2, 0,
-1, 50,
7, 0,
8, 0,
-2, 20,
-1, 51,
7, 0,
8, 0,
-2, 21,
}
const yyNprod = 40
const yyPrivate = 57344
var yyTokenNames []string
var yyStates []string
const yyLast = 251
var yyAct = [...]int{
9, 6, 7, 33, 35, 37, 22, 23, 60, 3,
24, 25, 26, 38, 39, 40, 1, 41, 33, 35,
8, 43, 44, 45, 46, 47, 48, 49, 50, 51,
52, 53, 54, 55, 56, 57, 58, 59, 61, 42,
27, 28, 30, 32, 34, 36, 65, 0, 21, 63,
4, 29, 31, 2, 18, 22, 23, 17, 0, 24,
25, 26, 64, 0, 66, 0, 67, 33, 35, 27,
28, 30, 32, 34, 36, 0, 0, 21, 0, 0,
29, 31, 0, 0, 22, 23, 0, 0, 24, 25,
26, 0, 0, 0, 0, 62, 33, 35, 27, 28,
30, 32, 34, 36, 0, 20, 21, 0, 0, 29,
31, 0, 0, 22, 23, 5, 0, 24, 25, 26,
19, 0, 0, 0, 0, 33, 35, 27, 28, 30,
32, 34, 36, 0, 19, 21, 0, 0, 29, 31,
0, 0, 22, 23, 0, 0, 24, 25, 26, 0,
0, 0, 0, 0, 33, 35, 27, 28, 30, 32,
0, 36, 0, 0, 0, 0, 0, 29, 31, 0,
0, 22, 23, 0, 0, 24, 25, 26, 27, 28,
30, 32, 0, 33, 35, 0, 0, 0, 0, 29,
31, 0, 0, 22, 23, 0, 0, 24, 25, 26,
30, 32, 10, 11, 15, 33, 35, 0, 0, 29,
31, 0, 0, 22, 23, 0, 0, 24, 25, 26,
0, 12, 10, 11, 15, 33, 35, 0, 13, 14,
16, 24, 25, 26, 6, 7, 0, 0, 0, 33,
35, 12, 0, 0, 0, 0, 0, 0, 13, 14,
16,
}
var yyPact = [...]int{
-35, -1000, 218, -35, -35, -1000, -1000, -1000, -1000, 91,
-27, -1000, 218, 218, 218, -1000, 218, -1000, 198, -1000,
218, 218, 218, 218, 218, 218, 218, 218, 218, 218,
218, 218, 218, 218, 218, 218, 218, 218, -31, -31,
-31, 62, -1000, 120, 33, 205, 205, -31, -31, -31,
191, 191, -16, -16, -16, -16, 120, 149, 120, 171,
29, 120, -1000, 218, -1000, 218, 120, 120,
}
var yyPgo = [...]int{
0, 16, 9, 20, 0, 8, 53, 50, 115,
}
var yyR1 = [...]int{
0, 1, 1, 2, 2, 3, 3, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 4, 4, 4, 4, 4, 4, 4, 4, 4,
4, 5, 5, 5, 6, 6, 7, 7, 8, 8,
}
var yyR2 = [...]int{
0, 1, 2, 2, 3, 3, 1, 1, 1, 2,
2, 2, 1, 5, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
4, 0, 1, 3, 0, 1, 1, 2, 1, 1,
}
var yyChk = [...]int{
-1000, -1, -6, -2, -7, -8, 36, 37, -3, -4,
4, 5, 23, 30, 31, 6, 32, -6, -7, -8,
14, 15, 22, 23, 26, 27, 28, 7, 8, 18,
9, 19, 10, 34, 11, 35, 12, 32, -4, -4,
-4, -4, -3, -4, -4, -4, -4, -4, -4, -4,
-4, -4, -4, -4, -4, -4, -4, -4, -4, -4,
-5, -4, 33, 16, 33, 17, -4, -4,
}
var yyDef = [...]int{
34, -2, 1, 34, 35, 36, 38, 39, 3, 6,
7, 8, 0, 0, 0, 12, 0, 2, 35, 37,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 31, 9, 10,
11, 0, 4, 5, 0, 15, 16, 17, 18, 19,
-2, -2, 22, 23, 24, 25, 26, 27, 28, 29,
0, 32, 14, 0, 30, 0, 13, 33,
}
var yyTok1 = [...]int{
1, 3, 3, 3, 3, 3, 3, 3, 3, 3,
37, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 30, 3, 3, 3, 28, 35, 3,
32, 33, 26, 22, 17, 23, 3, 27, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 16, 36,
19, 14, 18, 15, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 31, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 3, 3, 3, 3, 3, 3,
3, 3, 3, 3, 34,
}
var yyTok2 = [...]int{
2, 3, 4, 5, 6, 7, 8, 9, 10, 11,
12, 13, 20, 21, 24, 25, 29,
}
var yyTok3 = [...]int{
0,
}
var yyErrorMessages = [...]struct {
state int
token int
msg string
}{}
//line yaccpar:1
/* parser for yacc output */
var (
yyDebug = 0
yyErrorVerbose = false
)
type yyLexer interface {
Lex(lval *yySymType) int
Error(s string)
}
type yyParser interface {
Parse(yyLexer) int
Lookahead() int
}
type yyParserImpl struct {
lval yySymType
stack [yyInitialStackSize]yySymType
char int
}
func (p *yyParserImpl) Lookahead() int {
return p.char
}
func yyNewParser() yyParser {
return &yyParserImpl{}
}
const yyFlag = -1000
func yyTokname(c int) string {
if c >= 1 && c-1 < len(yyToknames) {
if yyToknames[c-1] != "" {
return yyToknames[c-1]
}
}
return __yyfmt__.Sprintf("tok-%v", c)
}
func yyStatname(s int) string {
if s >= 0 && s < len(yyStatenames) {
if yyStatenames[s] != "" {
return yyStatenames[s]
}
}
return __yyfmt__.Sprintf("state-%v", s)
}
func yyErrorMessage(state, lookAhead int) string {
const TOKSTART = 4
if !yyErrorVerbose {
return "syntax error"
}
for _, e := range yyErrorMessages {
if e.state == state && e.token == lookAhead {
return "syntax error: " + e.msg
}
}
res := "syntax error: unexpected " + yyTokname(lookAhead)
// To match Bison, suggest at most four expected tokens.
expected := make([]int, 0, 4)
// Look for shiftable tokens.
base := yyPact[state]
for tok := TOKSTART; tok-1 < len(yyToknames); tok++ {
if n := base + tok; n >= 0 && n < yyLast && yyChk[yyAct[n]] == tok {
if len(expected) == cap(expected) {
return res
}
expected = append(expected, tok)
}
}
if yyDef[state] == -2 {
i := 0
for yyExca[i] != -1 || yyExca[i+1] != state {
i += 2
}
// Look for tokens that we accept or reduce.
for i += 2; yyExca[i] >= 0; i += 2 {
tok := yyExca[i]
if tok < TOKSTART || yyExca[i+1] == 0 {
continue
}
if len(expected) == cap(expected) {
return res
}
expected = append(expected, tok)
}
// If the default action is to accept or reduce, give up.
if yyExca[i+1] != 0 {
return res
}
}
for i, tok := range expected {
if i == 0 {
res += ", expecting "
} else {
res += " or "
}
res += yyTokname(tok)
}
return res
}
func yylex1(lex yyLexer, lval *yySymType) (char, token int) {
token = 0
char = lex.Lex(lval)
if char <= 0 {
token = yyTok1[0]
goto out
}
if char < len(yyTok1) {
token = yyTok1[char]
goto out
}
if char >= yyPrivate {
if char < yyPrivate+len(yyTok2) {
token = yyTok2[char-yyPrivate]
goto out
}
}
for i := 0; i < len(yyTok3); i += 2 {
token = yyTok3[i+0]
if token == char {
token = yyTok3[i+1]
goto out
}
}
out:
if token == 0 {
token = yyTok2[1] /* unknown char */
}
if yyDebug >= 3 {
__yyfmt__.Printf("lex %s(%d)\n", yyTokname(token), uint(char))
}
return char, token
}
func yyParse(yylex yyLexer) int {
return yyNewParser().Parse(yylex)
}
func (yyrcvr *yyParserImpl) Parse(yylex yyLexer) int {
var yyn int
var yyVAL yySymType
var yyDollar []yySymType
_ = yyDollar // silence set and not used
yyS := yyrcvr.stack[:]
Nerrs := 0 /* number of errors */
Errflag := 0 /* error recovery flag */
yystate := 0
yyrcvr.char = -1
yytoken := -1 // yyrcvr.char translated into internal numbering
defer func() {
// Make sure we report no lookahead when not parsing.
yystate = -1
yyrcvr.char = -1
yytoken = -1
}()
yyp := -1
goto yystack
ret0:
return 0
ret1:
return 1
yystack:
/* put a state and value onto the stack */
if yyDebug >= 4 {
__yyfmt__.Printf("char %v in %v\n", yyTokname(yytoken), yyStatname(yystate))
}
yyp++
if yyp >= len(yyS) {
nyys := make([]yySymType, len(yyS)*2)
copy(nyys, yyS)
yyS = nyys
}
yyS[yyp] = yyVAL
yyS[yyp].yys = yystate
yynewstate:
yyn = yyPact[yystate]
if yyn <= yyFlag {
goto yydefault /* simple state */
}
if yyrcvr.char < 0 {
yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval)
}
yyn += yytoken
if yyn < 0 || yyn >= yyLast {
goto yydefault
}
yyn = yyAct[yyn]
if yyChk[yyn] == yytoken { /* valid shift */
yyrcvr.char = -1
yytoken = -1
yyVAL = yyrcvr.lval
yystate = yyn
if Errflag > 0 {
Errflag--
}
goto yystack
}
yydefault:
/* default state action */
yyn = yyDef[yystate]
if yyn == -2 {
if yyrcvr.char < 0 {
yyrcvr.char, yytoken = yylex1(yylex, &yyrcvr.lval)
}
/* look through exception table */
xi := 0
for {
if yyExca[xi+0] == -1 && yyExca[xi+1] == yystate {
break
}
xi += 2
}
for xi += 2; ; xi += 2 {
yyn = yyExca[xi+0]
if yyn < 0 || yyn == yytoken {
break
}
}
yyn = yyExca[xi+1]
if yyn < 0 {
goto ret0
}
}
if yyn == 0 {
/* error ... attempt to resume parsing */
switch Errflag {
case 0: /* brand new error */
yylex.Error(yyErrorMessage(yystate, yytoken))
Nerrs++
if yyDebug >= 1 {
__yyfmt__.Printf("%s", yyStatname(yystate))
__yyfmt__.Printf(" saw %s\n", yyTokname(yytoken))
}
fallthrough
case 1, 2: /* incompletely recovered error ... try again */
Errflag = 3
/* find a state where "error" is a legal shift action */
for yyp >= 0 {
yyn = yyPact[yyS[yyp].yys] + yyErrCode
if yyn >= 0 && yyn < yyLast {
yystate = yyAct[yyn] /* simulate a shift of "error" */
if yyChk[yystate] == yyErrCode {
goto yystack
}
}
/* the current p has no shift on "error", pop stack */
if yyDebug >= 2 {
__yyfmt__.Printf("error recovery pops state %d\n", yyS[yyp].yys)
}
yyp--
}
/* there is no state on the stack with an error shift ... abort */
goto ret1
case 3: /* no shift yet; clobber input char */
if yyDebug >= 2 {
__yyfmt__.Printf("error recovery discards %s\n", yyTokname(yytoken))
}
if yytoken == yyEofCode {
goto ret1
}
yyrcvr.char = -1
yytoken = -1
goto yynewstate /* try again in the same state */
}
}
/* reduction by production yyn */
if yyDebug >= 2 {
__yyfmt__.Printf("reduce %v in:\n\t%v\n", yyn, yyStatname(yystate))
}
yynt := yyn
yypt := yyp
_ = yypt // guard against "declared and not used"
yyp -= yyR2[yyn]
// yyp is now the index of $0. Perform the default action. Iff the
// reduced production is ε, $1 is possibly out of range.
if yyp+1 >= len(yyS) {
nyys := make([]yySymType, len(yyS)*2)
copy(nyys, yyS)
yyS = nyys
}
yyVAL = yyS[yyp+1]
/* consult goto table to find next state */
yyn = yyR1[yyn]
yyg := yyPgo[yyn]
yyj := yyg + yyS[yyp].yys + 1
if yyj >= yyLast {
yystate = yyAct[yyg]
} else {
yystate = yyAct[yyj]
if yyChk[yystate] != -yyn {
yystate = yyAct[yyg]
}
}
// dummy call; replaced with literal code
switch yynt {
case 1:
yyDollar = yyS[yypt-1 : yypt+1]
//line parser.go.y:45
{
yyVAL.compstmt = nil
}
case 2:
yyDollar = yyS[yypt-2 : yypt+1]
//line parser.go.y:49
{
yyVAL.compstmt = yyDollar[1].stmts
}
case 3:
yyDollar = yyS[yypt-2 : yypt+1]
//line parser.go.y:55
{
yyVAL.stmts = []ast.Stmt{yyDollar[2].stmt}
if l, ok := yylex.(*Lexer); ok {
l.stmts = yyVAL.stmts
}
}
case 4:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:62
{
if yyDollar[3].stmt != nil {
yyVAL.stmts = append(yyDollar[1].stmts, yyDollar[3].stmt)
if l, ok := yylex.(*Lexer); ok {
l.stmts = yyVAL.stmts
}
}
}
case 5:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:73
{
yyVAL.stmt = &ast.LetStmt{Lhs: yyDollar[1].expr, Operator: "=", Rhs: yyDollar[3].expr}
}
case 6:
yyDollar = yyS[yypt-1 : yypt+1]
//line parser.go.y:77
{
yyVAL.stmt = &ast.ExprStmt{Expr: yyDollar[1].expr}
}
case 7:
yyDollar = yyS[yypt-1 : yypt+1]
//line parser.go.y:83
{
yyVAL.expr = &ast.IdentExpr{Lit: yyDollar[1].tok.Lit}
}
case 8:
yyDollar = yyS[yypt-1 : yypt+1]
//line parser.go.y:87
{
yyVAL.expr = &ast.NumberExpr{Lit: yyDollar[1].tok.Lit}
}
case 9:
yyDollar = yyS[yypt-2 : yypt+1]
//line parser.go.y:91
{
yyVAL.expr = &ast.UnaryExpr{Operator: "-", Expr: yyDollar[2].expr}
}
case 10:
yyDollar = yyS[yypt-2 : yypt+1]
//line parser.go.y:95
{
yyVAL.expr = &ast.UnaryExpr{Operator: "!", Expr: yyDollar[2].expr}
}
case 11:
yyDollar = yyS[yypt-2 : yypt+1]
//line parser.go.y:99
{
yyVAL.expr = &ast.UnaryExpr{Operator: "^", Expr: yyDollar[2].expr}
}
case 12:
yyDollar = yyS[yypt-1 : yypt+1]
//line parser.go.y:103
{
yyVAL.expr = &ast.StringExpr{Lit: yyDollar[1].tok.Lit}
}
case 13:
yyDollar = yyS[yypt-5 : yypt+1]
//line parser.go.y:107
{
yyVAL.expr = &ast.TernaryOpExpr{Expr: yyDollar[1].expr, Lhs: yyDollar[3].expr, Rhs: yyDollar[5].expr}
}
case 14:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:111
{
yyVAL.expr = &ast.ParenExpr{SubExpr: yyDollar[2].expr}
}
case 15:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:115
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "+", Rhs: yyDollar[3].expr}
}
case 16:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:119
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "-", Rhs: yyDollar[3].expr}
}
case 17:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:123
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "*", Rhs: yyDollar[3].expr}
}
case 18:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:127
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "/", Rhs: yyDollar[3].expr}
}
case 19:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:131
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "%", Rhs: yyDollar[3].expr}
}
case 20:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:135
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "==", Rhs: yyDollar[3].expr}
}
case 21:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:139
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "!=", Rhs: yyDollar[3].expr}
}
case 22:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:143
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: ">", Rhs: yyDollar[3].expr}
}
case 23:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:147
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: ">=", Rhs: yyDollar[3].expr}
}
case 24:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:151
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "<", Rhs: yyDollar[3].expr}
}
case 25:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:155
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "<=", Rhs: yyDollar[3].expr}
}
case 26:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:159
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "|", Rhs: yyDollar[3].expr}
}
case 27:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:163
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "||", Rhs: yyDollar[3].expr}
}
case 28:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:167
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "&", Rhs: yyDollar[3].expr}
}
case 29:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:171
{
yyVAL.expr = &ast.BinOpExpr{Lhs: yyDollar[1].expr, Operator: "&&", Rhs: yyDollar[3].expr}
}
case 30:
yyDollar = yyS[yypt-4 : yypt+1]
//line parser.go.y:175
{
yyVAL.expr = &ast.CallExpr{Name: yyDollar[1].tok.Lit, SubExprs: yyDollar[3].exprs}
}
case 31:
yyDollar = yyS[yypt-0 : yypt+1]
//line parser.go.y:180
{
yyVAL.exprs = nil
}
case 32:
yyDollar = yyS[yypt-1 : yypt+1]
//line parser.go.y:184
{
yyVAL.exprs = []ast.Expr{yyDollar[1].expr}
}
case 33:
yyDollar = yyS[yypt-3 : yypt+1]
//line parser.go.y:188
{
yyVAL.exprs = append(yyDollar[1].exprs, yyDollar[3].expr)
}
case 36:
yyDollar = yyS[yypt-1 : yypt+1]
//line parser.go.y:198
{
}
case 37:
yyDollar = yyS[yypt-2 : yypt+1]
//line parser.go.y:201
{
}
case 38:
yyDollar = yyS[yypt-1 : yypt+1]
//line parser.go.y:206
{
}
case 39:
yyDollar = yyS[yypt-1 : yypt+1]
//line parser.go.y:209
{
}
}
goto yystack /* stack new state and value */
}

View File

@@ -1,214 +0,0 @@
%{
package parser
import (
"github.com/mattn/kinako/ast"
)
%}
%type<compstmt> compstmt
%type<stmts> stmts
%type<stmt> stmt
%type<expr> expr
%type<exprs> exprs
%union{
compstmt []ast.Stmt
stmts []ast.Stmt
stmt ast.Stmt
expr ast.Expr
exprs []ast.Expr
tok ast.Token
term ast.Token
terms ast.Token
opt_terms ast.Token
}
%token<tok> IDENT NUMBER STRING EQEQ NEQ GE LE OROR ANDAND POW
%right '='
%right '?' ':'
%left OROR
%left ANDAND
%left IDENT
%nonassoc EQEQ NEQ ','
%left '>' GE '<' LE SHIFTLEFT SHIFTRIGHT
%left '+' '-' PLUSPLUS MINUSMINUS
%left '*' '/' '%'
%right UNARY
%%
compstmt : opt_terms
{
$$ = nil
}
| stmts opt_terms
{
$$ = $1
}
stmts :
opt_terms stmt
{
$$ = []ast.Stmt{$2}
if l, ok := yylex.(*Lexer); ok {
l.stmts = $$
}
}
| stmts terms stmt
{
if $3 != nil {
$$ = append($1, $3)
if l, ok := yylex.(*Lexer); ok {
l.stmts = $$
}
}
}
stmt :
expr '=' expr
{
$$ = &ast.LetStmt{Lhs: $1, Operator: "=", Rhs: $3}
}
| expr
{
$$ = &ast.ExprStmt{Expr: $1}
}
expr :
IDENT
{
$$ = &ast.IdentExpr{Lit: $1.Lit}
}
| NUMBER
{
$$ = &ast.NumberExpr{Lit: $1.Lit}
}
| '-' expr %prec UNARY
{
$$ = &ast.UnaryExpr{Operator: "-", Expr: $2}
}
| '!' expr %prec UNARY
{
$$ = &ast.UnaryExpr{Operator: "!", Expr: $2}
}
| '^' expr %prec UNARY
{
$$ = &ast.UnaryExpr{Operator: "^", Expr: $2}
}
| STRING
{
$$ = &ast.StringExpr{Lit: $1.Lit}
}
| expr '?' expr ':' expr
{
$$ = &ast.TernaryOpExpr{Expr: $1, Lhs: $3, Rhs: $5}
}
| '(' expr ')'
{
$$ = &ast.ParenExpr{SubExpr: $2}
}
| expr '+' expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "+", Rhs: $3}
}
| expr '-' expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "-", Rhs: $3}
}
| expr '*' expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "*", Rhs: $3}
}
| expr '/' expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "/", Rhs: $3}
}
| expr '%' expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "%", Rhs: $3}
}
| expr EQEQ expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "==", Rhs: $3}
}
| expr NEQ expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "!=", Rhs: $3}
}
| expr '>' expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: ">", Rhs: $3}
}
| expr GE expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: ">=", Rhs: $3}
}
| expr '<' expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "<", Rhs: $3}
}
| expr LE expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "<=", Rhs: $3}
}
| expr '|' expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "|", Rhs: $3}
}
| expr OROR expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "||", Rhs: $3}
}
| expr '&' expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "&", Rhs: $3}
}
| expr ANDAND expr
{
$$ = &ast.BinOpExpr{Lhs: $1, Operator: "&&", Rhs: $3}
}
| IDENT '(' exprs ')'
{
$$ = &ast.CallExpr{Name: $1.Lit, SubExprs: $3}
}
exprs :
{
$$ = nil
}
| expr
{
$$ = []ast.Expr{$1}
}
| exprs ',' expr
{
$$ = append($1, $3)
}
opt_terms : /* none */
| terms
;
terms : term
{
}
| terms term
{
}
;
term : ';'
{
}
| '\n'
{
}
;
%%

File diff suppressed because it is too large Load Diff

View File

@@ -1,258 +0,0 @@
package vm
import (
"fmt"
"reflect"
"strings"
"sync"
"github.com/mattn/kinako/parser"
)
// Env provides interface to run VM. This mean function scope and blocked-scope.
// If stack goes to blocked-scope, it will make new Env.
type Env struct {
name string
env map[string]reflect.Value
typ map[string]reflect.Type
parent *Env
interrupt *bool
sync.RWMutex
}
// NewEnv creates new global scope.
func NewEnv() *Env {
b := false
return &Env{
env: make(map[string]reflect.Value),
typ: make(map[string]reflect.Type),
parent: nil,
interrupt: &b,
}
}
// NewEnv creates new child scope.
func (e *Env) NewEnv() *Env {
return &Env{
env: make(map[string]reflect.Value),
typ: make(map[string]reflect.Type),
parent: e,
name: e.name,
interrupt: e.interrupt,
}
}
func NewPackage(n string) *Env {
b := false
return &Env{
env: make(map[string]reflect.Value),
typ: make(map[string]reflect.Type),
parent: nil,
name: n,
interrupt: &b,
}
}
func (e *Env) NewPackage(n string) *Env {
return &Env{
env: make(map[string]reflect.Value),
typ: make(map[string]reflect.Type),
parent: e,
name: n,
interrupt: e.interrupt,
}
}
// Destroy deletes current scope.
func (e *Env) Destroy() {
e.Lock()
defer e.Unlock()
if e.parent == nil {
return
}
for k, v := range e.parent.env {
if v.IsValid() && v.Interface() == e {
delete(e.parent.env, k)
}
}
e.parent = nil
e.env = nil
}
// NewModule creates new module scope as global.
func (e *Env) NewModule(n string) *Env {
m := &Env{
env: make(map[string]reflect.Value),
parent: e,
name: n,
}
e.Define(n, m)
return m
}
// SetName sets a name of the scope. This means that the scope is module.
func (e *Env) SetName(n string) {
e.Lock()
e.name = n
e.Unlock()
}
// GetName returns module name.
func (e *Env) GetName() string {
e.RLock()
defer e.RUnlock()
return e.name
}
// Addr returns pointer value which specified symbol. It goes to upper scope until
// found or returns error.
func (e *Env) Addr(k string) (reflect.Value, error) {
e.RLock()
defer e.RUnlock()
if v, ok := e.env[k]; ok {
return v.Addr(), nil
}
if e.parent == nil {
return NilValue, fmt.Errorf("Undefined symbol '%s'", k)
}
return e.parent.Addr(k)
}
// Type returns type which specified symbol. It goes to upper scope until
// found or returns error.
func (e *Env) Type(k string) (reflect.Type, error) {
e.RLock()
defer e.RUnlock()
if v, ok := e.typ[k]; ok {
return v, nil
}
if e.parent == nil {
return NilType, fmt.Errorf("Undefined type '%s'", k)
}
return e.parent.Type(k)
}
// Get returns value which specified symbol. It goes to upper scope until
// found or returns error.
func (e *Env) Get(k string) (reflect.Value, error) {
e.RLock()
defer e.RUnlock()
if v, ok := e.env[k]; ok {
return v, nil
}
if e.parent == nil {
return NilValue, fmt.Errorf("Undefined symbol '%s'", k)
}
return e.parent.Get(k)
}
// Set modifies value which specified as symbol. It goes to upper scope until
// found or returns error.
func (e *Env) Set(k string, v interface{}) error {
e.Lock()
defer e.Unlock()
if _, ok := e.env[k]; ok {
val, ok := v.(reflect.Value)
if !ok {
val = reflect.ValueOf(v)
}
e.env[k] = val
return nil
}
if e.parent == nil {
return fmt.Errorf("Unknown symbol '%s'", k)
}
return e.parent.Set(k, v)
}
// DefineGlobal defines symbol in global scope.
func (e *Env) DefineGlobal(k string, v interface{}) error {
if e.parent == nil {
return e.Define(k, v)
}
return e.parent.DefineGlobal(k, v)
}
// DefineType defines type which specifis symbol in global scope.
func (e *Env) DefineType(k string, t interface{}) error {
if strings.Contains(k, ".") {
return fmt.Errorf("Unknown symbol '%s'", k)
}
global := e
keys := []string{k}
e.RLock()
for global.parent != nil {
if global.name != "" {
keys = append(keys, global.name)
}
global = global.parent
}
e.RUnlock()
for i, j := 0, len(keys)-1; i < j; i, j = i+1, j-1 {
keys[i], keys[j] = keys[j], keys[i]
}
typ, ok := t.(reflect.Type)
if !ok {
typ = reflect.TypeOf(t)
}
global.Lock()
global.typ[strings.Join(keys, ".")] = typ
global.Unlock()
return nil
}
// Define defines symbol in current scope.
func (e *Env) Define(k string, v interface{}) error {
if strings.Contains(k, ".") {
return fmt.Errorf("Unknown symbol '%s'", k)
}
val, ok := v.(reflect.Value)
if !ok {
val = reflect.ValueOf(v)
}
e.Lock()
e.env[k] = val
e.Unlock()
return nil
}
// String return the name of current scope.
func (e *Env) String() string {
e.RLock()
defer e.RUnlock()
return e.name
}
// Dump show symbol values in the scope.
func (e *Env) Dump() {
e.RLock()
for k, v := range e.env {
fmt.Printf("%v = %#v\n", k, v)
}
e.RUnlock()
}
// Execute parses and runs source in current scope.
func (e *Env) Execute(src string) (reflect.Value, error) {
stmts, err := parser.ParseSrc(src)
if err != nil {
return NilValue, err
}
return Run(stmts, e)
}

View File

@@ -1,476 +0,0 @@
package vm
import (
"errors"
"fmt"
"math"
"os"
"reflect"
"strconv"
"strings"
"github.com/mattn/kinako/ast"
)
var (
NilValue = reflect.ValueOf((*interface{})(nil))
NilType = reflect.TypeOf((*interface{})(nil))
TrueValue = reflect.ValueOf(true)
FalseValue = reflect.ValueOf(false)
)
// Error provides a convenient interface for handling runtime error.
// It can be Error interface with type cast which can call Pos().
type Error struct {
Message string
}
var (
BreakError = errors.New("Unexpected break statement")
ContinueError = errors.New("Unexpected continue statement")
ReturnError = errors.New("Unexpected return statement")
InterruptError = errors.New("Execution interrupted")
)
// Error returns the error message.
func (e *Error) Error() string {
return e.Message
}
// Func is function interface to reflect functions internaly.
type Func func(args ...reflect.Value) (reflect.Value, error)
// Run executes statements in the specified environment.
func Run(stmts []ast.Stmt, env *Env) (reflect.Value, error) {
rv := NilValue
var err error
for _, stmt := range stmts {
rv, err = RunSingleStmt(stmt, env)
if err != nil {
return rv, err
}
}
return rv, nil
}
// Interrupts the execution of any running statements in the specified environment.
//
// Note that the execution is not instantly aborted: after a call to Interrupt,
// the current running statement will finish, but the next statement will not run,
// and instead will return a NilValue and an InterruptError.
func Interrupt(env *Env) {
env.Lock()
*(env.interrupt) = true
env.Unlock()
}
// RunSingleStmt executes one statement in the specified environment.
func RunSingleStmt(stmt ast.Stmt, env *Env) (reflect.Value, error) {
env.Lock()
if *(env.interrupt) {
*(env.interrupt) = false
env.Unlock()
return NilValue, InterruptError
}
env.Unlock()
switch stmt := stmt.(type) {
case *ast.ExprStmt:
rv, err := invokeExpr(stmt.Expr, env)
if err != nil {
return rv, err
}
return rv, nil
case *ast.LetStmt:
rv := NilValue
var err error
rv, err = invokeExpr(stmt.Rhs, env)
if err != nil {
return rv, err
}
_, err = invokeLetExpr(stmt.Lhs, rv, env)
if err != nil {
return rv, err
}
return rv, nil
default:
return NilValue, errors.New("unknown statement")
}
}
// toString converts all reflect.Value-s into string.
func toString(v reflect.Value) string {
if v.Kind() == reflect.Interface {
v = v.Elem()
}
if v.Kind() == reflect.String {
return v.String()
}
if !v.IsValid() {
return "nil"
}
return fmt.Sprint(v.Interface())
}
// toBool converts all reflect.Value-s into bool.
func toBool(v reflect.Value) bool {
if v.Kind() == reflect.Interface {
v = v.Elem()
}
switch v.Kind() {
case reflect.Float32, reflect.Float64:
return v.Float() != 0.0
case reflect.Int, reflect.Int32, reflect.Int64:
return v.Int() != 0
case reflect.Bool:
return v.Bool()
case reflect.String:
if v.String() == "true" {
return true
}
if toInt64(v) != 0 {
return true
}
}
return false
}
// toFloat64 converts all reflect.Value-s into float64.
func toFloat64(v reflect.Value) float64 {
if v.Kind() == reflect.Interface {
v = v.Elem()
}
switch v.Kind() {
case reflect.Float32, reflect.Float64:
return v.Float()
case reflect.Int, reflect.Int32, reflect.Int64:
return float64(v.Int())
}
return 0.0
}
func isNil(v reflect.Value) bool {
if !v.IsValid() || v.Kind().String() == "unsafe.Pointer" {
return true
}
if (v.Kind() == reflect.Interface || v.Kind() == reflect.Ptr) && v.IsNil() {
return true
}
return false
}
func isNum(v reflect.Value) bool {
switch v.Kind() {
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr, reflect.Float32, reflect.Float64:
return true
}
return false
}
// equal returns true when lhsV and rhsV is same value.
func equal(lhsV, rhsV reflect.Value) bool {
lhsIsNil, rhsIsNil := isNil(lhsV), isNil(rhsV)
if lhsIsNil && rhsIsNil {
return true
}
if (!lhsIsNil && rhsIsNil) || (lhsIsNil && !rhsIsNil) {
return false
}
if lhsV.Kind() == reflect.Interface || lhsV.Kind() == reflect.Ptr {
lhsV = lhsV.Elem()
}
if rhsV.Kind() == reflect.Interface || rhsV.Kind() == reflect.Ptr {
rhsV = rhsV.Elem()
}
if !lhsV.IsValid() || !rhsV.IsValid() {
return true
}
if isNum(lhsV) && isNum(rhsV) {
if rhsV.Type().ConvertibleTo(lhsV.Type()) {
rhsV = rhsV.Convert(lhsV.Type())
}
}
if lhsV.CanInterface() && rhsV.CanInterface() {
return reflect.DeepEqual(lhsV.Interface(), rhsV.Interface())
}
return reflect.DeepEqual(lhsV, rhsV)
}
// toInt64 converts all reflect.Value-s into int64.
func toInt64(v reflect.Value) int64 {
if v.Kind() == reflect.Interface {
v = v.Elem()
}
switch v.Kind() {
case reflect.Float32, reflect.Float64:
return int64(v.Float())
case reflect.Int, reflect.Int32, reflect.Int64:
return v.Int()
case reflect.String:
s := v.String()
var i int64
var err error
if strings.HasPrefix(s, "0x") {
i, err = strconv.ParseInt(s, 16, 64)
} else {
i, err = strconv.ParseInt(s, 10, 64)
}
if err == nil {
return int64(i)
}
}
return 0
}
func invokeLetExpr(expr ast.Expr, rv reflect.Value, env *Env) (reflect.Value, error) {
switch lhs := expr.(type) {
case *ast.IdentExpr:
if env.Set(lhs.Lit, rv) != nil {
if strings.Contains(lhs.Lit, ".") {
return NilValue, fmt.Errorf("Undefined symbol '%s'", lhs.Lit)
}
env.Define(lhs.Lit, rv)
}
return rv, nil
}
return NilValue, errors.New("Invalid operation")
}
// invokeExpr evaluates one expression.
func invokeExpr(expr ast.Expr, env *Env) (reflect.Value, error) {
switch e := expr.(type) {
case *ast.NumberExpr:
if strings.Contains(e.Lit, ".") || strings.Contains(e.Lit, "e") {
v, err := strconv.ParseFloat(e.Lit, 64)
if err != nil {
return NilValue, err
}
return reflect.ValueOf(float64(v)), nil
}
var i int64
var err error
if strings.HasPrefix(e.Lit, "0x") {
i, err = strconv.ParseInt(e.Lit[2:], 16, 64)
} else {
i, err = strconv.ParseInt(e.Lit, 10, 64)
}
if err != nil {
return NilValue, err
}
return reflect.ValueOf(i), nil
case *ast.IdentExpr:
return env.Get(e.Lit)
case *ast.StringExpr:
return reflect.ValueOf(e.Lit), nil
case *ast.UnaryExpr:
v, err := invokeExpr(e.Expr, env)
if err != nil {
return v, err
}
switch e.Operator {
case "-":
if v.Kind() == reflect.Float64 {
return reflect.ValueOf(-v.Float()), nil
}
return reflect.ValueOf(-v.Int()), nil
case "^":
return reflect.ValueOf(^toInt64(v)), nil
case "!":
return reflect.ValueOf(!toBool(v)), nil
default:
return NilValue, errors.New("Unknown operator ''")
}
case *ast.ParenExpr:
v, err := invokeExpr(e.SubExpr, env)
if err != nil {
return v, err
}
return v, nil
case *ast.BinOpExpr:
lhsV := NilValue
rhsV := NilValue
var err error
lhsV, err = invokeExpr(e.Lhs, env)
if err != nil {
return lhsV, err
}
if lhsV.Kind() == reflect.Interface {
lhsV = lhsV.Elem()
}
if e.Rhs != nil {
rhsV, err = invokeExpr(e.Rhs, env)
if err != nil {
return rhsV, err
}
if rhsV.Kind() == reflect.Interface {
rhsV = rhsV.Elem()
}
}
switch e.Operator {
case "+":
if lhsV.Kind() == reflect.String || rhsV.Kind() == reflect.String {
return reflect.ValueOf(toString(lhsV) + toString(rhsV)), nil
}
if (lhsV.Kind() == reflect.Array || lhsV.Kind() == reflect.Slice) && (rhsV.Kind() != reflect.Array && rhsV.Kind() != reflect.Slice) {
return reflect.Append(lhsV, rhsV), nil
}
if (lhsV.Kind() == reflect.Array || lhsV.Kind() == reflect.Slice) && (rhsV.Kind() == reflect.Array || rhsV.Kind() == reflect.Slice) {
return reflect.AppendSlice(lhsV, rhsV), nil
}
if lhsV.Kind() == reflect.Float64 || rhsV.Kind() == reflect.Float64 {
return reflect.ValueOf(toFloat64(lhsV) + toFloat64(rhsV)), nil
}
return reflect.ValueOf(toInt64(lhsV) + toInt64(rhsV)), nil
case "-":
if lhsV.Kind() == reflect.Float64 || rhsV.Kind() == reflect.Float64 {
return reflect.ValueOf(toFloat64(lhsV) - toFloat64(rhsV)), nil
}
return reflect.ValueOf(toInt64(lhsV) - toInt64(rhsV)), nil
case "*":
if lhsV.Kind() == reflect.String && (rhsV.Kind() == reflect.Int || rhsV.Kind() == reflect.Int32 || rhsV.Kind() == reflect.Int64) {
return reflect.ValueOf(strings.Repeat(toString(lhsV), int(toInt64(rhsV)))), nil
}
if lhsV.Kind() == reflect.Float64 || rhsV.Kind() == reflect.Float64 {
return reflect.ValueOf(toFloat64(lhsV) * toFloat64(rhsV)), nil
}
return reflect.ValueOf(toInt64(lhsV) * toInt64(rhsV)), nil
case "/":
return reflect.ValueOf(toFloat64(lhsV) / toFloat64(rhsV)), nil
case "%":
return reflect.ValueOf(toInt64(lhsV) % toInt64(rhsV)), nil
case "==":
return reflect.ValueOf(equal(lhsV, rhsV)), nil
case "!=":
return reflect.ValueOf(equal(lhsV, rhsV) == false), nil
case ">":
return reflect.ValueOf(toFloat64(lhsV) > toFloat64(rhsV)), nil
case ">=":
return reflect.ValueOf(toFloat64(lhsV) >= toFloat64(rhsV)), nil
case "<":
return reflect.ValueOf(toFloat64(lhsV) < toFloat64(rhsV)), nil
case "<=":
return reflect.ValueOf(toFloat64(lhsV) <= toFloat64(rhsV)), nil
case "|":
return reflect.ValueOf(toInt64(lhsV) | toInt64(rhsV)), nil
case "||":
if toBool(lhsV) {
return lhsV, nil
}
return rhsV, nil
case "&":
return reflect.ValueOf(toInt64(lhsV) & toInt64(rhsV)), nil
case "&&":
if toBool(lhsV) {
return rhsV, nil
}
return lhsV, nil
case "**":
if lhsV.Kind() == reflect.Float64 {
return reflect.ValueOf(math.Pow(toFloat64(lhsV), toFloat64(rhsV))), nil
}
return reflect.ValueOf(int64(math.Pow(toFloat64(lhsV), toFloat64(rhsV)))), nil
case ">>":
return reflect.ValueOf(toInt64(lhsV) >> uint64(toInt64(rhsV))), nil
case "<<":
return reflect.ValueOf(toInt64(lhsV) << uint64(toInt64(rhsV))), nil
default:
return NilValue, errors.New("Unknown operator")
}
case *ast.CallExpr:
f, err := env.Get(e.Name)
if err != nil {
return f, err
}
args := []reflect.Value{}
for i, expr := range e.SubExprs {
arg, err := invokeExpr(expr, env)
if err != nil {
return arg, err
}
if i < f.Type().NumIn() {
if !f.Type().IsVariadic() {
it := f.Type().In(i)
if arg.Kind().String() == "unsafe.Pointer" {
arg = reflect.New(it).Elem()
}
if arg.Kind() != it.Kind() && arg.IsValid() && arg.Type().ConvertibleTo(it) {
arg = arg.Convert(it)
} else if arg.Kind() == reflect.Func {
if _, isFunc := arg.Interface().(Func); isFunc {
rfunc := arg
arg = reflect.MakeFunc(it, func(args []reflect.Value) []reflect.Value {
for i := range args {
args[i] = reflect.ValueOf(args[i])
}
return rfunc.Call(args)[:it.NumOut()]
})
}
} else if !arg.IsValid() {
arg = reflect.Zero(it)
}
}
}
if !arg.IsValid() {
arg = NilValue
}
args = append(args, arg)
}
ret := NilValue
fnc := func() {
defer func() {
if os.Getenv("KINAKO_DEBUG") == "" {
if ex := recover(); ex != nil {
if e, ok := ex.(error); ok {
err = e
} else {
err = errors.New(fmt.Sprint(ex))
}
}
}
}()
if f.Kind() == reflect.Interface {
f = f.Elem()
}
rets := f.Call(args)
if f.Type().NumOut() == 1 {
ret = rets[0]
} else {
var result []interface{}
for _, r := range rets {
result = append(result, r.Interface())
}
ret = reflect.ValueOf(result)
}
}
fnc()
if err != nil {
return ret, err
}
return ret, nil
case *ast.TernaryOpExpr:
rv, err := invokeExpr(e.Expr, env)
if err != nil {
return rv, err
}
if toBool(rv) {
lhsV, err := invokeExpr(e.Lhs, env)
if err != nil {
return lhsV, err
}
return lhsV, nil
}
rhsV, err := invokeExpr(e.Rhs, env)
if err != nil {
return rhsV, err
}
return rhsV, nil
default:
return NilValue, errors.New("Unknown expression")
}
}

View File

@@ -1,54 +0,0 @@
package vm
import (
"reflect"
"testing"
)
func TestExecute(t *testing.T) {
e := NewEnv()
e.Define("foo", int64(1))
e.Define("bar", int64(2))
e.Define("baz", int64(3))
tests := []struct {
input string
want interface{}
}{
{
input: "foo+bar",
want: int64(3),
},
{
input: "foo-bar",
want: int64(-1),
},
{
input: "foo*bar",
want: int64(2),
},
{
input: "foo/bar",
want: float64(0.5),
},
{
input: "baz*(foo+bar)",
want: int64(9),
},
{
input: "baz > 2 ? foo : bar",
want: int64(1),
},
}
for _, tt := range tests {
r, err := e.Execute(tt.input)
if err != nil {
t.Fatal(err)
}
got := r.Interface()
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("want %v, but %v:", tt.want, got)
}
}
}