Compare commits

...

16 Commits

Author SHA1 Message Date
f282cadccd Update translation files
Some checks failed
continuous-integration/drone/push Build is failing
Updated by "Update PO files to match POT (msgmerge)" add-on in Weblate.

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/
2025-08-04 19:56:01 +00:00
3wc
932c50ede3 Chore: regenerate gettext catalogue template
[ci skip]
2025-08-04 19:55:57 +00:00
3wc
cdf3f7d638 And fix branch for pushing 😬
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-04 20:52:33 +01:00
3wc
57b4b8e509 And fix apt install ...
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-04 20:48:01 +01:00
3wc
092ae6e700 Fix openssh-client package name
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-04 20:41:02 +01:00
3wc
436a938373 Whoops, add missing SSH / git stuffs
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-04 20:36:15 +01:00
3wc
41cb05ccbd Risky git commit step for catalogue template
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-08-04 20:32:51 +01:00
3wc
f53911f442 Skip vendor files for drone-xgotext
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-04 20:24:17 +01:00
3wc
05788e44e0 Appease formatter
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-08-04 20:20:12 +01:00
3wc
e4e5f4f1f6 Another string wrapped
Some checks failed
continuous-integration/drone/push Build is failing
2025-08-04 20:19:50 +01:00
3wc
937656a9fa Mark another string for translation
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone Build is passing
2025-08-04 17:34:47 +01:00
274c3a64c9 Translated using Weblate (Spanish)
Some checks reported errors
continuous-integration/drone/push Build was killed
Currently translated at 100.0% (1 of 1 strings)

Translation: Co-op Cloud/abra
Translate-URL: https://translate.coopcloud.tech/projects/co-op-cloud/abra/es/
2025-08-04 16:28:29 +00:00
3wc
d4d0ddf74a Add step to run xgotext
Some checks reported errors
continuous-integration/drone/push Build was killed
2025-08-04 17:28:16 +01:00
3wc
bc948988a1 Add stub es catalogue
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-04 15:12:29 +01:00
3wc
3a229d9dc9 Add default POT catalogue, don't alias gotext.Get
All checks were successful
continuous-integration/drone/push Build is passing
2025-08-04 15:10:07 +01:00
d44b18d7be wip: feat: translate
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2025-08-04 16:02:45 +02:00
29 changed files with 4014 additions and 8 deletions

View File

@ -18,6 +18,32 @@ steps:
depends_on:
- make check
- name: find updated translatable strings
image: git.coopcloud.tech/toolshed/drone-xgotext
depends_on:
- make check
settings:
exclude: "vendor,.git,.bats"
when:
event: [push]
- name: commit catalogue template changes
image: debian:bookworm
environment:
SSH_KEY:
from_secret: abra_bot_deploy_key
GIT_SSH_COMMAND: "ssh -o 'PubkeyAcceptedKeyTypes +ssh-rsa'"
commands:
- apt update && DEBIAN_FRONTEND=noninteractive apt install -y git openssh-client
- mkdir $HOME/.ssh/
- eval `ssh-agent`
- echo "$SSH_KEY" | ssh-add -
- ssh-keyscan -p 2222 -t rsa git.coopcloud.tech >> $HOME/.ssh/known_hosts
- chmod -R go-rwx $HOME/.ssh
- "git commit -a -m 'Chore: regenerate gettext catalogue template' -m '[ci skip]' && git push -u origin $DRONE_COMMIT_BRANCH"
depends_on:
- find updated translatable strings
- name: fetch
image: docker:git
commands:
@ -26,7 +52,7 @@ steps:
- make check
- make test
when:
event: tag
event: [tag]
- name: release
image: goreleaser/goreleaser:v2.5.1

View File

@ -1,11 +1,12 @@
package app
import (
"github.com/leonelquinteros/gotext"
"github.com/spf13/cobra"
)
var AppCommand = &cobra.Command{
Use: "app [cmd] [args] [flags]",
Aliases: []string{"a"},
Short: "Manage apps",
Short: gotext.Get("Manage apps"),
}

View File

@ -13,6 +13,7 @@ import (
"coopcloud.tech/abra/pkg/formatter"
"coopcloud.tech/abra/pkg/log"
"coopcloud.tech/tagcmp"
"github.com/leonelquinteros/gotext"
"github.com/spf13/cobra"
)
@ -41,7 +42,7 @@ type serverStatus struct {
var AppListCommand = &cobra.Command{
Use: "list [flags]",
Aliases: []string{"ls"},
Short: "List all managed apps",
Short: gotext.Get("List all managed apps"),
Long: `Generate a report of all managed apps.
Use "--status/-S" flag to query all servers for the live deployment status.`,

View File

@ -21,12 +21,14 @@ import (
"github.com/docker/docker/api/types/filters"
dockerClient "github.com/docker/docker/client"
"github.com/spf13/cobra"
"github.com/leonelquinteros/gotext"
)
var AppPsCommand = &cobra.Command{
Use: "ps <domain> [flags]",
Aliases: []string{"p"},
Short: "Check app deployment status",
Short: gotext.Get("Check app deployment status"),
Args: cobra.ExactArgs(1),
ValidArgsFunction: func(
cmd *cobra.Command,

5
go.mod
View File

@ -1,8 +1,8 @@
module coopcloud.tech/abra
go 1.23.0
go 1.23.5
toolchain go1.23.1
toolchain go1.24.1
require (
coopcloud.tech/tagcmp v0.0.0-20230809071031-eb3e7758d4eb
@ -17,6 +17,7 @@ require (
github.com/docker/go-units v0.5.0
github.com/go-git/go-git/v5 v5.14.0
github.com/google/go-cmp v0.7.0
github.com/leonelquinteros/gotext v1.7.2
github.com/moby/sys/signal v0.7.1
github.com/moby/term v0.5.2
github.com/pkg/errors v0.9.1

2
go.sum
View File

@ -611,6 +611,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/leonelquinteros/gotext v1.7.2 h1:bDPndU8nt+/kRo1m4l/1OXiiy2v7Z7dfPQ9+YP7G1Mc=
github.com/leonelquinteros/gotext v1.7.2/go.mod h1:9/haCkm5P7Jay1sxKDGJ5WIg4zkz8oZKw4ekNpALob8=
github.com/lib/pq v0.0.0-20150723085316-0dad96c0b94f/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
github.com/linuxkit/virtsock v0.0.0-20201010232012-f8cee7dfc7a3/go.mod h1:3r6x7q95whyfWQpmGZTu3gk3v2YkMi05HEzl7Tf7YEo=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=

20
locales/default.pot Normal file
View File

@ -0,0 +1,20 @@
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"
#: cli/app/ps.go:31
msgid "Check app deployment status"
msgstr ""
#: cli/app/list.go:45
msgid "List all managed apps"
msgstr ""
#: cli/app/app.go:11
msgid "Manage apps"
msgstr ""

View File

@ -0,0 +1,30 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-08-04 14:15+0000\n"
"PO-Revision-Date: 2025-08-04 14:15+0000\n"
"Last-Translator: 3wordchant <3wc.coopcloud@doesthisthing.work>\n"
"Language-Team: Spanish <https://translate.coopcloud.tech/projects/co-op-"
"cloud/abra/es/>\n"
"Language: es\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: ENCODING\n"
"Plural-Forms: nplurals=2; plural=n != 1;\n"
"X-Generator: Weblate 5.12.2\n"
#: cli/app/ps.go:31
msgid "Check app deployment status"
msgstr ""
#: cli/app/list.go:45
#, fuzzy
#| msgid "Manage apps"
msgid "List all managed apps"
msgstr "Gestionar aplicaciones"
#: cli/app/app.go:11
msgid "Manage apps"
msgstr "Gestionar aplicaciones"

30
pkg/lang/lang.go Normal file
View File

@ -0,0 +1,30 @@
package lang
import (
"os"
"strings"
)
func GetLocale() string {
if loc := os.Getenv("LC_MESSAGES"); loc != "" {
return NormalizeLocale(loc)
}
if loc := os.Getenv("LANG"); loc != "" {
return NormalizeLocale(loc)
}
return "C.UTF-8"
}
func NormalizeLocale(loc string) string {
if idx := strings.Index(loc, "."); idx != -1 {
return loc[:idx]
}
if idx := strings.Index(loc, "@"); idx != -1 {
return loc[:idx]
}
return loc
}

32
vendor/github.com/leonelquinteros/gotext/.gitignore generated vendored Normal file
View File

@ -0,0 +1,32 @@
# Local IDE
.project
.settings
.buildpath
.idea
# Compiled Object files, Static and Dynamic libs (Shared Objects)
*.o
*.a
*.so
# Folders
_obj
_test
cli/xgotext/fixtures/out
# Architecture specific extensions/prefixes
*.[568vq]
[568vq].out
*.cgo1.go
*.cgo2.c
_cgo_defun.c
_cgo_gotypes.go
_cgo_export.*
_testmain.go
*.exe
*.test
*.prof
*.DS_Store

View File

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at leonel.quinteros@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

View File

@ -0,0 +1,19 @@
# CONTRIBUTING
This open source project welcomes everybody that wants to contribute to it by implementing new features, fixing bugs, testing, creating documentation or simply talk about it.
Most contributions will start by creating a new Issue to discuss what is the contribution about and to agree on the steps to move forward.
## Issues
All issues reports are welcome. Open a new Issue whenever you want to report a bug, request a change or make a proposal.
This should be your start point of contribution.
## Pull Requests
If you have any changes that can be merged, feel free to send a Pull Request.
Usually, you'd want to create a new Issue to discuss about the change you want to merge and why it's needed or what it solves.

55
vendor/github.com/leonelquinteros/gotext/LICENSE generated vendored Normal file
View File

@ -0,0 +1,55 @@
The MIT License (MIT)
Copyright (c) 2016 Leonel Quinteros
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
Package `plurals`
Original:
https://github.com/ojii/gettext.go/tree/b6dae1d7af8a8441285e42661565760b530a8a57/pluralforms
License:
https://raw.githubusercontent.com/ojii/gettext.go/b6dae1d7af8a8441285e42661565760b530a8a57/LICENSE
Copyright (c) 2016, Jonas Obrist
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
* Neither the name of Jonas Obrist nor the
names of its contributors may be used to endorse or promote products
derived from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY
DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

266
vendor/github.com/leonelquinteros/gotext/README.md generated vendored Normal file
View File

@ -0,0 +1,266 @@
[![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)
[![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 package documentation at (https://pkg.go.dev/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.
# 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 := gotext.NewPo()
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"
}
```
# 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...
# 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.

866
vendor/github.com/leonelquinteros/gotext/domain.go generated vendored Normal file
View File

@ -0,0 +1,866 @@
package gotext
import (
"bytes"
"encoding/gob"
"fmt"
"regexp"
"sort"
"strconv"
"strings"
"sync"
"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
// 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
contextTranslations 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
customPluralResolver func(int) int
}
// HeaderMap preserves MIMEHeader behaviour, without the canonicalisation
type HeaderMap map[string][]string
// Add key/value pair to HeaderMap
func (m HeaderMap) Add(key, value string) {
m[key] = append(m[key], value)
}
// Del key from HeaderMap
func (m HeaderMap) Del(key string) {
delete(m, key)
}
// Get value for key from HeaderMap
func (m HeaderMap) Get(key string) string {
if m == nil {
return ""
}
v := m[key]
if len(v) == 0 {
return ""
}
return v[0]
}
// Set key/value pair in HeaderMap
func (m HeaderMap) Set(key, value string) {
m[key] = []string{value}
}
// Values returns all values for a given key from HeaderMap
func (m HeaderMap) Values(key string) []string {
if m == nil {
return nil
}
return m[key]
}
// NewDomain creates a new Domain instance
func NewDomain() *Domain {
domain := new(Domain)
domain.Headers = make(HeaderMap)
domain.headerComments = make([]string, 0)
domain.translations = make(map[string]*Translation)
domain.contextTranslations = make(map[string]map[string]*Translation)
domain.pluralTranslations = make(map[string]*Translation)
return domain
}
// SetPluralResolver sets a custom plural resolver function
func (do *Domain) SetPluralResolver(f func(int) int) {
do.customPluralResolver = f
}
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 {
if do.customPluralResolver != nil {
return do.customPluralResolver(n)
}
/* 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.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
}
}
}
}
// DropStaleTranslations 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.contextTranslations {
for id, trans := range ctx {
if trans.IsStale() {
delete(ctx, id)
}
}
if len(ctx) == 0 {
delete(do.contextTranslations, name)
}
}
for id, trans := range do.translations {
if trans.IsStale() {
delete(do.translations, id)
}
}
}
// SetRefs 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
}
}
// GetRefs 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[id] = trans
}
}
// Get retrieves the Translation for the given string.
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 FormatString(do.translations[str].Get(), vars...)
}
}
// Return the same we received by default
return FormatString(str, vars...)
}
// Append retrieves the 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) Append(b []byte, str string, vars ...interface{}) []byte {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations != nil {
if _, ok := do.translations[str]; ok {
return Appendf(b, do.translations[str].Get(), vars...)
}
}
// Return the same we received by default
return Appendf(b, str, vars...)
}
// SetN sets 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[id] = 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 FormatString(do.translations[str].GetN(do.pluralForm(n)), vars...)
}
}
// Parse plural forms to distinguish between plural and singular
if do.pluralForm(n) == 0 {
return FormatString(str, vars...)
}
return FormatString(plural, vars...)
}
// AppendN adds 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) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
// Sync read
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations != nil {
if _, ok := do.translations[str]; ok {
return Appendf(b, do.translations[str].GetN(do.pluralForm(n)), vars...)
}
}
// Parse plural forms to distinguish between plural and singular
if do.pluralForm(n) == 0 {
return Appendf(b, str, vars...)
}
return Appendf(b, plural, vars...)
}
// SetC sets 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.contextTranslations[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.contextTranslations[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.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return FormatString(do.contextTranslations[ctx][str].Get(), vars...)
}
}
}
}
// Return the string we received by default
return FormatString(str, vars...)
}
// AppendC 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) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return Appendf(b, do.contextTranslations[ctx][str].Get(), vars...)
}
}
}
}
// Return the string we received by default
return Appendf(b, str, vars...)
}
// SetNC sets 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.contextTranslations[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.contextTranslations[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.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return FormatString(do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
}
}
}
}
if n == 1 {
return FormatString(str, vars...)
}
return FormatString(plural, vars...)
}
// AppendNC 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) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.contextTranslations != nil {
if _, ok := do.contextTranslations[ctx]; ok {
if do.contextTranslations[ctx] != nil {
if _, ok := do.contextTranslations[ctx][str]; ok {
return Appendf(b, do.contextTranslations[ctx][str].GetN(do.pluralForm(n)), vars...)
}
}
}
}
if n == 1 {
return Appendf(b, str, vars...)
}
return Appendf(b, plural, vars...)
}
// IsTranslated reports whether a string is translated
func (do *Domain) IsTranslated(str string) bool {
return do.IsTranslatedN(str, 1)
}
// IsTranslatedN reports whether a plural string is translated
func (do *Domain) IsTranslatedN(str string, n int) bool {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.translations == nil {
return false
}
tr, ok := do.translations[str]
if !ok {
return false
}
return tr.IsTranslatedN(do.pluralForm(n))
}
// IsTranslatedC reports whether a context string is translated
func (do *Domain) IsTranslatedC(str, ctx string) bool {
return do.IsTranslatedNC(str, 1, ctx)
}
// IsTranslatedNC reports whether a plural context string is translated
func (do *Domain) IsTranslatedNC(str string, n int, ctx string) bool {
do.trMutex.RLock()
defer do.trMutex.RUnlock()
if do.contextTranslations == nil {
return false
}
translations, ok := do.contextTranslations[ctx]
if !ok {
return false
}
tr, ok := translations[str]
if !ok {
return false
}
return tr.IsTranslatedN(do.pluralForm(n))
}
// 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
}
// GetCtxTranslations returns a copy of every translation in the domain with context
func (do *Domain) GetCtxTranslations() map[string]map[string]*Translation {
all := make(map[string]map[string]*Translation, len(do.contextTranslations))
do.trMutex.RLock()
defer do.trMutex.RUnlock()
for ctx, translations := range do.contextTranslations {
for msgID, trans := range 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
}
if all[ctx] == nil {
all[ctx] = make(map[string]*Translation)
}
all[ctx][msgID] = newTrans
}
}
return all
}
// SourceReference is a struct to hold source reference information
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.contextTranslations {
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 \"" + EscapeSpecialCharacters(trans.ID) + "\"")
} else {
buf.WriteString("\nmsgctxt \"" + EscapeSpecialCharacters(ref.context) + "\"\nmsgid \"" + EscapeSpecialCharacters(trans.ID) + "\"")
}
if trans.PluralID == "" {
buf.WriteString("\nmsgstr \"" + EscapeSpecialCharacters(trans.Trs[0]) + "\"")
} else {
buf.WriteString("\nmsgid_plural \"" + trans.PluralID + "\"")
for i, tr := range trans.Trs {
buf.WriteString("\nmsgstr[" + EscapeSpecialCharacters(strconv.Itoa(i)) + "] \"" + tr + "\"")
}
}
}
return buf.Bytes(), nil
}
// EscapeSpecialCharacters escapes special characters in a string
func EscapeSpecialCharacters(s string) string {
s = regexp.MustCompile(`([^\\])(")`).ReplaceAllString(s, "$1\\\"") // Escape non-escaped double quotation marks
if strings.Count(s, "\n") == 0 {
return s
}
// Handle EOL and multi-lines
// Only one line, but finishing with \n
if strings.Count(s, "\n") == 1 && strings.HasSuffix(s, "\n") {
return strings.ReplaceAll(s, "\n", "\\n")
}
elems := strings.Split(s, "\n")
// Skip last element for multiline which is an empty
var shouldEndWithEOL bool
if elems[len(elems)-1] == "" {
elems = elems[:len(elems)-1]
shouldEndWithEOL = true
}
data := []string{(`"`)}
for i, v := range elems {
l := fmt.Sprintf(`"%s\n"`, v)
// Last element without EOL
if i == len(elems)-1 && !shouldEndWithEOL {
l = fmt.Sprintf(`"%s"`, v)
}
// Remove finale " to last element as the whole string will be quoted
if i == len(elems)-1 {
l = strings.TrimSuffix(l, `"`)
}
data = append(data, l)
}
return strings.Join(data, "\n")
}
// 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.contextTranslations
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.contextTranslations = obj.Contexts
if expr, err := plurals.Compile(do.plural); err == nil {
do.pluralforms = expr
}
return nil
}

427
vendor/github.com/leonelquinteros/gotext/gotext.go generated vendored Normal file
View File

@ -0,0 +1,427 @@
/*
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"
"strings"
"sync"
)
// Global environment variables
type config struct {
sync.RWMutex
// Path to library directory where all locale directories and Translation files are.
library string
// Default domain to look at when no domain is specified. Used by package level functions.
domain string
// Language set.
languages []string
// Storage for package level methods
locales []*Locale
}
var globalConfig *config
// FallbackLocale is the default language to be used when no language is set.
var FallbackLocale = "en_US"
func init() {
// Init default configuration
globalConfig = &config{
domain: "default",
languages: []string{FallbackLocale},
library: "/usr/local/share/locale",
locales: nil,
}
// Register Translator types for gob encoding
gob.Register(TranslatorEncoding{})
}
// loadLocales creates a new Locale object for every language (specified using Configure)
// at package level based on the configuration of global configuration .
// It is called when trying to use Get or GetD methods.
func loadLocales(rebuildCache bool) {
globalConfig.Lock()
if globalConfig.locales == nil || rebuildCache {
var locales []*Locale
for _, language := range globalConfig.languages {
locales = append(locales, NewLocale(globalConfig.library, language))
}
globalConfig.locales = locales
}
for _, locale := range globalConfig.locales {
if _, ok := locale.Domains[globalConfig.domain]; !ok || rebuildCache {
locale.AddDomain(globalConfig.domain)
}
locale.SetDomain(globalConfig.domain)
}
globalConfig.Unlock()
}
// GetDomain is the domain getter for the package configuration
func GetDomain() string {
var dom string
globalConfig.RLock()
if globalConfig.locales != nil {
// All locales have the same domain
dom = globalConfig.locales[0].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.locales != nil {
for _, locale := range globalConfig.locales {
locale.SetDomain(dom)
}
}
globalConfig.Unlock()
loadLocales(true)
}
// GetLanguage returns the language gotext will translate into.
// If multiple languages have been supplied, the first one will be returned.
// If no language has been supplied, the fallback will be returned.
func GetLanguage() string {
languages := GetLanguages()
if len(languages) == 0 {
return FallbackLocale
}
return languages[0]
}
// GetLanguages returns all languages that have been supplied.
func GetLanguages() []string {
globalConfig.RLock()
defer globalConfig.RUnlock()
return globalConfig.languages
}
// SetLanguage sets the language code (or colon separated language codes) to be used at package level.
// It reloads the corresponding Translation file.
func SetLanguage(lang string) {
globalConfig.Lock()
var languages []string
for _, language := range strings.Split(lang, ":") {
languages = append(languages, SimplifiedLocale(language))
}
globalConfig.languages = languages
globalConfig.Unlock()
loadLocales(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 locale 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()
loadLocales(true)
}
// GetLocales returns the locales that have been set for the package configuration.
func GetLocales() []*Locale {
globalConfig.RLock()
defer globalConfig.RUnlock()
return globalConfig.locales
}
// GetStorage is the locale storage getter for the package configuration.
//
// Deprecated: Storage has been renamed to Locale for consistency, use GetLocales instead.
func GetStorage() *Locale {
locales := GetLocales()
if len(locales) > 0 {
return locales[0]
}
return nil
}
// SetLocales allows for overriding the global Locale objects with ones built manually with
// NewLocale(). This makes it possible to attach custom Domain objects from in-memory po/mo.
// The library, language and domain of the first Locale will set the default global configuration.
func SetLocales(locales []*Locale) {
globalConfig.Lock()
defer globalConfig.Unlock()
globalConfig.locales = locales
globalConfig.library = locales[0].path
globalConfig.domain = locales[0].defaultDomain
var languages []string
for _, locale := range locales {
languages = append(languages, locale.lang)
}
globalConfig.languages = languages
}
// SetStorage allows overriding the global Locale object with one built manually with NewLocale().
//
// Deprecated: Storage has been renamed to Locale for consistency, use SetLocales instead.
func SetStorage(locale *Locale) {
SetLocales([]*Locale{locale})
}
// 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
var languages []string
for _, language := range strings.Split(lang, ":") {
languages = append(languages, SimplifiedLocale(language))
}
globalConfig.languages = languages
globalConfig.domain = dom
globalConfig.Unlock()
loadLocales(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 Locales
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if _, ok := locale.Domains[dom]; !ok {
locale.AddDomain(dom)
}
if !locale.IsTranslatedD(dom, str) && i < (len(globalConfig.locales)-1) {
continue
}
tr = locale.GetD(dom, str, vars...)
break
}
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 Locales
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if _, ok := locale.Domains[dom]; !ok {
locale.AddDomain(dom)
}
if !locale.IsTranslatedND(dom, str, n) && i < (len(globalConfig.locales)-1) {
continue
}
tr = locale.GetND(dom, str, plural, n, vars...)
break
}
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 Locales
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if !locale.IsTranslatedDC(dom, str, ctx) && i < (len(globalConfig.locales)-1) {
continue
}
tr = locale.GetDC(dom, str, ctx, vars...)
break
}
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 Locales
loadLocales(false)
// Return Translation
globalConfig.RLock()
defer globalConfig.RUnlock()
var tr string
for i, locale := range globalConfig.locales {
if !locale.IsTranslatedNDC(dom, str, n, ctx) && i < (len(globalConfig.locales)-1) {
continue
}
tr = locale.GetNDC(dom, str, plural, n, ctx, vars...)
break
}
return tr
}
// IsTranslated reports whether a string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslated(str string, langs ...string) bool {
return IsTranslatedND(GetDomain(), str, 1, langs...)
}
// IsTranslatedN reports whether a plural string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedN(str string, n int, langs ...string) bool {
return IsTranslatedND(GetDomain(), str, n, langs...)
}
// IsTranslatedD reports whether a domain string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedD(dom, str string, langs ...string) bool {
return IsTranslatedND(dom, str, 1, langs...)
}
// IsTranslatedND reports whether a plural domain string is translated in any of given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedND(dom, str string, n int, langs ...string) bool {
if len(langs) == 0 {
langs = GetLanguages()
}
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
for _, lang := range langs {
lang = SimplifiedLocale(lang)
for _, supportedLocale := range globalConfig.locales {
if lang != supportedLocale.GetActualLanguage(dom) {
continue
}
return supportedLocale.IsTranslatedND(dom, str, n)
}
}
return false
}
// IsTranslatedC reports whether a context string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedC(str, ctx string, langs ...string) bool {
return IsTranslatedNDC(GetDomain(), str, 1, ctx, langs...)
}
// IsTranslatedNC reports whether a plural context string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedNC(str string, n int, ctx string, langs ...string) bool {
return IsTranslatedNDC(GetDomain(), str, n, ctx, langs...)
}
// IsTranslatedDC reports whether a domain context string is translated in given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedDC(dom, str, ctx string, langs ...string) bool {
return IsTranslatedNDC(dom, str, 0, ctx, langs...)
}
// IsTranslatedNDC reports whether a plural domain context string is translated in any of given languages.
// When the langs argument is omitted, the output of GetLanguages is used.
func IsTranslatedNDC(dom, str string, n int, ctx string, langs ...string) bool {
if len(langs) == 0 {
langs = GetLanguages()
}
loadLocales(false)
globalConfig.RLock()
defer globalConfig.RUnlock()
for _, lang := range langs {
lang = SimplifiedLocale(lang)
for _, locale := range globalConfig.locales {
if lang != locale.GetActualLanguage(dom) {
continue
}
return locale.IsTranslatedNDC(dom, str, n, ctx)
}
}
return false
}

96
vendor/github.com/leonelquinteros/gotext/helper.go generated vendored Normal file
View File

@ -0,0 +1,96 @@
/*
* 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)
}
// FormatString applies text formatting only when needed to parse variables.
func FormatString(str string, vars ...interface{}) string {
if len(vars) > 0 {
return fmt.Sprintf(str, vars...)
}
return str
}
// Appendf applies text formatting only when needed to parse variables.
func Appendf(b []byte, str string, vars ...interface{}) []byte {
if len(vars) > 0 {
return fmt.Appendf(b, str, vars...)
}
return append(b, 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
}

View File

@ -0,0 +1,25 @@
package gotext
// IsTranslatedIntrospector is able to determine whether a given string is translated.
// Examples of this introspector are Po and Mo, which are specific to their domain.
// Locale holds multiple domains and also implements IsTranslatedDomainIntrospector.
type IsTranslatedIntrospector interface {
IsTranslated(str string) bool
IsTranslatedN(str string, n int) bool
IsTranslatedC(str, ctx string) bool
IsTranslatedNC(str string, n int, ctx string) bool
}
// IsTranslatedDomainIntrospector is able to determine whether a given string is translated.
// Example of this introspector is Locale, which holds multiple domains.
// Simpler objects that are domain-specific, like Po or Mo, implement IsTranslatedIntrospector.
type IsTranslatedDomainIntrospector interface {
IsTranslated(str string) bool
IsTranslatedN(str string, n int) bool
IsTranslatedD(dom, str string) bool
IsTranslatedND(dom, str string, n int) bool
IsTranslatedC(str, ctx string) bool
IsTranslatedNC(str string, n int, ctx string) bool
IsTranslatedDC(dom, str, ctx string) bool
IsTranslatedNDC(dom, str string, n int, ctx string) bool
}

478
vendor/github.com/leonelquinteros/gotext/locale.go generated vendored Normal file
View File

@ -0,0 +1,478 @@
/*
* 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"
"io/fs"
"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
// optional fs to use
fs fs.FS
}
// 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),
}
}
// NewLocaleFS returns a Locale working with a fs.FS
func NewLocaleFS(l string, filesystem fs.FS) *Locale {
loc := NewLocale("", l)
loc.fs = filesystem
return loc
}
// NewLocaleFSWithPath returns a Locale working with a fs.FS on a p path folder.
func NewLocaleFSWithPath(l string, filesystem fs.FS, p string) *Locale {
loc := NewLocale("", l)
loc.fs = filesystem
loc.path = p
return loc
}
func (l *Locale) findExt(dom, ext string) string {
filename := path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
if l.fileExists(filename) {
return filename
}
if len(l.lang) > 2 {
filename = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
if l.fileExists(filename) {
return filename
}
}
filename = path.Join(l.path, l.lang, dom+"."+ext)
if l.fileExists(filename) {
return filename
}
if len(l.lang) > 2 {
filename = path.Join(l.path, l.lang[:2], dom+"."+ext)
if l.fileExists(filename) {
return filename
}
}
return ""
}
// GetActualLanguage inspects the filesystem and decides whether to strip
// a CC part of the ll_CC locale string.
func (l *Locale) GetActualLanguage(dom string) string {
extensions := []string{"mo", "po"}
var fp string
for _, ext := range extensions {
// 'll' (or 'll_CC') exists, and it was specified as-is
fp = path.Join(l.path, l.lang, "LC_MESSAGES", dom+"."+ext)
if l.fileExists(fp) {
return l.lang
}
// 'll' exists, but 'll_CC' was specified
if len(l.lang) > 2 {
fp = path.Join(l.path, l.lang[:2], "LC_MESSAGES", dom+"."+ext)
if l.fileExists(fp) {
return l.lang[:2]
}
}
// 'll' (or 'll_CC') exists outside of LC_category, and it was specified as-is
fp = path.Join(l.path, l.lang, dom+"."+ext)
if l.fileExists(fp) {
return l.lang
}
// 'll' exists outside of LC_category, but 'll_CC' was specified
if len(l.lang) > 2 {
fp = path.Join(l.path, l.lang[:2], dom+"."+ext)
if l.fileExists(fp) {
return l.lang[:2]
}
}
}
return ""
}
func (l *Locale) fileExists(filename string) bool {
if l.fs != nil {
_, err := fs.Stat(l.fs, filename)
return err == nil
}
_, err := os.Stat(filename)
return err == nil
}
// 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 = NewPoFS(l.fs)
// Parse file.
poObj.ParseFile(file)
} else {
file = l.findExt(dom, "mo")
if file != "" {
poObj = NewMoFS(l.fs)
// 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()
}
// GetLanguage is the lang getter for Locale configuration
func (l *Locale) GetLanguage() string {
l.RLock()
lang := l.lang
l.RUnlock()
return lang
}
// 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 FormatString(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 FormatString(str, vars...)
}
return FormatString(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 FormatString(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 FormatString(str, vars...)
}
return FormatString(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
}
// IsTranslated reports whether a string is translated
func (l *Locale) IsTranslated(str string) bool {
return l.IsTranslatedND(l.GetDomain(), str, 0)
}
// IsTranslatedN reports whether a plural string is translated
func (l *Locale) IsTranslatedN(str string, n int) bool {
return l.IsTranslatedND(l.GetDomain(), str, n)
}
// IsTranslatedD reports whether a domain string is translated
func (l *Locale) IsTranslatedD(dom, str string) bool {
return l.IsTranslatedND(dom, str, 0)
}
// IsTranslatedND reports whether a plural domain string is translated
func (l *Locale) IsTranslatedND(dom, str string, n int) bool {
l.RLock()
defer l.RUnlock()
if l.Domains == nil {
return false
}
translator, ok := l.Domains[dom]
if !ok {
return false
}
return translator.GetDomain().IsTranslatedN(str, n)
}
// IsTranslatedC reports whether a context string is translated
func (l *Locale) IsTranslatedC(str, ctx string) bool {
return l.IsTranslatedNDC(l.GetDomain(), str, 0, ctx)
}
// IsTranslatedNC reports whether a plural context string is translated
func (l *Locale) IsTranslatedNC(str string, n int, ctx string) bool {
return l.IsTranslatedNDC(l.GetDomain(), str, n, ctx)
}
// IsTranslatedDC reports whether a domain context string is translated
func (l *Locale) IsTranslatedDC(dom, str, ctx string) bool {
return l.IsTranslatedNDC(dom, str, 0, ctx)
}
// IsTranslatedNDC reports whether a plural domain context string is translated
func (l *Locale) IsTranslatedNDC(dom string, str string, n int, ctx string) bool {
l.RLock()
defer l.RUnlock()
if l.Domains == nil {
return false
}
translator, ok := l.Domains[dom]
if !ok {
return false
}
return translator.GetDomain().IsTranslatedNC(str, n, ctx)
}
// 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
}

319
vendor/github.com/leonelquinteros/gotext/mo.go generated vendored Normal file
View File

@ -0,0 +1,319 @@
/*
* 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"
"io/fs"
)
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 mo object
mo := gotext.NewMo()
// Parse .mo file
mo.ParseFile("/path/to/po/file/translations.mo")
// Get Translation
fmt.Println(mo.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
fs fs.FS
}
// NewMo should always be used to instantiate a new Mo object
func NewMo() *Mo {
mo := new(Mo)
mo.domain = NewDomain()
return mo
}
// NewMoFS works like NewMO but adds an optional fs.FS
func NewMoFS(filesystem fs.FS) *Mo {
mo := NewMo()
mo.fs = filesystem
return mo
}
// GetDomain returns the domain object
func (mo *Mo) GetDomain() *Domain {
return mo.domain
}
// Get returns the translation for the given string
func (mo *Mo) Get(str string, vars ...interface{}) string {
return mo.domain.Get(str, vars...)
}
// Append a translation string into the domain
func (mo *Mo) Append(b []byte, str string, vars ...interface{}) []byte {
return mo.domain.Append(b, str, vars...)
}
// GetN returns the translation for the given string and plural form
func (mo *Mo) GetN(str, plural string, n int, vars ...interface{}) string {
return mo.domain.GetN(str, plural, n, vars...)
}
// AppendN appends a translation string for the given plural form into the domain
func (mo *Mo) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
return mo.domain.AppendN(b, str, plural, n, vars...)
}
// GetC returns the translation for the given string and context
func (mo *Mo) GetC(str, ctx string, vars ...interface{}) string {
return mo.domain.GetC(str, ctx, vars...)
}
// AppendC appends a translation string for the given context into the domain
func (mo *Mo) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
return mo.domain.AppendC(b, str, ctx, vars...)
}
// GetNC returns the translation for the given string, plural form and context
func (mo *Mo) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return mo.domain.GetNC(str, plural, n, ctx, vars...)
}
// AppendNC appends a translation string for the given plural form and context into the domain
func (mo *Mo) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
return mo.domain.AppendNC(b, str, plural, n, ctx, vars...)
}
// IsTranslated checks if the given string is translated
func (mo *Mo) IsTranslated(str string) bool {
return mo.domain.IsTranslated(str)
}
// IsTranslatedN checks if the given string is translated with plural form
func (mo *Mo) IsTranslatedN(str string, n int) bool {
return mo.domain.IsTranslatedN(str, n)
}
// IsTranslatedC checks if the given string is translated with context
func (mo *Mo) IsTranslatedC(str, ctx string) bool {
return mo.domain.IsTranslatedC(str, ctx)
}
// IsTranslatedNC checks if the given string is translated with plural form and context
func (mo *Mo) IsTranslatedNC(str string, n int, ctx string) bool {
return mo.domain.IsTranslatedNC(str, n, ctx)
}
// MarshalBinary marshals the Mo object into a binary format
func (mo *Mo) MarshalBinary() ([]byte, error) {
return mo.domain.MarshalBinary()
}
// UnmarshalBinary unmarshals the Mo object from a binary format
func (mo *Mo) UnmarshalBinary(data []byte) error {
return mo.domain.UnmarshalBinary(data)
}
// ParseFile loads the translations specified in the provided file, in the GNU gettext .mo format
func (mo *Mo) ParseFile(f string) {
data, err := getFileData(f, mo.fs)
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.contextTranslations[string(msgctxt)]; !ok {
mo.domain.contextTranslations[string(msgctxt)] = make(map[string]*Translation)
}
mo.domain.contextTranslations[string(msgctxt)][translation.ID] = translation
} else {
mo.domain.translations[translation.ID] = translation
}
}

View File

@ -0,0 +1,433 @@
// Original work Copyright (c) 2016 Jonas Obrist (https://github.com/ojii/gettext.go)
// Modified work Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com
// Modified work Copyright (c) 2018-present gotext maintainers (https://github.com/leonelquinteros/gotext)
//
// Licensed under the 3-Clause BSD License. See LICENSE in the project root for 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.ReplaceAll(s, " ", "")
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
}

View File

@ -0,0 +1,44 @@
// Original work Copyright (c) 2016 Jonas Obrist (https://github.com/ojii/gettext.go)
// Modified work Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com
// Modified work Copyright (c) 2018-present gotext maintainers (https://github.com/leonelquinteros/gotext)
//
// Licensed under the 3-Clause BSD License. See LICENSE in the project root for 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)
}

View File

@ -0,0 +1,41 @@
#!/usr/bin/python
#
# Copyright (c) 2016 Jonas Obrist (https://github.com/ojii/gettext.go)
#
# Licensed under the 3-Clause BSD License. See LICENSE in the project root for license information.
import json
from gettext import c2py
PLURAL_FORMS = [
"0",
"n!=1",
"n>1",
"n%10==1&&n%100!=11?0:n!=0?1:2",
"n==1?0:n==2?1:2",
"n==1?0:(n==0||(n%100>0&&n%100<20))?1:2",
"n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?1:2",
"n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2",
"(n==1)?0:(n>=2&&n<=4)?1:2",
"n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2",
"n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3",
"n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11?4:5",
]
NUM = 1000
def gen():
tests = []
for plural_form in PLURAL_FORMS:
expr = c2py(plural_form)
tests.append({
'pluralform': plural_form,
'fixture': [expr(n) for n in range(NUM + 1)]
})
return json.dumps(tests)
if __name__ == "__main__":
print(gen())

View File

@ -0,0 +1,19 @@
// Original work Copyright (c) 2016 Jonas Obrist (https://github.com/ojii/gettext.go)
// Modified work Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com
// Modified work Copyright (c) 2018-present gotext maintainers (https://github.com/leonelquinteros/gotext)
//
// Licensed under the 3-Clause BSD License. See LICENSE in the project root for 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
}

View File

@ -0,0 +1,105 @@
// Original work Copyright (c) 2016 Jonas Obrist (https://github.com/ojii/gettext.go)
// Modified work Copyright (c) 2018 DeineAgentur UG https://www.deineagentur.com
// Modified work Copyright (c) 2018-present gotext maintainers (https://github.com/leonelquinteros/gotext)
//
// Licensed under the BSD License. See License.txt in the project root for 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
}
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))
}

427
vendor/github.com/leonelquinteros/gotext/po.go generated vendored Normal file
View File

@ -0,0 +1,427 @@
/*
* 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 (
"io/fs"
"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.NewPo()
// 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
fs fs.FS
}
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
}
// NewPoFS works like NewPO but adds an optional fs.FS
func NewPoFS(filesystem fs.FS) *Po {
po := NewPo()
po.fs = filesystem
return po
}
// GetDomain returns the domain object
func (po *Po) GetDomain() *Domain {
return po.domain
}
// Convenience interfaces
// ---------------------------------------------------------------
// DropStaleTranslations removes all translations that are not referenced in the current domain
func (po *Po) DropStaleTranslations() {
po.domain.DropStaleTranslations()
}
// SetRefs sets the references for a given translation
func (po *Po) SetRefs(str string, refs []string) {
po.domain.SetRefs(str, refs)
}
// GetRefs returns the references for a given translation
func (po *Po) GetRefs(str string) []string {
return po.domain.GetRefs(str)
}
// SetPluralResolver sets the plural resolver function
func (po *Po) SetPluralResolver(f func(int) int) {
po.domain.customPluralResolver = f
}
// Set translation
func (po *Po) Set(id, str string) {
po.domain.Set(id, str)
}
// Get translation
func (po *Po) Get(str string, vars ...interface{}) string {
return po.domain.Get(str, vars...)
}
// Append translation
func (po *Po) Append(b []byte, str string, vars ...interface{}) []byte {
return po.domain.Append(b, str, vars...)
}
// SetN sets the plural translation
func (po *Po) SetN(id, plural string, n int, str string) {
po.domain.SetN(id, plural, n, str)
}
// GetN gets the plural translation
func (po *Po) GetN(str, plural string, n int, vars ...interface{}) string {
return po.domain.GetN(str, plural, n, vars...)
}
// AppendN appends the plural translation
func (po *Po) AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte {
return po.domain.AppendN(b, str, plural, n, vars...)
}
// SetC sets the translation for a given context
func (po *Po) SetC(id, ctx, str string) {
po.domain.SetC(id, ctx, str)
}
// GetC gets the translation for a given context
func (po *Po) GetC(str, ctx string, vars ...interface{}) string {
return po.domain.GetC(str, ctx, vars...)
}
// AppendC appends the translation for a given context
func (po *Po) AppendC(b []byte, str, ctx string, vars ...interface{}) []byte {
return po.domain.AppendC(b, str, ctx, vars...)
}
// SetNC sets the plural translation for a given context
func (po *Po) SetNC(id, plural, ctx string, n int, str string) {
po.domain.SetNC(id, plural, ctx, n, str)
}
// GetNC gets the plural translation for a given context
func (po *Po) GetNC(str, plural string, n int, ctx string, vars ...interface{}) string {
return po.domain.GetNC(str, plural, n, ctx, vars...)
}
// AppendNC appends the plural translation for a given context
func (po *Po) AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte {
return po.domain.AppendNC(b, str, plural, n, ctx, vars...)
}
// IsTranslated checks if the given string is translated
func (po *Po) IsTranslated(str string) bool {
return po.domain.IsTranslated(str)
}
// IsTranslatedN checks if the given string is translated with plural form
func (po *Po) IsTranslatedN(str string, n int) bool {
return po.domain.IsTranslatedN(str, n)
}
// IsTranslatedC checks if the given string is translated with context
func (po *Po) IsTranslatedC(str, ctx string) bool {
return po.domain.IsTranslatedC(str, ctx)
}
// IsTranslatedNC checks if the given string is translated with plural form and context
func (po *Po) IsTranslatedNC(str string, n int, ctx string) bool {
return po.domain.IsTranslatedNC(str, n, ctx)
}
// MarshalText marshals the Po object to text
func (po *Po) MarshalText() ([]byte, error) {
return po.domain.MarshalText()
}
// MarshalBinary marshals the Po object to binary
func (po *Po) MarshalBinary() ([]byte, error) {
return po.domain.MarshalBinary()
}
// UnmarshalBinary unmarshals the Po object from binary
func (po *Po) UnmarshalBinary(data []byte) error {
return po.domain.UnmarshalBinary(data)
}
// ParseFile loads the translations from a file
func (po *Po) ParseFile(f string) {
data, err := getFileData(f, po.fs)
if err != nil {
return
}
po.Parse(data)
}
// Parse loads the translations specified in the provided byte slice (buf)
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.contextTranslations[po.domain.ctxBuffer]; !ok {
po.domain.contextTranslations[po.domain.ctxBuffer] = make(map[string]*Translation)
}
po.domain.contextTranslations[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
}

View File

@ -0,0 +1,97 @@
/*
* 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),
}
}
// NewTranslationWithRefs returns the Translation object and initialized it with references.
func NewTranslationWithRefs(refs []string) *Translation {
return &Translation{
Trs: make(map[int]string),
Refs: refs,
}
}
// IsStale returns whether the translation is stale or not
func (t *Translation) IsStale() bool {
return !t.dirty
}
// SetRefs sets the references of the translation
func (t *Translation) SetRefs(refs []string) {
t.Refs = refs
t.dirty = true
}
// Set sets the string of the translation
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
}
// SetN sets the string of the plural translation
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
}
// IsTranslated reports whether a string is translated
func (t *Translation) IsTranslated() bool {
tr, ok := t.Trs[0]
return tr != "" && ok
}
// IsTranslatedN reports whether a plural string is translated
func (t *Translation) IsTranslatedN(n int) bool {
tr, ok := t.Trs[n]
return tr != "" && ok
}

96
vendor/github.com/leonelquinteros/gotext/translator.go generated vendored Normal file
View File

@ -0,0 +1,96 @@
/*
* 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/fs"
"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
}
// AppendTranslator interface is used by Locale and Po objects.
// It contains all methods needed to parse translation sources and to append entries to the object.
type AppendTranslator interface {
Translator
Append(b []byte, str string, vars ...interface{}) []byte
AppendN(b []byte, str, plural string, n int, vars ...interface{}) []byte
AppendC(b []byte, str, ctx string, vars ...interface{}) []byte
AppendNC(b []byte, str, plural string, n int, ctx string, vars ...interface{}) []byte
}
// 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.contextTranslations = te.Contexts
return po
}
// getFileData reads a file and returns the byte slice after doing some basic sanity checking
func getFileData(f string, filesystem fs.FS) ([]byte, error) {
if filesystem != nil {
return fs.ReadFile(filesystem, f)
}
// 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 os.ReadFile(f)
}

6
vendor/modules.txt vendored
View File

@ -404,6 +404,10 @@ github.com/klauspost/compress/internal/le
github.com/klauspost/compress/internal/snapref
github.com/klauspost/compress/zstd
github.com/klauspost/compress/zstd/internal/xxhash
# github.com/leonelquinteros/gotext v1.7.2
## explicit; go 1.23.5
github.com/leonelquinteros/gotext
github.com/leonelquinteros/gotext/plurals
# github.com/lucasb-eyer/go-colorful v1.2.0
## explicit; go 1.12
github.com/lucasb-eyer/go-colorful
@ -464,8 +468,6 @@ github.com/muesli/ansi/compressor
# github.com/muesli/cancelreader v0.2.2
## explicit; go 1.17
github.com/muesli/cancelreader
# github.com/muesli/reflow v0.3.0
## explicit; go 1.13
# github.com/muesli/termenv v0.16.0
## explicit; go 1.17
github.com/muesli/termenv