pax_global_header00006660000000000000000000000064137465443720014531gustar00rootroot0000000000000052 comment=1b8a993ae40a69bc9e9ed20a93b526c1e6b7d457 gotext-1.5.0/000077500000000000000000000000001374654437200130465ustar00rootroot00000000000000gotext-1.5.0/.github/000077500000000000000000000000001374654437200144065ustar00rootroot00000000000000gotext-1.5.0/.github/ISSUE_TEMPLATE.md000066400000000000000000000003411374654437200171110ustar00rootroot00000000000000# 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 gotext-1.5.0/.github/PULL_REQUEST_TEMPLATE.md000066400000000000000000000012471374654437200202130ustar00rootroot00000000000000# 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. gotext-1.5.0/.github/workflows/000077500000000000000000000000001374654437200164435ustar00rootroot00000000000000gotext-1.5.0/.github/workflows/build.yml000066400000000000000000000012131374654437200202620ustar00rootroot00000000000000name: 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 github.com/leonelquinteros/gotext/cli/xgotext - name: Test run: go test -v -race ./... gotext-1.5.0/.gitignore000066400000000000000000000005271374654437200150420ustar00rootroot00000000000000# Eclipse shit .project .settings .buildpath # golang jetbrains shit .idea # Compiled Object files, Static and Dynamic libs (Shared Objects) *.o *.a *.so # Folders _obj _test # Architecture specific extensions/prefixes *.[568vq] [568vq].out *.cgo1.go *.cgo2.c _cgo_defun.c _cgo_gotypes.go _cgo_export.* _testmain.go *.exe *.test *.prof gotext-1.5.0/CODE_OF_CONDUCT.md000066400000000000000000000062271374654437200156540ustar00rootroot00000000000000# 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/ gotext-1.5.0/CONTRIBUTING.md000066400000000000000000000013521374654437200153000ustar00rootroot00000000000000# 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. gotext-1.5.0/LICENSE000066400000000000000000000020731374654437200140550ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 Leonel Quinteros Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. gotext-1.5.0/README.md000066400000000000000000000216271374654437200143350ustar00rootroot00000000000000[![GitHub release](https://img.shields.io/github/release/leonelquinteros/gotext.svg)](https://github.com/leonelquinteros/gotext) [![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) ![Gotext build](https://github.com/leonelquinteros/gotext/workflows/Gotext%20build/badge.svg?branch=master) [![Go Report Card](https://goreportcard.com/badge/github.com/leonelquinteros/gotext)](https://goreportcard.com/report/github.com/leonelquinteros/gotext) [![PkgGoDev](https://pkg.go.dev/badge/github.com/leonelquinteros/gotext)](https://pkg.go.dev/github.com/leonelquinteros/gotext) # Gotext [GNU gettext utilities](https://www.gnu.org/software/gettext) for Go. # Features - Implements GNU gettext support in native Go. - Complete support for [PO files](https://www.gnu.org/software/gettext/manual/html_node/PO-Files.html) including: - Support for multiline strings and headers. - Support for variables inside translation strings using Go's [fmt syntax](https://golang.org/pkg/fmt/). - Support for [pluralization rules](https://www.gnu.org/software/gettext/manual/html_node/Translating-plural-forms.html). - Support for [message contexts](https://www.gnu.org/software/gettext/manual/html_node/Contexts.html). - 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 [MIT license](LICENSE) # Documentation Refer to the Godoc package documentation at (https://godoc.org/github.com/leonelquinteros/gotext) # Installation ``` go get github.com/leonelquinteros/gotext ``` - 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. ## 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 [Go Modules](https://github.com/golang/go/wiki/Modules) (Recommended) Add `github.com/leonelquinteros/gotext` inside the `require` section in your `go.mod` file. i.e. ``` require ( github.com/leonelquinteros/gotext v1.4.0 ) ``` ### Vendoring with [dep](https://golang.github.io/dep/) To use last stable version (v1.4.0 at the moment of writing) ``` dep ensure -add github.com/leonelquinteros/gotext@v1.4.0 ``` Import as ```go import "github.com/leonelquinteros/gotext" ``` ### Vendoring with [gopkg.in](http://labix.org/gopkg.in) [http://gopkg.in/leonelquinteros/gotext.v1](http://gopkg.in/leonelquinteros/gotext.v1) To get the latest v1 package stable release, execute: ``` go get gopkg.in/leonelquinteros/gotext.v1 ``` Import as ```go import "gopkg.in/leonelquinteros/gotext.v1" ``` 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. 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). 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: ``` /path/to/locales /path/to/locales/en_US /path/to/locales/en_US/LC_MESSAGES /path/to/locales/en_US/LC_MESSAGES/default.po /path/to/locales/en_US/LC_MESSAGES/extras.po /path/to/locales/en_UK /path/to/locales/en_UK/LC_MESSAGES /path/to/locales/en_UK/LC_MESSAGES/default.po /path/to/locales/en_UK/LC_MESSAGES/extras.po /path/to/locales/en_AU /path/to/locales/en_AU/LC_MESSAGES /path/to/locales/en_AU/LC_MESSAGES/default.po /path/to/locales/en_AU/LC_MESSAGES/extras.po /path/to/locales/es /path/to/locales/es/default.po /path/to/locales/es/extras.po /path/to/locales/es_ES /path/to/locales/es_ES/default.po /path/to/locales/es_ES/extras.po /path/to/locales/fr /path/to/locales/fr/default.po /path/to/locales/fr/extras.po ``` And so on... # Usage examples ## Using package for single language/domain settings For quick/simple translations you can use the package level functions directly. ```go import ( "fmt" "github.com/leonelquinteros/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")) } ``` ## Using dynamic variables on translations All translation strings support dynamic variables to be inserted without translate. Use the fmt.Printf syntax (from Go's "fmt" package) to specify how to print the non-translated variable inside the translation string. ```go import ( "fmt" "github.com/leonelquinteros/gotext" ) func main() { // Configure package gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name") // Set variables name := "John" // Translate text with variables fmt.Println(gotext.Get("Hi, my name is %s", name)) } ``` ## Using Locale object When having multiple languages/domains/libraries at the same time, you can create Locale objects for each variation so you can handle each settings on their own. ```go import ( "fmt" "github.com/leonelquinteros/gotext" ) func main() { // Create Locale with library path and language code l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY") // Load domain '/path/to/locales/root/dir/es_UY/default.po' l.AddDomain("default") // Translate text from default domain 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: ``` {{ .Loc.Get "Translate this" }} ``` ## Using the Po object to handle .po files and PO-formatted strings For when you need to work with PO files and strings, you can directly use the Po object to parse it and access the translations in there in the same way. ```go import ( "fmt" "github.com/leonelquinteros/gotext" ) func main() { // Set PO content str := ` msgid "Translate this" msgstr "Translated text" msgid "Another string" msgstr "" msgid "One with var: %s" msgstr "This one sets the var: %s" ` // Create Po object po := new(gotext.Po) po.Parse(str) fmt.Println(po.Get("Translate this")) } ``` ## Use plural forms of translations PO format supports defining one or more plural forms for the same translation. Relying on the PO file headers, a Plural-Forms formula can be set on the translation file as defined in (https://www.gnu.org/savannah-checkouts/gnu/gettext/manual/html_node/Plural-forms.html) ```go import ( "fmt" "github.com/leonelquinteros/gotext" ) func main() { // Set PO content str := ` msgid "" msgstr "" # Header below "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Translate this" msgstr "Translated text" msgid "Another string" msgstr "" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular: %s" msgstr[1] "This one is the plural: %s" ` // 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 - Please, contribute. - Use the package on your projects. - Report issues on Github. - Send pull requests for bugfixes and improvements. - Send proposals on Github issues. gotext-1.5.0/cli/000077500000000000000000000000001374654437200136155ustar00rootroot00000000000000gotext-1.5.0/cli/xgotext/000077500000000000000000000000001374654437200153175ustar00rootroot00000000000000gotext-1.5.0/cli/xgotext/README.md000066400000000000000000000023751374654437200166050ustar00rootroot00000000000000# xgotext CLI tool to extract translation strings from Go packages into .POT files. ## Installation ``` go install github.com/leonelquinteros/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 gotext-1.5.0/cli/xgotext/fixtures/000077500000000000000000000000001374654437200171705ustar00rootroot00000000000000gotext-1.5.0/cli/xgotext/fixtures/i18n/000077500000000000000000000000001374654437200177475ustar00rootroot00000000000000gotext-1.5.0/cli/xgotext/fixtures/i18n/default.po000066400000000000000000000007661374654437200217440ustar00rootroot00000000000000msgid "" 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] "" gotext-1.5.0/cli/xgotext/fixtures/i18n/domain.po000066400000000000000000000004341374654437200215570ustar00rootroot00000000000000msgid "" 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 "" gotext-1.5.0/cli/xgotext/fixtures/i18n/domain2.po000066400000000000000000000004561374654437200216450ustar00rootroot00000000000000msgid "" 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 "" gotext-1.5.0/cli/xgotext/fixtures/i18n/translations.po000066400000000000000000000006051374654437200230310ustar00rootroot00000000000000msgid "" 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] "" gotext-1.5.0/cli/xgotext/fixtures/main.go000066400000000000000000000036711374654437200204520ustar00rootroot00000000000000package main import ( "errors" "fmt" "github.com/leonelquinteros/gotext" alias "github.com/leonelquinteros/gotext" "github.com/leonelquinteros/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") } gotext-1.5.0/cli/xgotext/fixtures/pkg/000077500000000000000000000000001374654437200177515ustar00rootroot00000000000000gotext-1.5.0/cli/xgotext/fixtures/pkg/pkg.go000066400000000000000000000003261374654437200210620ustar00rootroot00000000000000package pkg import "github.com/leonelquinteros/gotext" type SubTranslate struct { L gotext.Locale } type Translate struct { L gotext.Locale S SubTranslate } func test() { gotext.Get("inside sub package") } gotext-1.5.0/cli/xgotext/main.go000066400000000000000000000017001374654437200165700ustar00rootroot00000000000000package main import ( "flag" "log" "strings" "github.com/leonelquinteros/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) } } gotext-1.5.0/cli/xgotext/parser/000077500000000000000000000000001374654437200166135ustar00rootroot00000000000000gotext-1.5.0/cli/xgotext/parser/domain.go000066400000000000000000000107611374654437200204160ustar00rootroot00000000000000package 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 } gotext-1.5.0/cli/xgotext/parser/golang.go000066400000000000000000000126761374654437200204250ustar00rootroot00000000000000package 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() != "github.com/leonelquinteros/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 != "github.com/leonelquinteros/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) } gotext-1.5.0/cli/xgotext/parser/parser.go000066400000000000000000000025721374654437200204440ustar00rootroot00000000000000package 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 } gotext-1.5.0/domain.go000066400000000000000000000354621374654437200146560ustar00rootroot00000000000000package gotext import ( "bytes" "encoding/gob" "sort" "strconv" "strings" "sync" "golang.org/x/text/language" "github.com/leonelquinteros/gotext/plurals" ) // Domain has all the common functions for dealing with a gettext domain // it's initialized with a GettextFile (which represents either a Po or Mo file) type Domain struct { Headers HeaderMap // Language header Language string tag language.Tag // Plural-Forms header PluralForms string // Preserve comments at head of PO for round-trip headerComments []string // Parsed Plural-Forms header values nplurals int plural string pluralforms plurals.Expression // Storage translations map[string]*Translation contexts map[string]map[string]*Translation pluralTranslations map[string]*Translation // Sync Mutex trMutex sync.RWMutex pluralMutex sync.RWMutex // Parsing buffers trBuffer *Translation ctxBuffer string refBuffer string } // Preserve MIMEHeader behaviour, without the canonicalisation type HeaderMap map[string][]string func (m HeaderMap) Add(key, value string) { m[key] = append(m[key], value) } func (m HeaderMap) Del(key string) { delete(m, key) } func (m HeaderMap) Get(key string) string { if m == nil { return "" } v := m[key] if len(v) == 0 { return "" } return v[0] } func (m HeaderMap) Set(key, value string) { m[key] = []string{value} } func (m HeaderMap) Values(key string) []string { if m == nil { return nil } return m[key] } func NewDomain() *Domain { domain := new(Domain) domain.Headers = make(HeaderMap) domain.headerComments = make([]string, 0) domain.translations = make(map[string]*Translation) domain.contexts = make(map[string]map[string]*Translation) domain.pluralTranslations = make(map[string]*Translation) return domain } func (do *Domain) pluralForm(n int) int { // do we really need locking here? not sure how this plurals.Expression works, so sticking with it for now do.pluralMutex.RLock() defer do.pluralMutex.RUnlock() // Failure fallback if do.pluralforms == nil { /* Use the Germanic plural rule. */ if n == 1 { return 0 } return 1 } return do.pluralforms.Eval(uint32(n)) } // parseHeaders retrieves data from previously parsed headers. it's called by both Mo and Po when parsing func (do *Domain) parseHeaders() { raw := "" if _, ok := do.translations[raw]; ok { raw = do.translations[raw].Get() } // textproto.ReadMIMEHeader() forces keys through CanonicalMIMEHeaderKey(); must read header manually to have one-to-one round-trip of keys languageKey := "Language" pluralFormsKey := "Plural-Forms" rawLines := strings.Split(raw, "\n") for _, line := range rawLines { if len(line) == 0 { continue } colonIdx := strings.Index(line, ":") if colonIdx < 0 { continue } key := line[:colonIdx] lowerKey := strings.ToLower(key) if lowerKey == strings.ToLower(languageKey) { languageKey = key } else if lowerKey == strings.ToLower(pluralFormsKey) { pluralFormsKey = key } value := strings.TrimSpace(line[colonIdx+1:]) do.Headers.Add(key, value) } // Get/save needed headers do.Language = do.Headers.Get(languageKey) do.tag = language.Make(do.Language) do.PluralForms = do.Headers.Get(pluralFormsKey) // Parse Plural-Forms formula if do.PluralForms == "" { return } // Split plural form header value pfs := strings.Split(do.PluralForms, ";") // Parse values for _, i := range pfs { vs := strings.SplitN(i, "=", 2) if len(vs) != 2 { continue } switch strings.TrimSpace(vs[0]) { case "nplurals": do.nplurals, _ = strconv.Atoi(vs[1]) case "plural": do.plural = vs[1] if expr, err := plurals.Compile(do.plural); err == nil { do.pluralforms = expr } } } } // Drops any translations stored that have not been Set*() since 'po' // was initialised func (do *Domain) DropStaleTranslations() { do.trMutex.Lock() do.pluralMutex.Lock() defer do.trMutex.Unlock() defer do.pluralMutex.Unlock() for name, ctx := range do.contexts { for id, trans := range ctx { if trans.IsStale() { delete(ctx, id) } } if len(ctx) == 0 { delete(do.contexts, name) } } for id, trans := range do.translations { if trans.IsStale() { delete(do.translations, id) } } } // Set source references for a given translation func (do *Domain) SetRefs(str string, refs []string) { do.trMutex.Lock() do.pluralMutex.Lock() defer do.trMutex.Unlock() defer do.pluralMutex.Unlock() if trans, ok := do.translations[str]; ok { trans.Refs = refs } else { trans = NewTranslation() trans.ID = str trans.SetRefs(refs) do.translations[str] = trans } } // Get source references for a given translation func (do *Domain) GetRefs(str string) []string { // Sync read do.trMutex.RLock() defer do.trMutex.RUnlock() if do.translations != nil { if trans, ok := do.translations[str]; ok { return trans.Refs } } return nil } // Set the translation of a given string func (do *Domain) Set(id, str string) { do.trMutex.Lock() do.pluralMutex.Lock() defer do.trMutex.Unlock() defer do.pluralMutex.Unlock() if trans, ok := do.translations[id]; ok { trans.Set(str) } else { trans = NewTranslation() trans.ID = id trans.Set(str) do.translations[str] = trans } } func (do *Domain) Get(str string, vars ...interface{}) string { // Sync read do.trMutex.RLock() defer do.trMutex.RUnlock() if do.translations != nil { if _, ok := do.translations[str]; ok { return Printf(do.translations[str].Get(), vars...) } } // Return the same we received by default return Printf(str, vars...) } // Set the (N)th plural form for the given string func (do *Domain) SetN(id, plural string, n int, str string) { // Get plural form _before_ lock down pluralForm := do.pluralForm(n) do.trMutex.Lock() do.pluralMutex.Lock() defer do.trMutex.Unlock() defer do.pluralMutex.Unlock() if trans, ok := do.translations[id]; ok { trans.SetN(pluralForm, str) } else { trans = NewTranslation() trans.ID = id trans.PluralID = plural trans.SetN(pluralForm, str) do.translations[str] = trans } } // 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 (do *Domain) GetN(str, plural string, n int, vars ...interface{}) string { // Sync read do.trMutex.RLock() defer do.trMutex.RUnlock() if do.translations != nil { if _, ok := do.translations[str]; ok { return Printf(do.translations[str].GetN(do.pluralForm(n)), vars...) } } // Parse plural forms to distinguish between plural and singular if do.pluralForm(n) == 0 { return Printf(str, vars...) } return Printf(plural, vars...) } // Set the translation for the given string in the given context func (do *Domain) SetC(id, ctx, str string) { do.trMutex.Lock() do.pluralMutex.Lock() defer do.trMutex.Unlock() defer do.pluralMutex.Unlock() if context, ok := do.contexts[ctx]; ok { if trans, hasTrans := context[id]; hasTrans { trans.Set(str) } else { trans = NewTranslation() trans.ID = id trans.Set(str) context[id] = trans } } else { trans := NewTranslation() trans.ID = id trans.Set(str) do.contexts[ctx] = map[string]*Translation{ id: trans, } } } // 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 (do *Domain) GetC(str, ctx string, vars ...interface{}) string { do.trMutex.RLock() defer do.trMutex.RUnlock() if do.contexts != nil { if _, ok := do.contexts[ctx]; ok { if do.contexts[ctx] != nil { if _, ok := do.contexts[ctx][str]; ok { return Printf(do.contexts[ctx][str].Get(), vars...) } } } } // Return the string we received by default return Printf(str, vars...) } // Set the (N)th plural form for the given string in the given context func (do *Domain) SetNC(id, plural, ctx string, n int, str string) { // Get plural form _before_ lock down pluralForm := do.pluralForm(n) do.trMutex.Lock() do.pluralMutex.Lock() defer do.trMutex.Unlock() defer do.pluralMutex.Unlock() if context, ok := do.contexts[ctx]; ok { if trans, hasTrans := context[id]; hasTrans { trans.SetN(pluralForm, str) } else { trans = NewTranslation() trans.ID = id trans.SetN(pluralForm, str) context[id] = trans } } else { trans := NewTranslation() trans.ID = id trans.SetN(pluralForm, str) do.contexts[ctx] = map[string]*Translation{ id: trans, } } } // 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 (do *Domain) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string { do.trMutex.RLock() defer do.trMutex.RUnlock() if do.contexts != nil { if _, ok := do.contexts[ctx]; ok { if do.contexts[ctx] != nil { if _, ok := do.contexts[ctx][str]; ok { return Printf(do.contexts[ctx][str].GetN(do.pluralForm(n)), vars...) } } } } if n == 1 { return Printf(str, vars...) } return Printf(plural, vars...) } //GetTranslations returns a copy of every translation in the domain. It does not support contexts. func (do *Domain) GetTranslations() map[string]*Translation { all := make(map[string]*Translation, len(do.translations)) do.trMutex.RLock() defer do.trMutex.RUnlock() for msgID, trans := range do.translations { newTrans := NewTranslation() newTrans.ID = trans.ID newTrans.PluralID = trans.PluralID newTrans.dirty = trans.dirty if len(trans.Refs) > 0 { newTrans.Refs = make([]string, len(trans.Refs)) copy(newTrans.Refs, trans.Refs) } for k, v := range trans.Trs { newTrans.Trs[k] = v } all[msgID] = newTrans } return all } type SourceReference struct { path string line int context string trans *Translation } func extractPathAndLine(ref string) (string, int) { var path string var line int colonIdx := strings.IndexRune(ref, ':') if colonIdx >= 0 { path = ref[:colonIdx] line, _ = strconv.Atoi(ref[colonIdx+1:]) } else { path = ref line = 0 } return path, line } // MarshalText implements encoding.TextMarshaler interface // Assists round-trip of POT/PO content func (do *Domain) MarshalText() ([]byte, error) { var buf bytes.Buffer if len(do.headerComments) > 0 { buf.WriteString(strings.Join(do.headerComments, "\n")) buf.WriteByte(byte('\n')) } buf.WriteString("msgid \"\"\nmsgstr \"\"") // Standard order consistent with xgettext headerOrder := map[string]int{ "project-id-version": 0, "report-msgid-bugs-to": 1, "pot-creation-date": 2, "po-revision-date": 3, "last-translator": 4, "language-team": 5, "language": 6, "mime-version": 7, "content-type": 9, "content-transfer-encoding": 10, "plural-forms": 11, } headerKeys := make([]string, 0, len(do.Headers)) for k, _ := range do.Headers { headerKeys = append(headerKeys, k) } sort.Slice(headerKeys, func(i, j int) bool { var iOrder int var jOrder int var ok bool if iOrder, ok = headerOrder[strings.ToLower(headerKeys[i])]; !ok { iOrder = 8 } if jOrder, ok = headerOrder[strings.ToLower(headerKeys[j])]; !ok { jOrder = 8 } if iOrder < jOrder { return true } if iOrder > jOrder { return false } return headerKeys[i] < headerKeys[j] }) for _, k := range headerKeys { // Access Headers map directly so as not to canonicalise v := do.Headers[k] for _, value := range v { buf.WriteString("\n\"" + k + ": " + value + "\\n\"") } } // Just as with headers, output translations in consistent order (to minimise diffs between round-trips), with (first) source reference taking priority, followed by context and finally ID references := make([]SourceReference, 0) for name, ctx := range do.contexts { for id, trans := range ctx { if id == "" { continue } if len(trans.Refs) > 0 { path, line := extractPathAndLine(trans.Refs[0]) references = append(references, SourceReference{ path, line, name, trans, }) } else { references = append(references, SourceReference{ "", 0, name, trans, }) } } } for id, trans := range do.translations { if id == "" { continue } if len(trans.Refs) > 0 { path, line := extractPathAndLine(trans.Refs[0]) references = append(references, SourceReference{ path, line, "", trans, }) } else { references = append(references, SourceReference{ "", 0, "", trans, }) } } sort.Slice(references, func(i, j int) bool { if references[i].path < references[j].path { return true } if references[i].path > references[j].path { return false } if references[i].line < references[j].line { return true } if references[i].line > references[j].line { return false } if references[i].context < references[j].context { return true } if references[i].context > references[j].context { return false } return references[i].trans.ID < references[j].trans.ID }) for _, ref := range references { trans := ref.trans if len(trans.Refs) > 0 { buf.WriteString("\n\n#: " + strings.Join(trans.Refs, " ")) } else { buf.WriteByte(byte('\n')) } if ref.context == "" { buf.WriteString("\nmsgid \"" + trans.ID + "\"") } else { buf.WriteString("\nmsgctxt \"" + ref.context + "\"\nmsgid \"" + trans.ID + "\"") } if trans.PluralID == "" { buf.WriteString("\nmsgstr \"" + trans.Trs[0] + "\"") } else { buf.WriteString("\nmsgid_plural \"" + trans.PluralID + "\"") for i, tr := range trans.Trs { buf.WriteString("\nmsgstr[" + strconv.Itoa(i) + "] \"" + tr + "\"") } } } return buf.Bytes(), nil } // MarshalBinary implements encoding.BinaryMarshaler interface func (do *Domain) MarshalBinary() ([]byte, error) { obj := new(TranslatorEncoding) obj.Headers = do.Headers obj.Language = do.Language obj.PluralForms = do.PluralForms obj.Nplurals = do.nplurals obj.Plural = do.plural obj.Translations = do.translations obj.Contexts = do.contexts var buff bytes.Buffer encoder := gob.NewEncoder(&buff) err := encoder.Encode(obj) return buff.Bytes(), err } // UnmarshalBinary implements encoding.BinaryUnmarshaler interface func (do *Domain) UnmarshalBinary(data []byte) error { buff := bytes.NewBuffer(data) obj := new(TranslatorEncoding) decoder := gob.NewDecoder(buff) err := decoder.Decode(obj) if err != nil { return err } do.Headers = obj.Headers do.Language = obj.Language do.PluralForms = obj.PluralForms do.nplurals = obj.Nplurals do.plural = obj.Plural do.translations = obj.Translations do.contexts = obj.Contexts if expr, err := plurals.Compile(do.plural); err == nil { do.pluralforms = expr } return nil } gotext-1.5.0/domain_test.go000066400000000000000000000030671374654437200157110ustar00rootroot00000000000000package gotext import "testing" const ( enUSFixture = "fixtures/en_US/default.po" ) //since both Po and Mo just pass-through to Domain for MarshalBinary and UnmarshalBinary, test it here func TestBinaryEncoding(t *testing.T) { // Create po objects po := NewPo() po2 := NewPo() // Parse file po.ParseFile(enUSFixture) buff, err := po.GetDomain().MarshalBinary() if err != nil { t.Fatal(err) } err = po2.GetDomain().UnmarshalBinary(buff) if err != nil { t.Fatal(err) } // Test translations tr := po2.Get("My text") if tr != translatedText { t.Errorf("Expected '%s' but got '%s'", translatedText, tr) } // Test translations tr = po2.Get("language") if tr != "en_US" { t.Errorf("Expected 'en_US' but got '%s'", tr) } } func TestDomain_GetTranslations(t *testing.T) { po := NewPo() po.ParseFile(enUSFixture) domain := po.GetDomain() all := domain.GetTranslations() if len(all) != len(domain.translations) { t.Error("lengths should match") } for k, v := range domain.translations { if all[k] == v { t.Error("GetTranslations should be returning a copy, but pointers are equal") } if all[k].ID != v.ID { t.Error("IDs should match") } if all[k].PluralID != v.PluralID { t.Error("PluralIDs should match") } if all[k].dirty != v.dirty { t.Error("dirty flag should match") } if len(all[k].Trs) != len(v.Trs) { t.Errorf("Trs length does not match: %d != %d", len(all[k].Trs), len(v.Trs)) } if len(all[k].Refs) != len(v.Refs) { t.Errorf("Refs length does not match: %d != %d", len(all[k].Refs), len(v.Refs)) } } } gotext-1.5.0/fixtures/000077500000000000000000000000001374654437200147175ustar00rootroot00000000000000gotext-1.5.0/fixtures/ar/000077500000000000000000000000001374654437200153215ustar00rootroot00000000000000gotext-1.5.0/fixtures/ar/categories.po000066400000000000000000000012531374654437200200070ustar00rootroot00000000000000msgid "" 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 مستند إضاÙÙŠ"gotext-1.5.0/fixtures/ar/no_plural_header.po000066400000000000000000000000751374654437200211660ustar00rootroot00000000000000msgid "Alcohol & Tobacco" msgstr "الكحول والتبغ" gotext-1.5.0/fixtures/de/000077500000000000000000000000001374654437200153075ustar00rootroot00000000000000gotext-1.5.0/fixtures/de/default.mo000066400000000000000000000022521374654437200172710ustar00rootroot00000000000000Þ• |Ü *!L,i– ›¦&® Õá êøM Y[$µÚï  7Tlo ‰   CtxOne with var: %sSeveral with vars: %sCtxSome random in a contextEmpty plural form singularEmpty plural formMoreMulti-lineMy textOne with var: %sSeveral with vars: %sSome randomlanguagemulti line idmulti line plural idPlural-Forms: nplurals=2; plural=(n != 1); Project-Id-Version: POT-Creation-Date: PO-Revision-Date: Last-Translator: Josef Fröhle Language-Team: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language: de X-Generator: Poedit 2.0.6 X-Poedit-SourceCharset: UTF-8 This one is the singular in a Ctx context: %sThis one is the plural in a Ctx context: %sSome random translation in a contextSingular translatedMore translationMulti lineTranslated textThis one is the singular: %sThis one is the plural: %sSome random translationdeid with multiline contentplural id with multiline contentgotext-1.5.0/fixtures/de/default.po000066400000000000000000000027511374654437200173000ustar00rootroot00000000000000msgid "" 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 \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" gotext-1.5.0/fixtures/de_DE/000077500000000000000000000000001374654437200156575ustar00rootroot00000000000000gotext-1.5.0/fixtures/de_DE/LC_MESSAGES/000077500000000000000000000000001374654437200174445ustar00rootroot00000000000000gotext-1.5.0/fixtures/de_DE/LC_MESSAGES/default.mo000066400000000000000000000022601374654437200214250ustar00rootroot00000000000000Þ• |Ü *!L,i– ›¦&® Õá êøP Y^$¸Ýò 7Wou    CtxOne with var: %sSeveral with vars: %sCtxSome random in a contextEmpty plural form singularEmpty plural formMoreMulti-lineMy textOne with var: %sSeveral with vars: %sSome randomlanguagemulti line idmulti line plural idPlural-Forms: nplurals=2; plural=(n != 1); Project-Id-Version: POT-Creation-Date: PO-Revision-Date: Last-Translator: Josef Fröhle Language-Team: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language: de_DE X-Generator: Poedit 2.0.6 X-Poedit-SourceCharset: UTF-8 This one is the singular in a Ctx context: %sThis one is the plural in a Ctx context: %sSome random translation in a contextSingular translatedMore translationMulti lineTranslated textThis one is the singular: %sThis one is the plural: %sSome random translationde_DEid with multiline contentplural id with multiline contentgotext-1.5.0/fixtures/de_DE/LC_MESSAGES/default.po000066400000000000000000000027571374654437200214430ustar00rootroot00000000000000msgid "" 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 \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" gotext-1.5.0/fixtures/en_AU/000077500000000000000000000000001374654437200157065ustar00rootroot00000000000000gotext-1.5.0/fixtures/en_AU/default.po000066400000000000000000000027101374654437200176720ustar00rootroot00000000000000msgid "" 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 \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" gotext-1.5.0/fixtures/en_GB/000077500000000000000000000000001374654437200156715ustar00rootroot00000000000000gotext-1.5.0/fixtures/en_GB/default.mo000066400000000000000000000022521374654437200176530ustar00rootroot00000000000000Þ• |Ü *!L,i– ›¦&® Õá êöPYY$³Øí þ 7Qio ‰   CtxOne with var: %sSeveral with vars: %sCtxSome random in a contextEmpty plural form singularEmpty plural formMoreMulti-lineMy textOne with var: %sSeveral with vars: %sSome randomlanguagemultilineidmultilinepluralidPlural-Forms: nplurals=2; plural=(n != 1); Project-Id-Version: POT-Creation-Date: PO-Revision-Date: Last-Translator: Josef Fröhle Language-Team: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language: en_US X-Generator: Poedit 2.0.6 X-Poedit-SourceCharset: UTF-8 This one is the singular in a Ctx context: %sThis one is the plural in a Ctx context: %sSome random translation in a contextSingular translatedMore translationMulti lineTranslated textThis one is the singular: %sThis one is the plural: %sSome random translationen_GBid with multiline contentplural id with multiline contentgotext-1.5.0/fixtures/en_US/000077500000000000000000000000001374654437200157305ustar00rootroot00000000000000gotext-1.5.0/fixtures/en_US/default.mo000066400000000000000000000022521374654437200177120ustar00rootroot00000000000000Þ• |Ü *!L,i– ›¦&® Õá êöPYY$³Øí þ 7Qio ‰   CtxOne with var: %sSeveral with vars: %sCtxSome random in a contextEmpty plural form singularEmpty plural formMoreMulti-lineMy textOne with var: %sSeveral with vars: %sSome randomlanguagemultilineidmultilinepluralidPlural-Forms: nplurals=2; plural=(n != 1); Project-Id-Version: POT-Creation-Date: PO-Revision-Date: Last-Translator: Josef Fröhle Language-Team: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language: en_US X-Generator: Poedit 2.0.6 X-Poedit-SourceCharset: UTF-8 This one is the singular in a Ctx context: %sThis one is the plural in a Ctx context: %sSome random translation in a contextSingular translatedMore translationMulti lineTranslated textThis one is the singular: %sThis one is the plural: %sSome random translationen_USid with multiline contentplural id with multiline contentgotext-1.5.0/fixtures/en_US/default.po000066400000000000000000000027101374654437200177140ustar00rootroot00000000000000msgid "" 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 \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" gotext-1.5.0/fixtures/fr/000077500000000000000000000000001374654437200153265ustar00rootroot00000000000000gotext-1.5.0/fixtures/fr/LC_MESSAGES/000077500000000000000000000000001374654437200171135ustar00rootroot00000000000000gotext-1.5.0/fixtures/fr/LC_MESSAGES/default.mo000066400000000000000000000022511374654437200210740ustar00rootroot00000000000000Þ• |Ü *!L,i– ›¦&® Õá êøL YZ$´Ùî ÿ 7Skn ˆ   CtxOne with var: %sSeveral with vars: %sCtxSome random in a contextEmpty plural form singularEmpty plural formMoreMulti-lineMy textOne with var: %sSeveral with vars: %sSome randomlanguagemulti line idmulti line plural idPlural-Forms: nplurals=2; plural=(n > 1); Project-Id-Version: POT-Creation-Date: PO-Revision-Date: Last-Translator: Josef Fröhle Language-Team: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Language: fr X-Generator: Poedit 2.0.6 X-Poedit-SourceCharset: UTF-8 This one is the singular in a Ctx context: %sThis one is the plural in a Ctx context: %sSome random translation in a contextSingular translatedMore translationMulti lineTranslated textThis one is the singular: %sThis one is the plural: %sSome random translationfrid with multiline contentplural id with multiline contentgotext-1.5.0/fixtures/fr/LC_MESSAGES/default.po000066400000000000000000000027501374654437200211030ustar00rootroot00000000000000msgid "" 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 \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" gotext-1.5.0/go.mod000066400000000000000000000002741374654437200141570ustar00rootroot00000000000000module github.com/leonelquinteros/gotext // go: no requirements found in Gopkg.lock require ( golang.org/x/text v0.3.0 golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd ) go 1.13 gotext-1.5.0/go.sum000066400000000000000000000030121374654437200141750ustar00rootroot00000000000000golang.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 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= 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= gotext-1.5.0/gotext.go000066400000000000000000000166571374654437200147260ustar00rootroot00000000000000/* Package gotext implements GNU gettext utilities. For quick/simple translations you can use the package level functions directly. import ( "fmt" "github.com/leonelquinteros/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")) } */ package gotext import ( "encoding/gob" "sync" ) // Global environment variables type config struct { sync.RWMutex // Default domain to look at when no domain is specified. Used by package level functions. domain string // Language set. language string // 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) { globalConfig.Lock() if globalConfig.storage == nil || force { globalConfig.storage = NewLocale(globalConfig.library, globalConfig.language) } 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 { 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. func SetDomain(dom string) { 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 { 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. func SetLanguage(lang string) { globalConfig.Lock() globalConfig.language = SimplifiedLocale(lang) globalConfig.Unlock() loadStorage(true) } // GetLibrary is the library getter for the package configuration func GetLibrary() string { 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. func SetLibrary(lib string) { 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. // It receives the library path, language code and domain name. // This function is recommended to be used when changing more than one setting, // as using each setter will introduce a I/O overhead because the Translation file will be loaded after each set. func Configure(lib, lang, dom string) { 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. // 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(GetDomain(), str, vars...) } // 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(GetDomain(), str, plural, n, vars...) } // 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 { // 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. // 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 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. // 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(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. // 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(GetDomain(), str, plural, n, ctx, vars...) } // GetDC returns the corresponding Translation in the given domain for the given string in the given context. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func GetDC(dom, str, ctx string, vars ...interface{}) string { // 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. // 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 globalConfig.RLock() tr := globalConfig.storage.GetNDC(dom, str, plural, n, ctx, vars...) globalConfig.RUnlock() return tr } gotext-1.5.0/gotext_test.go000066400000000000000000000316721374654437200157570ustar00rootroot00000000000000package gotext import ( "os" "path" "path/filepath" "sync" "testing" ) func TestGettersSetters(t *testing.T) { SetDomain("test") dom := GetDomain() if dom != "test" { t.Errorf("Expected GetDomain to return 'test', but got '%s'", dom) } SetLibrary("/tmp/test") lib := GetLibrary() if lib != "/tmp/test" { t.Errorf("Expected GetLibrary to return '/tmp/test', but got '%s'", lib) } SetLanguage("es") lang := GetLanguage() if lang != "es" { t.Errorf("Expected GetLanguage to return 'es', but got '%s'", lang) } } func TestPackageFunctions(t *testing.T) { // Set PO content str := ` msgid "" msgstr "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" # Some comment msgid "My text" msgstr "Translated text" # More comments msgid "Another string" msgstr "" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular: %s" msgstr[1] "This one is the plural: %s" msgstr[2] "And this is the second plural form: %s" msgctxt "Ctx" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular in a Ctx context: %s" msgstr[1] "This one is the plural in a Ctx context: %s" msgid "Some random" msgstr "Some random Translation" msgctxt "Ctx" msgid "Some random in a context" msgstr "Some random Translation in a context" msgid "More" msgstr "More Translation" msgid "Untranslated" msgid_plural "Several untranslated" msgstr[0] "" msgstr[1] "" ` anotherStr := ` msgid "" msgstr "Project-Id-Version: %s\n" "Report-Msgid-Bugs-To: %s\n" # Initial comment # More Headers below "Language: en\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Another text on a different domain" msgstr "Another text on another domain" ` // Create Locales directory on default location dirname := path.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.Join(dirname, "default.po") f, err := os.Create(filename) if err != nil { t.Fatalf("Can't create test file: %s", err.Error()) } _, err = f.WriteString(str) if err != nil { t.Fatalf("Can't write to test file: %s", err.Error()) } anotherFilename := path.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") // Test translations tr := Get("My text") if tr != translatedText { t.Errorf("Expected '%s' but got '%s'", translatedText, tr) } v := "Variable" tr = Get("One with var: %s", v) if tr != "This one is the singular: Variable" { t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr) } // Test plural tr = GetN("One with var: %s", "Several with vars: %s", 2, v) if tr != "This one is the plural: Variable" { t.Errorf("Expected 'This one is the plural: Variable' but got '%s'", tr) } // Test context translations tr = GetC("Some random in a context", "Ctx") if tr != "Some random Translation in a context" { t.Errorf("Expected 'Some random Translation in a context' but got '%s'", tr) } v = "Variable" tr = GetC("One with var: %s", "Ctx", v) if tr != "This one is the singular in a Ctx context: Variable" { t.Errorf("Expected 'This one is the singular in a Ctx context: Variable' but got '%s'", tr) } tr = GetNC("One with var: %s", "Several with vars: %s", 19, "Ctx", v) if tr != "This one is the plural in a Ctx context: Variable" { t.Errorf("Expected 'This one is the plural in a Ctx context: Variable' but got '%s'", tr) } tr = GetD("another", "Another text on a different domain") if tr != "Another text on another domain" { t.Errorf("Expected 'Another text on another domain' but got '%s'", tr) } } func TestUntranslated(t *testing.T) { // Set PO content str := ` msgid "" msgstr "" # Initial comment # Headers below "Language: en\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Untranslated" msgid_plural "Several untranslated" msgstr[0] "" msgstr[1] "" ` // Create Locales directory on default location dirname := path.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.Join(dirname, "default.po") f, err := os.Create(filename) if err != nil { t.Fatalf("Can't create test file: %s", err.Error()) } defer f.Close() _, err = f.WriteString(str) if err != nil { t.Fatalf("Can't write to test file: %s", err.Error()) } // Set package configuration Configure("/tmp", "en_US", "default") // Test untranslated tr := Get("Untranslated") if tr != "Untranslated" { t.Errorf("Expected 'Untranslated' but got '%s'", tr) } tr = GetN("Untranslated", "Several untranslated", 1) if tr != "Untranslated" { t.Errorf("Expected 'Untranslated' but got '%s'", tr) } tr = GetN("Untranslated", "Several untranslated", 2) if tr != "Several untranslated" { t.Errorf("Expected 'Several untranslated' but got '%s'", tr) } tr = GetD("default", "Untranslated") if tr != "Untranslated" { t.Errorf("Expected 'Untranslated' but got '%s'", tr) } tr = GetND("default", "Untranslated", "Several untranslated", 1) if tr != "Untranslated" { t.Errorf("Expected 'Untranslated' but got '%s'", tr) } tr = GetND("default", "Untranslated", "Several untranslated", 2) if tr != "Several untranslated" { t.Errorf("Expected 'Several untranslated' but got '%s'", tr) } } func TestMoAndPoTranslator(t *testing.T) { fixPath, _ := filepath.Abs("./fixtures/") Configure(fixPath, "en_GB", "default") // Check default domain Translation SetDomain("default") tr := Get("My text") if tr != translatedText { t.Errorf("Expected '%s'. Got '%s'", translatedText, 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 != translatedText { t.Errorf("Expected '%s'. Got '%s'", translatedText, 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 := path.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(path.Join(dirname, "default.po")) if err != nil { t.Fatalf("Can't create test file: %s", err.Error()) } defer fDefault.Close() fCustom, err := os.Create(path.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 msgid "My text" msgstr "Translated text" # More comments msgid "Another string" msgstr "" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular: %s" msgstr[1] "This one is the plural: %s" msgstr[2] "And this is the second plural form: %s" msgctxt "Ctx" msgid "Some random in a context" msgstr "Some random Translation in a context" ` // Create Locales directory on default location dirname := path.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.Join("/tmp", GetDomain()+".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()) } var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) // Test translations go func() { defer wg.Done() GetLibrary() SetLibrary(path.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.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) } } gotext-1.5.0/helper.go000066400000000000000000000042421374654437200146560ustar00rootroot00000000000000/* * 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 } gotext-1.5.0/helper_test.go000066400000000000000000000052301374654437200157130ustar00rootroot00000000000000/* * 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) } } gotext-1.5.0/locale.go000066400000000000000000000222731374654437200146420ustar00rootroot00000000000000/* * 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 ( "bytes" "encoding/gob" "os" "path" "sync" ) /* Locale wraps the entire i18n collection for a single language (locale) It's used by the package functions, but it can also be used independently to handle multiple languages at the same time by working with this object. Example: import ( "encoding/gob" "bytes" "fmt" "github.com/leonelquinteros/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,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,mo}') l.AddDomain("extras") // Translate text from domain fmt.Println(l.GetD("extras", "Translate this")) } */ type Locale struct { // Path to locale files. path string // Language for this Locale lang string // List of available Domains for this locale. Domains map[string]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 .po/.mo files directory (p) and a language code to use (l). func NewLocale(p, l string) *Locale { return &Locale{ path: p, lang: SimplifiedLocale(l), Domains: make(map[string]Translator), } } func (l *Locale) findExt(dom, ext string) string { filename := path.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+"."+ext) if _, err := os.Stat(filename); err == nil { return filename } } filename = path.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+"."+ext) if _, err := os.Stat(filename); err == nil { 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) { var poObj Translator file := l.findExt(dom, "po") if file != "" { poObj = NewPo() // Parse file. poObj.ParseFile(file) } else { file = l.findExt(dom, "mo") if file != "" { poObj = NewMo() // Parse file. poObj.ParseFile(file) } else { // fallback return if no file found with return } } // Save new domain l.Lock() if l.Domains == nil { l.Domains = make(map[string]Translator) } if l.defaultDomain == "" { l.defaultDomain = dom } l.Domains[dom] = poObj // Unlock "Save new domain" l.Unlock() } // 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(l.GetDomain(), str, vars...) } // 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(l.GetDomain(), str, plural, n, vars...) } // 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 { // 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. // 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...) } } } // 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. // 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(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. // 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(l.GetDomain(), str, plural, n, ctx, vars...) } // GetDC returns the corresponding Translation in the given domain for the given string in the given context. // Supports optional parameters (vars... interface{}) to be inserted on the formatted string using the fmt.Printf syntax. func (l *Locale) GetDC(dom, str, ctx string, vars ...interface{}) string { // 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. // 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...) } } } // Use western default rule (plural > 1) to handle missing domain default result. if n == 1 { return Printf(str, vars...) } return Printf(plural, vars...) } //GetTranslations returns a copy of all translations in all domains of this locale. It does not support contexts. func (l *Locale) GetTranslations() map[string]*Translation { all := make(map[string]*Translation) l.RLock() defer l.RUnlock() for _, translator := range l.Domains { for msgID, trans := range translator.GetDomain().GetTranslations() { all[msgID] = trans } } return all } // 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 } gotext-1.5.0/locale_test.go000066400000000000000000000413601374654437200156770ustar00rootroot00000000000000/* * 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 TestLocale(t *testing.T) { // Set PO content str := ` msgid "" msgstr "" # Initial comment # Headers below "Language: en\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" # Some comment msgid "My text" msgstr "Translated text" # More comments msgid "Another string" msgstr "" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular: %s" msgstr[1] "This one is the plural: %s" msgstr[2] "And this is the second plural form: %s" msgid "This one has invalid syntax translations" msgid_plural "Plural index" msgstr[abc] "Wrong index" msgstr[1 "Forgot to close brackets" msgstr[0] "Badly formatted string' msgctxt "Ctx" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular in a Ctx context: %s" msgstr[1] "This one is the plural in a Ctx context: %s" msgid "Some random" msgstr "Some random Translation" msgctxt "Ctx" msgid "Some random in a context" msgstr "Some random Translation in a context" msgid "More" msgstr "More Translation" ` // Create Locales directory with simplified language code dirname := path.Join("/tmp", "en", "LC_MESSAGES") err := os.MkdirAll(dirname, os.ModePerm) if err != nil { t.Fatalf("Can't create test directory: %s", err.Error()) } // Write PO content to file filename := path.Join(dirname, "my_domain.po") f, err := os.Create(filename) if err != nil { t.Fatalf("Can't create test file: %s", err.Error()) } defer f.Close() _, err = f.WriteString(str) if err != nil { t.Fatalf("Can't write to test file: %s", err.Error()) } // Create Locale with full language code l := NewLocale("/tmp", "en_US") // Force nil domain storage l.Domains = nil // Add domain l.AddDomain("my_domain") // Test translations tr := l.GetD("my_domain", "My text") if tr != translatedText { t.Errorf("Expected '%s' but got '%s'", translatedText, tr) } v := "Variable" tr = l.GetD("my_domain", "One with var: %s", v) if tr != "This one is the singular: Variable" { t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr) } // Test plural tr = l.GetND("my_domain", "One with var: %s", "Several with vars: %s", 7, v) if tr != "This one is the plural: Variable" { t.Errorf("Expected 'This one is the plural: Variable' but got '%s'", tr) } // Test context translations tr = l.GetC("Some random in a context", "Ctx") if tr != "Some random Translation in a context" { t.Errorf("Expected 'Some random Translation in a context'. Got '%s'", tr) } v = "Test" tr = l.GetNC("One with var: %s", "Several with vars: %s", 23, "Ctx", v) if tr != "This one is the plural in a Ctx context: Test" { t.Errorf("Expected 'This one is the plural in a Ctx context: Test'. Got '%s'", tr) } tr = l.GetDC("my_domain", "One with var: %s", "Ctx", v) if tr != "This one is the singular in a Ctx context: Test" { t.Errorf("Expected 'This one is the singular in a Ctx context: Test' but got '%s'", tr) } // Test plural tr = l.GetNDC("my_domain", "One with var: %s", "Several with vars: %s", 3, "Ctx", v) if tr != "This one is the plural in a Ctx context: Test" { t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr) } // Test last Translation tr = l.GetD("my_domain", "More") if tr != "More Translation" { t.Errorf("Expected 'More Translation' but got '%s'", tr) } } func TestLocaleFails(t *testing.T) { // Set PO content str := ` msgid "" msgstr "" # Initial comment # Headers below "Language: en\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" # Some comment msgid "My text" msgstr "Translated text" # More comments msgid "Another string" msgstr "" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular: %s" msgstr[1] "This one is the plural: %s" msgstr[2] "And this is the second plural form: %s" msgid "This one has invalid syntax translations" msgid_plural "Plural index" msgstr[abc] "Wrong index" msgstr[1 "Forgot to close brackets" msgstr[0] "Badly formatted string' msgid "Invalid formatted id[] with no translations msgctxt "Ctx" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular in a Ctx context: %s" msgstr[1] "This one is the plural in a Ctx context: %s" msgid "Some random" msgstr "Some random Translation" msgctxt "Ctx" msgid "Some random in a context" msgstr "Some random Translation in a context" msgid "More" msgstr "More Translation" ` // Create Locales directory with simplified language code dirname := path.Join("/tmp", "en", "LC_MESSAGES") err := os.MkdirAll(dirname, os.ModePerm) if err != nil { t.Fatalf("Can't create test directory: %s", err.Error()) } // Write PO content to file filename := path.Join(dirname, "my_domain.po") f, err := os.Create(filename) if err != nil { t.Fatalf("Can't create test file: %s", err.Error()) } defer f.Close() _, err = f.WriteString(str) if err != nil { t.Fatalf("Can't write to test file: %s", err.Error()) } // Create Locale with full language code l := NewLocale("/tmp", "en_US") // Force nil domain storage l.Domains = nil // Add domain l.AddDomain("my_domain") // Test 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) } v := "Variable" tr = l.GetN("One with var: %s", "Several with vars: %s", 2, v) if tr != "Several with vars: Variable" { t.Errorf("Expected 'Several with vars: Variable' but got '%s'", tr) } // Test inexistent translations tr = l.Get("This is a test") if tr != "This is a test" { t.Errorf("Expected 'This is a test' but got '%s'", tr) } tr = l.GetN("This is a test", "This are tests", 1) if tr != "This 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) } // 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("/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) } } func TestLocaleRace(t *testing.T) { // Set PO content str := `# Some comment msgid "My text" msgstr "Translated text" # More comments msgid "Another string" msgstr "" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular: %s" msgstr[1] "This one is the plural: %s" msgstr[2] "And this is the second plural form: %s" ` // Create Locales directory with simplified language code dirname := path.Join("/tmp", "es") err := os.MkdirAll(dirname, os.ModePerm) if err != nil { t.Fatalf("Can't create test directory: %s", err.Error()) } // Write PO content to file filename := path.Join(dirname, "race.po") f, err := os.Create(filename) if err != nil { t.Fatalf("Can't create test file: %s", err.Error()) } defer f.Close() _, err = f.WriteString(str) if err != nil { t.Fatalf("Can't write to test file: %s", err.Error()) } // Create Locale l := NewLocale("/tmp", "es") // Init sync channels ac := make(chan bool) rc := make(chan bool) // Add domain in goroutine go func(l *Locale, done chan bool) { l.AddDomain("race") done <- true }(l, ac) // Get translations in goroutine go func(l *Locale, done chan bool) { l.GetD("race", "My text") done <- true }(l, rc) // Get translations at top level l.GetD("race", "My text") // Wait for goroutines to finish <-ac <-rc } func TestAddTranslator(t *testing.T) { // Create po object po := NewPo() // 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 != translatedText { t.Errorf("Expected '%s' but got '%s'", translatedText, 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")) } } func TestLocale_GetTranslations(t *testing.T) { l := NewLocale("fixtures/", "en_US") l.AddDomain("default") all := l.GetTranslations() if len(all) < 5 { t.Errorf("length of all translations is too few: %d", len(all)) } const moreMsgID = "More" more, ok := all[moreMsgID] if !ok { t.Error("missing expected translation") } if more.Get() != l.Get(moreMsgID) { t.Errorf("translations of msgid %s do not match: \"%s\" != \"%s\"", moreMsgID, more.Get(), l.Get(moreMsgID)) } } gotext-1.5.0/mo.go000066400000000000000000000145021374654437200140120ustar00rootroot00000000000000/* * 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 ( "bytes" "encoding/binary" ) 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" "github.com/leonelquinteros/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 { //these three public members are for backwards compatibility. they are just set to the value in the domain Headers HeaderMap Language string PluralForms string domain *Domain } //NewMo should always be used to instantiate a new Mo object func NewMo() *Mo { mo := new(Mo) mo.domain = NewDomain() return mo } func (mo *Mo) GetDomain() *Domain { return mo.domain } //all of the Get functions are for convenience and aid in backwards compatibility func (mo *Mo) Get(str string, vars ...interface{}) string { return mo.domain.Get(str, vars...) } func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string { return mo.domain.GetN(str, plural, n, vars...) } func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string { return mo.domain.GetC(str, ctx, vars...) } func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string { return mo.domain.GetNC(str, plural, n, ctx, vars...) } func (mo *Mo) MarshalBinary() ([]byte, error) { return mo.domain.MarshalBinary() } func (mo *Mo) UnmarshalBinary(data []byte) error { return mo.domain.UnmarshalBinary(data) } func (mo *Mo) ParseFile(f string) { data, err := getFileData(f) if err != nil { return } mo.Parse(data) } // Parse loads the translations specified in the provided byte slice, in the GNU gettext .mo format func (mo *Mo) Parse(buf []byte) { // Lock while parsing mo.domain.trMutex.Lock() mo.domain.pluralMutex.Lock() defer mo.domain.trMutex.Unlock() defer mo.domain.pluralMutex.Unlock() 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) } } // Parse headers mo.domain.parseHeaders() // set values on this struct // this is for backwards compatibility mo.Language = mo.domain.Language mo.PluralForms = mo.domain.PluralForms mo.Headers = mo.domain.Headers } 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.domain.contexts[string(msgctxt)]; !ok { mo.domain.contexts[string(msgctxt)] = make(map[string]*Translation) } mo.domain.contexts[string(msgctxt)][translation.ID] = translation } else { mo.domain.translations[translation.ID] = translation } } gotext-1.5.0/mo_test.go000066400000000000000000000124631374654437200150550ustar00rootroot00000000000000/* * 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 mo object mo := NewMo() // 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 != translatedText { t.Errorf("Expected '%s' but got '%s'", translatedText, 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 mo object mo := NewMo() // 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 != translatedText { t.Errorf("Expected '%s' but got '%s'", translatedText, 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 mo object mo := NewMo() // 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 := NewMo() // 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 := NewMo() mo2 := NewMo() // 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 != translatedText { t.Errorf("Expected '%s' but got '%s'", translatedText, tr) } // Test translations tr = mo2.Get("language") if tr != "en_US" { t.Errorf("Expected 'en_US' but got '%s'", tr) } } gotext-1.5.0/plurals/000077500000000000000000000000001374654437200145305ustar00rootroot00000000000000gotext-1.5.0/plurals/compiler.go000066400000000000000000000233761374654437200167040ustar00rootroot00000000000000/* * 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 len(s) == 0 { return []string{} } 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 } gotext-1.5.0/plurals/compiler_test.go000066400000000000000000000020441374654437200177300ustar00rootroot00000000000000/* * 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 } } } } } gotext-1.5.0/plurals/expression.go000066400000000000000000000015161374654437200172610ustar00rootroot00000000000000/* * 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) } gotext-1.5.0/plurals/math.go000066400000000000000000000005461374654437200160150ustar00rootroot00000000000000/* * 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 } gotext-1.5.0/plurals/testdata/000077500000000000000000000000001374654437200163415ustar00rootroot00000000000000gotext-1.5.0/plurals/testdata/pluralforms.json000066400000000000000000001077211374654437200216120ustar00rootroot00000000000000[{"fixture": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "pluralform": "0"}, {"fixture": [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "pluralform": "n!=1"}, {"fixture": [0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "pluralform": "n>1"}, {"fixture": [2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1], "pluralform": "n%10==1&&n%100!=11?0:n!=0?1:2"}, {"fixture": [2, 0, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], "pluralform": "n==1?0:n==2?1:2"}, {"fixture": [1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], "pluralform": "n==1?0:(n==0||(n%100>0&&n%100<20))?1:2"}, {"fixture": [2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2, 0, 1, 1, 1, 1, 1, 1, 1, 1, 2], "pluralform": "n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?1:2"}, {"fixture": [2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2], "pluralform": "n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2"}, {"fixture": [2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2], "pluralform": "(n==1)?0:(n>=2&&n<=4)?1:2"}, {"fixture": [2, 0, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 2, 2, 2], "pluralform": "n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2"}, {"fixture": [3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3, 0, 1, 2, 2, 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, 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, 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, 3, 3, 3, 3, 3, 3], "pluralform": "n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3"}, {"fixture": [0, 1, 2, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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, 3, 3, 3, 3, 3, 3, 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, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 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], "pluralform": "n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11?4:5"}] gotext-1.5.0/plurals/tests.go000066400000000000000000000026471374654437200162320ustar00rootroot00000000000000/* * 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)) } gotext-1.5.0/po.go000066400000000000000000000201601374654437200140120ustar00rootroot00000000000000/* * 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 ( "strconv" "strings" ) /* 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. Example: import ( "fmt" "github.com/leonelquinteros/gotext" ) func main() { // Create po object po := gotext.NewPoTranslator() // Parse .po file po.ParseFile("/path/to/po/file/translations.po") // Get Translation fmt.Println(po.Get("Translate this")) } */ type Po struct { //these three public members are for backwards compatibility. they are just set to the value in the domain Headers HeaderMap Language string PluralForms string domain *Domain } type parseState int const ( head parseState = iota msgCtxt msgID msgIDPlural msgStr ) //NewPo should always be used to instantiate a new Po object func NewPo() *Po { po := new(Po) po.domain = NewDomain() return po } func (po *Po) GetDomain() *Domain { return po.domain } // Convenience interfaces func (po *Po) DropStaleTranslations() { po.domain.DropStaleTranslations() } func (po *Po) SetRefs(str string, refs []string) { po.domain.SetRefs(str, refs) } func (po *Po) GetRefs(str string) []string { return po.domain.GetRefs(str) } func (po *Po) Set(id, str string) { po.domain.Set(id, str) } func (po *Po) Get(str string, vars ...interface{}) string { return po.domain.Get(str, vars...) } func (po *Po) SetN(id, plural string, n int, str string) { po.domain.SetN(id, plural, n, str) } func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string { return po.domain.GetN(str, plural, n, vars...) } func (po *Po) SetC(id, ctx, str string) { po.domain.SetC(id, ctx, str) } func (po *Po) GetC(str, ctx string, vars ...interface{}) string { return po.domain.GetC(str, ctx, vars...) } func (po *Po) SetNC(id, plural, ctx string, n int, str string) { po.domain.SetNC(id, plural, ctx, n, str) } func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string { return po.domain.GetNC(str, plural, n, ctx, vars...) } func (po *Po) MarshalText() ([]byte, error) { return po.domain.MarshalText() } func (po *Po) MarshalBinary() ([]byte, error) { return po.domain.MarshalBinary() } func (po *Po) UnmarshalBinary(data []byte) error { return po.domain.UnmarshalBinary(data) } func (po *Po) ParseFile(f string) { data, err := getFileData(f) if err != nil { return } po.Parse(data) } // Parse loads the translations specified in the provided string (str) func (po *Po) Parse(buf []byte) { if po.domain == nil { panic("NewPo() was not used to instantiate this object") } // Lock while parsing po.domain.trMutex.Lock() po.domain.pluralMutex.Lock() defer po.domain.trMutex.Unlock() defer po.domain.pluralMutex.Unlock() // Get lines lines := strings.Split(string(buf), "\n") // Init buffer po.domain.trBuffer = NewTranslation() po.domain.ctxBuffer = "" po.domain.refBuffer = "" state := head for _, l := range lines { // Trim spaces l = strings.TrimSpace(l) // Skip invalid lines if !po.isValidLine(l) { po.parseComment(l, state) continue } // Buffer context and continue if strings.HasPrefix(l, "msgctxt") { po.parseContext(l) state = msgCtxt continue } // Buffer msgid and continue if strings.HasPrefix(l, "msgid") && !strings.HasPrefix(l, "msgid_plural") { po.parseID(l) state = msgID continue } // Check for plural form if strings.HasPrefix(l, "msgid_plural") { po.parsePluralID(l) po.domain.pluralTranslations[po.domain.trBuffer.PluralID] = po.domain.trBuffer state = msgIDPlural continue } // Save Translation if strings.HasPrefix(l, "msgstr") { po.parseMessage(l) state = msgStr continue } // Multi line strings and headers if strings.HasPrefix(l, "\"") && strings.HasSuffix(l, "\"") { po.parseString(l, state) continue } } // Save last Translation buffer. po.saveBuffer() // Parse headers po.domain.parseHeaders() // set values on this struct // this is for backwards compatibility po.Language = po.domain.Language po.PluralForms = po.domain.PluralForms po.Headers = po.domain.Headers } // saveBuffer takes the context and Translation buffers // and saves it on the translations collection func (po *Po) saveBuffer() { // With no context... if po.domain.ctxBuffer == "" { po.domain.translations[po.domain.trBuffer.ID] = po.domain.trBuffer } else { // With context... if _, ok := po.domain.contexts[po.domain.ctxBuffer]; !ok { po.domain.contexts[po.domain.ctxBuffer] = make(map[string]*Translation) } po.domain.contexts[po.domain.ctxBuffer][po.domain.trBuffer.ID] = po.domain.trBuffer // Cleanup current context buffer if needed if po.domain.trBuffer.ID != "" { po.domain.ctxBuffer = "" } } // Flush Translation buffer if po.domain.refBuffer == "" { po.domain.trBuffer = NewTranslation() } else { po.domain.trBuffer = NewTranslationWithRefs(strings.Split(po.domain.refBuffer, " ")) } } // Either preserves comments before the first "msgid", for later round-trip. // Or preserves source references for a given translation. func (po *Po) parseComment(l string, state parseState) { if len(l) > 0 && l[0] == '#' { if state == head { po.domain.headerComments = append(po.domain.headerComments, l) } else if len(l) > 1 { switch l[1] { case ':': if len(l) > 2 { po.domain.refBuffer = strings.TrimSpace(l[2:]) } } } } } // parseContext takes a line starting with "msgctxt", // saves the current Translation buffer and creates a new context. func (po *Po) parseContext(l string) { // Save current Translation buffer. po.saveBuffer() // Buffer context po.domain.ctxBuffer, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgctxt"))) } // parseID takes a line starting with "msgid", // saves the current Translation and creates a new msgid buffer. func (po *Po) parseID(l string) { // Save current Translation buffer. po.saveBuffer() // Set id po.domain.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.domain.trBuffer.PluralID, _ = strconv.Unquote(strings.TrimSpace(strings.TrimPrefix(l, "msgid_plural"))) } // parseMessage takes a line starting with "msgstr" and saves it into the current buffer. func (po *Po) parseMessage(l string) { l = strings.TrimSpace(strings.TrimPrefix(l, "msgstr")) // Check for indexed Translation forms if strings.HasPrefix(l, "[") { idx := strings.Index(l, "]") if idx == -1 { // Skip wrong index formatting return } // Parse index i, err := strconv.Atoi(l[1:idx]) if err != nil { // Skip wrong index formatting return } // Parse Translation string po.domain.trBuffer.Trs[i], _ = strconv.Unquote(strings.TrimSpace(l[idx+1:])) // Loop return } // Save single Translation form under 0 index po.domain.trBuffer.Trs[0], _ = strconv.Unquote(l) } // parseString takes a well formatted string without prefix // and creates headers or attach multi-line strings when corresponding func (po *Po) parseString(l string, state parseState) { clean, _ := strconv.Unquote(l) switch state { case msgStr: // Append to last Translation found po.domain.trBuffer.Trs[len(po.domain.trBuffer.Trs)-1] += clean case msgID: // Multiline msgid - Append to current id po.domain.trBuffer.ID += clean case msgIDPlural: // Multiline msgid - Append to current id po.domain.trBuffer.PluralID += clean case msgCtxt: // Multiline context - Append to current context po.domain.ctxBuffer += clean } } // isValidLine checks for line prefixes to detect valid syntax. func (po *Po) isValidLine(l string) bool { // Check prefix valid := []string{ "\"", "msgctxt", "msgid", "msgid_plural", "msgstr", } for _, v := range valid { if strings.HasPrefix(l, v) { return true } } return false } gotext-1.5.0/po_test.go000066400000000000000000000366101374654437200150600ustar00rootroot00000000000000/* * 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" "os" "path" "testing" ) const ( translatedText = "Translated text" ) func TestPo_Get(t *testing.T) { // Create po object po := NewPo() // 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 != translatedText { t.Errorf("Expected '%s' but got '%s'", translatedText, 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 := ` msgid "" msgstr "" # Initial comment # Headers below "Language: en\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" # Some comment msgid "My text" msgstr "Translated text" # More comments msgid "Another string" msgstr "" # Multi-line msgid msgid "" "multi" "line" "id" msgstr "id with multiline content" # Multi-line msgid_plural msgid "" "multi" "line" "plural" "id" msgstr "plural id with multiline content" #Multi-line string msgid "Multi-line" msgstr "" "Multi " "line" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular: %s" msgstr[1] "This one is the plural: %s" msgstr[2] "And this is the second plural form: %s" 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" ` // Write PO content to file filename := path.Clean(os.TempDir() + string(os.PathSeparator) + "default.po") f, err := os.Create(filename) if err != nil { t.Fatalf("Can't create test file: %s", err.Error()) } defer f.Close() _, err = f.WriteString(str) if err != nil { t.Fatalf("Can't write to test file: %s", err.Error()) } // Create po object po := NewPo() // Try to parse a directory po.ParseFile(path.Clean(os.TempDir())) // Parse file po.ParseFile(filename) // Test translations tr := po.Get("My text") if tr != translatedText { t.Errorf("Expected '%s' but got '%s'", translatedText, tr) } v := "Variable" tr = po.Get("One with var: %s", v) if tr != "This one is the singular: Variable" { t.Errorf("Expected 'This one is the singular: Variable' but got '%s'", tr) } // Test multi-line id tr = po.Get("multilineid") if tr != "id with multiline content" { t.Errorf("Expected 'id with multiline content' but got '%s'", tr) } // Test multi-line plural id tr = po.Get("multilinepluralid") if tr != "plural id with multiline content" { t.Errorf("Expected 'plural id with multiline content' but got '%s'", tr) } // Test multi-line tr = po.Get("Multi-line") if tr != "Multi line" { t.Errorf("Expected 'Multi line' but got '%s'", tr) } // Test plural tr = po.GetN("One with var: %s", "Several with vars: %s", 2, v) if tr != "This one is the plural: Variable" { t.Errorf("Expected 'This one is the plural: Variable' but got '%s'", tr) } // Test 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) } tr = po.GetN("This is a test", "This are tests", 100) if tr != "This are tests" { t.Errorf("Expected 'This are tests' but got '%s'", tr) } // Test context translations v = "Test" tr = po.GetC("One with var: %s", "Ctx", v) if tr != "This one is the singular in a Ctx context: Test" { t.Errorf("Expected 'This one is the singular in a Ctx context: Test' but got '%s'", tr) } // Test plural tr = po.GetNC("One with var: %s", "Several with vars: %s", 17, "Ctx", v) if tr != "This one is the plural in a Ctx context: Test" { t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr) } // Test default plural vs singular return responses tr = po.GetN("Original", "Original plural", 4) if tr != "Original plural" { t.Errorf("Expected 'Original plural' but got '%s'", tr) } tr = po.GetN("Original", "Original plural", 1) if tr != "Original" { t.Errorf("Expected 'Original' but got '%s'", tr) } // Test empty Translation strings tr = po.Get("Empty Translation") if tr != "Empty Translation" { t.Errorf("Expected 'Empty Translation' but got '%s'", tr) } tr = po.Get("Empty plural form singular") if tr != "Singular translated" { t.Errorf("Expected 'Singular translated' but got '%s'", tr) } tr = po.GetN("Empty plural form singular", "Empty plural form", 1) if tr != "Singular translated" { t.Errorf("Expected 'Singular translated' but got '%s'", tr) } tr = po.GetN("Empty plural form singular", "Empty plural form", 2) if tr != "Empty plural form" { t.Errorf("Expected 'Empty plural form' but got '%s'", tr) } // Test last Translation tr = po.Get("More") if tr != "More Translation" { t.Errorf("Expected 'More Translation' but got '%s'", tr) } } func TestPlural(t *testing.T) { // Set PO content str := ` msgid "" msgstr "" "Plural-Forms: nplurals=2; plural=(n != 1);\n" msgid "Singular: %s" msgid_plural "Plural: %s" msgstr[0] "TR Singular: %s" msgstr[1] "TR Plural: %s" msgstr[2] "TR Plural 2: %s" ` // Create po object po := NewPo() 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 := NewPo() po.Parse([]byte(str)) v := "Var" tr := po.GetN("Singular: %s", "Plural: %s", 2, v) if tr != "TR Plural: Var" { t.Errorf("Expected 'TR Plural: Var' but got '%s'", tr) } tr = po.GetN("Singular: %s", "Plural: %s", 1, v) if tr != "TR Singular: Var" { t.Errorf("Expected 'TR Singular: Var' but got '%s'", tr) } } func TestPoHeaders(t *testing.T) { // Set PO content str := ` msgid "" msgstr "" # Initial comment # Headers below "Language: en\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" # Some comment msgid "Example" msgstr "Translated example" ` // Create po object po := NewPo() // Parse po.Parse([]byte(str)) // Check headers expected if po.Language != "en" { t.Errorf("Expected 'Language: en' but got '%s'", po.Language) } do := po.GetDomain() // Check headers expected if do.PluralForms != "nplurals=2; plural=(n != 1);" { t.Errorf("Expected 'Plural-Forms: nplurals=2; plural=(n != 1);' but got '%s'", do.PluralForms) } } func TestMissingPoHeadersSupport(t *testing.T) { // Set PO content str := ` msgid "Example" msgstr "Translated example" ` // Create po object po := NewPo() // Parse po.Parse([]byte(str)) // Check Translation expected if po.Get("Example") != "Translated example" { t.Errorf("Expected 'Translated example' but got '%s'", po.Get("Example")) } } type pluralTest struct { form, num int } func pluralExpected(t *testing.T, pluralTests []pluralTest, domain *Domain) { t.Helper() for _, pt := range pluralTests { pt := pt t.Run(fmt.Sprintf("pluralForm(%d)", pt.num), func(t *testing.T) { n := domain.pluralForm(pt.num) if n != pt.form { t.Errorf("Expected %d for pluralForm(%d), got %d", pt.form, pt.num, n) } }) } } func TestPluralFormsSingle(t *testing.T) { // Single form str := ` msgid "" msgstr "" "Plural-Forms: nplurals=1; plural=0;" # Some comment msgid "Singular" msgid_plural "Plural" msgstr[0] "Singular form" msgstr[1] "Plural form 1" msgstr[2] "Plural form 2" msgstr[3] "Plural form 3" ` // Create po object po := NewPo() // Parse po.Parse([]byte(str)) pluralTests := []pluralTest{ {form: 0, num: 0}, {form: 0, num: 1}, {form: 0, num: 2}, {form: 0, num: 3}, {form: 0, num: 50}, } pluralExpected(t, pluralTests, po.GetDomain()) } func TestPluralForms2(t *testing.T) { // 2 forms str := ` msgid "" msgstr "" "Plural-Forms: nplurals=2; plural=n != 1;" # Some comment msgid "Singular" msgid_plural "Plural" msgstr[0] "Singular form" msgstr[1] "Plural form 1" msgstr[2] "Plural form 2" msgstr[3] "Plural form 3" ` // Create po object po := NewPo() // Parse po.Parse([]byte(str)) pluralTests := []pluralTest{ {form: 1, num: 0}, {form: 0, num: 1}, {form: 1, num: 2}, {form: 1, num: 3}, } pluralExpected(t, pluralTests, po.GetDomain()) } func TestPluralForms3(t *testing.T) { // 3 forms str := ` msgid "" msgstr "" "Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2;" # Some comment msgid "Singular" msgid_plural "Plural" msgstr[0] "Singular form" msgstr[1] "Plural form 1" msgstr[2] "Plural form 2" msgstr[3] "Plural form 3" ` // Create po object po := NewPo() // Parse po.Parse([]byte(str)) pluralTests := []pluralTest{ {form: 2, num: 0}, {form: 0, num: 1}, {form: 1, num: 2}, {form: 1, num: 3}, {form: 1, num: 100}, {form: 1, num: 49}, } pluralExpected(t, pluralTests, po.GetDomain()) } func TestPluralFormsSpecial(t *testing.T) { // 3 forms special str := ` msgid "" msgstr "" "Plural-Forms: nplurals=3;" "plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;" # Some comment msgid "Singular" msgid_plural "Plural" msgstr[0] "Singular form" msgstr[1] "Plural form 1" msgstr[2] "Plural form 2" msgstr[3] "Plural form 3" ` // Create po object po := NewPo() // Parse po.Parse([]byte(str)) pluralTests := []pluralTest{ {form: 0, num: 1}, {form: 1, num: 2}, {form: 1, num: 4}, {form: 2, num: 0}, {form: 2, num: 1000}, } pluralExpected(t, pluralTests, po.GetDomain()) } func TestTranslationObject(t *testing.T) { tr := NewTranslation() str := tr.Get() if str != "" { t.Errorf("Expected '' but got '%s'", str) } // Set id tr.ID = "Text" str = tr.Get() // Get again if str != "Text" { t.Errorf("Expected 'Text' but got '%s'", str) } } func TestPoRace(t *testing.T) { // Set PO content str := `# Some comment msgid "My text" msgstr "Translated text" # More comments msgid "Another string" msgstr "" msgid "One with var: %s" msgid_plural "Several with vars: %s" msgstr[0] "This one is the singular: %s" msgstr[1] "This one is the plural: %s" msgstr[2] "And this is the second plural form: %s" ` // Create Po object po := NewPo() // Create sync channels pc := make(chan bool) rc := make(chan bool) // Parse po content in a goroutine go func(po *Po, done chan bool) { po.Parse([]byte(str)) done <- true }(po, pc) // Read some Translation on a goroutine go func(po *Po, done chan bool) { po.Get("My text") done <- true }(po, rc) // Read something at top level po.Get("My text") // Wait for goroutines to finish <-pc <-rc } func TestNewPoTranslatorRace(t *testing.T) { // Create Po object po := NewPo() // 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 }(po, pc) // Read some Translation on a goroutine go func(mo Translator, done chan bool) { mo.Get("My text") done <- true }(po, rc) // Read something at top level po.Get("My text") // Wait for goroutines to finish <-pc <-rc } func TestPoBinaryEncoding(t *testing.T) { // Create po objects po := NewPo() po2 := NewPo() // 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) } } func TestPoTextEncoding(t *testing.T) { // Create po objects po := NewPo() po2 := NewPo() // Parse file po.ParseFile("fixtures/en_US/default.po") if _, ok := po.Headers["Pot-Creation-Date"]; ok { t.Errorf("Expected non-canonicalised header, got canonicalised") } else { if _, ok = po.Headers["POT-Creation-Date"]; !ok { t.Errorf("Expected non-canonicalised header, but it was missing") } } // Round-trip buff, err := po.MarshalText() if err != nil { t.Fatal(err) } po2.Parse(buff) for k, v := range po.Headers { if v2, ok := po2.Headers[k]; ok { for i, value := range v { if value != v2[i] { t.Errorf("TestPoTextEncoding: Header Difference for %s: %s vs %s", k, value, v2[i]) } } } } // Test translations tr := po2.Get("My text") if tr != "Translated text" { t.Errorf("Expected 'Translated text' but got '%s'", tr) } tr = po2.Get("language") if tr != "en_US" { t.Errorf("Expected 'en_US' but got '%s'", tr) } tr = po2.Get("Some random") if tr != "Some random translation" { t.Errorf("Expected 'Some random translation' but got '%s'", tr) } v := "Test" tr = po.GetC("One with var: %s", "Ctx", v) if tr != "This one is the singular in a Ctx context: Test" { t.Errorf("Expected 'This one is the singular in a Ctx context: Test' but got '%s'", tr) } tr = po.GetNC("One with var: %s", "Several with vars: %s", 17, "Ctx", v) if tr != "This one is the plural in a Ctx context: Test" { t.Errorf("Expected 'This one is the plural in a Ctx context: Test' but got '%s'", tr) } // Another kind of round-trip po.Set("My text", "Translated text") po.Set("language", "en_US") // But remove 'the' po.SetNC("One with var: %s", "Several with vars: %s", "Ctx", 1, "This one is singular in a Ctx context: %s") po.SetNC("One with var: %s", "Several with vars: %s", "Ctx", 17, "This one is plural in a Ctx context: %s") po.DropStaleTranslations() buff, err = po.MarshalText() if err != nil { t.Fatal(err) } po2 = NewPo() po2.Parse(buff) for k, v := range po.Headers { if v2, ok := po2.Headers[k]; ok { for i, value := range v { if value != v2[i] { t.Errorf("Only translations should have been dropped, not headers") } } } } tr = po2.Get("My text") if tr != "Translated text" { t.Errorf("Expected 'Translated text' but got '%s'", tr) } tr = po2.Get("language") if tr != "en_US" { t.Errorf("Expected 'en_US' but got '%s'", tr) } tr = po2.Get("Some random") if tr == "Some random translation" || tr != "Some random" { t.Errorf("Expected 'Some random' translation to be dropped; was present") } // With 'the' removed? v = "Test" tr = po.GetC("One with var: %s", "Ctx", v) if tr != "This one is singular in a Ctx context: Test" { t.Errorf("Expected 'This one is singular in a Ctx context: Test' but got '%s'", tr) } tr = po.GetNC("One with var: %s", "Several with vars: %s", 17, "Ctx", v) if tr != "This one is plural in a Ctx context: Test" { t.Errorf("Expected 'This one is plural in a Ctx context: Test' but got '%s'", tr) } } gotext-1.5.0/translation.go000066400000000000000000000031331374654437200157330ustar00rootroot00000000000000/* * 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 Refs []string dirty bool } // NewTranslation returns the Translation object and initialized it. func NewTranslation() *Translation { return &Translation{ Trs: make(map[int]string), } } func NewTranslationWithRefs(refs []string) *Translation { return &Translation{ Trs: make(map[int]string), Refs: refs, } } func (t *Translation) IsStale() bool { return t.dirty == false } func (t *Translation) SetRefs(refs []string) { t.Refs = refs t.dirty = true } func (t *Translation) Set(str string) { t.Trs[0] = str t.dirty = true } // 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 } func (t *Translation) SetN(n int, str string) { t.Trs[n] = str t.dirty = true } // 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 } gotext-1.5.0/translator.go000066400000000000000000000045561374654437200156000ustar00rootroot00000000000000/* * 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 ( "errors" "io/ioutil" "os" ) // 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 MarshalBinary() ([]byte, error) UnmarshalBinary([]byte) error GetDomain() *Domain } // TranslatorEncoding is used as intermediary storage to encode Translator objects to Gob. type TranslatorEncoding struct { // Headers storage Headers HeaderMap // 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 unmarshalling the TranslatorEncoding object. // Internally uses a Po object as it should be switchable with Mo objects without problem. // External Translator implementations should be able to serialize into a TranslatorEncoding object in order to // deserialize into a Po-compatible object. func (te *TranslatorEncoding) GetTranslator() Translator { po := NewPo() po.domain = NewDomain() po.domain.Headers = te.Headers po.domain.Language = te.Language po.domain.PluralForms = te.PluralForms po.domain.nplurals = te.Nplurals po.domain.plural = te.Plural po.domain.translations = te.Translations po.domain.contexts = te.Contexts return po } //getFileData reads a file and returns the byte slice after doing some basic sanity checking func getFileData(f string) ([]byte, error) { // Check if file exists info, err := os.Stat(f) if err != nil { return nil, err } // Check that isn't a directory if info.IsDir() { return nil, errors.New("cannot parse a directory") } return ioutil.ReadFile(f) }