working commit

This commit is contained in:
2026-03-13 19:02:42 +02:00
parent bebbf79c7a
commit 5c1da77f4c
1329 changed files with 314708 additions and 39 deletions
+5
View File
@@ -0,0 +1,5 @@
language: go
go:
- "1.14"
- tip
+27
View File
@@ -0,0 +1,27 @@
Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. 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 Google Inc. 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 THE COPYRIGHT
OWNER OR CONTRIBUTORS 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.
+191
View File
@@ -0,0 +1,191 @@
- *赞助 BTC: 1Cbd6oGAUUyBi7X7MaR4np4nTmQZXVgkCW*
- *赞助 ETH: 0x623A3C3a72186A6336C79b18Ac1eD36e1c71A8a6*
- *Go语言付费QQ群: 1055927514*
----
# gettext-go: GNU gettext for Go ([Imported By Kubernetes](https://pkg.go.dev/github.com/chai2010/gettext-go@v0.1.0/gettext?tab=importedby))
- PkgDoc: [http://godoc.org/github.com/chai2010/gettext-go](http://godoc.org/github.com/chai2010/gettext-go)
- PkgDoc: [http://pkg.go.dev/github.com/chai2010/gettext-go](http://pkg.go.dev/github.com/chai2010/gettext-go)
## Install
1. `go get github.com/chai2010/gettext-go`
2. `go run hello.go`
The godoc.org or go.dev has more information.
## Examples
```Go
package main
import (
"fmt"
"github.com/chai2010/gettext-go"
)
func main() {
gettext := gettext.New("hello", "./examples/locale").SetLanguage("zh_CN")
fmt.Println(gettext.Gettext("Hello, world!"))
// Output: 你好, 世界!
}
```
```Go
package main
import (
"fmt"
"github.com/chai2010/gettext-go"
)
func main() {
gettext.SetLanguage("zh_CN")
gettext.BindLocale(gettext.New("hello", "locale"))
// gettext.BindLocale("hello", "locale") // from locale dir
// gettext.BindLocale("hello", "locale.zip") // from locale zip file
// gettext.BindLocale("hello", "locale.zip", zipData) // from embedded zip data
// translate source text
fmt.Println(gettext.Gettext("Hello, world!"))
// Output: 你好, 世界!
// if no msgctxt in PO file (only msgid and msgstr),
// specify context as "" by
fmt.Println(gettext.PGettext("", "Hello, world!"))
// Output: 你好, 世界!
// translate resource
fmt.Println(string(gettext.Getdata("poems.txt"))))
// Output: ...
}
```
Go file: [hello.go](https://github.com/chai2010/gettext-go/blob/master/examples/hello.go); PO file: [hello.po](https://github.com/chai2010/gettext-go/blob/master/examples/locale/default/LC_MESSAGES/hello.po);
----
## API Changes (v0.1.0 vs v1.0.0)
### Renamed package path
| v0.1.0 (old) | v1.0.0 (new) |
| ----------------------------------------------- | --------------------------------------- |
| `github.com/chai2010/gettext-go/gettext` | `github.com/chai2010/gettext-go` |
| `github.com/chai2010/gettext-go/gettext/po` | `github.com/chai2010/gettext-go/po` |
| `github.com/chai2010/gettext-go/gettext/mo` | `github.com/chai2010/gettext-go/mo` |
| `github.com/chai2010/gettext-go/gettext/plural` | `github.com/chai2010/gettext-go/plural` |
### Renamed functions
| v0.1.0 (old) | v1.0.0 (new) |
| ---------------------------------- | --------------------------- |
| `gettext-go/gettext.*` | `gettext-go.*` |
| `gettext-go/gettext.DefaultLocal` | `gettext-go.DefaultLanguage`|
| `gettext-go/gettext.BindTextdomain`| `gettext-go.BindLocale` |
| `gettext-go/gettext.Textdomain` | `gettext-go.SetDomain` |
| `gettext-go/gettext.SetLocale` | `gettext-go.SetLanguage` |
| `gettext-go/gettext/po.Load` | `gettext-go/po.LoadFile` |
| `gettext-go/gettext/po.LoadData` | `gettext-go/po.Load` |
| `gettext-go/gettext/mo.Load` | `gettext-go/mo.LoadFile` |
| `gettext-go/gettext/mo.LoadData` | `gettext-go/mo.Load` |
### Use empty string as the default context for `gettext.Gettext`
```go
package main
// v0.1.0
// if the **context** missing, use `callerName(2)` as the context:
// v1.0.0
// if the **context** missing, use empty string as the context:
func main() {
gettext.Gettext("hello")
// v0.1.0 => gettext.PGettext("main.main", "hello")
// v1.0.0 => gettext.PGettext("", "hello")
gettext.DGettext("domain", "hello")
// v0.1.0 => gettext.DPGettext("domain", "main.main", "hello")
// v1.0.0 => gettext.DPGettext("domain", "", "hello")
gettext.NGettext("domain", "hello", "hello2", n)
// v0.1.0 => gettext.PNGettext("domain", "main.main", "hello", "hello2", n)
// v1.0.0 => gettext.PNGettext("domain", "", "hello", "hello2", n)
gettext.DNGettext("domain", "hello", "hello2", n)
// v0.1.0 => gettext.DPNGettext("domain", "main.main", "hello", "hello2", n)
// v1.0.0 => gettext.DPNGettext("domain", "", "hello", "hello2", n)
}
```
### `BindLocale` support `FileSystem` interface
```go
// Use FileSystem:
// BindLocale(New("poedit", "name", OS("path/to/dir"))) // bind "poedit" domain
// BindLocale(New("poedit", "name", OS("path/to.zip"))) // bind "poedit" domain
```
## New API in v1.0.0
`Gettexter` interface:
```go
type Gettexter interface {
FileSystem() FileSystem
GetDomain() string
SetDomain(domain string) Gettexter
GetLanguage() string
SetLanguage(lang string) Gettexter
Gettext(msgid string) string
PGettext(msgctxt, msgid string) string
NGettext(msgid, msgidPlural string, n int) string
PNGettext(msgctxt, msgid, msgidPlural string, n int) string
DGettext(domain, msgid string) string
DPGettext(domain, msgctxt, msgid string) string
DNGettext(domain, msgid, msgidPlural string, n int) string
DPNGettext(domain, msgctxt, msgid, msgidPlural string, n int) string
Getdata(name string) []byte
DGetdata(domain, name string) []byte
}
func New(domain, path string, data ...interface{}) Gettexter
```
`FileSystem` interface:
```go
type FileSystem interface {
LocaleList() []string
LoadMessagesFile(domain, lang, ext string) ([]byte, error)
LoadResourceFile(domain, lang, name string) ([]byte, error)
String() string
}
func NewFS(name string, x interface{}) FileSystem
func OS(root string) FileSystem
func ZipFS(r *zip.Reader, name string) FileSystem
func NilFS(name string) FileSystem
```
----
## BUGS
Please report bugs to <chaishushan@gmail.com>.
Thanks!
+67
View File
@@ -0,0 +1,67 @@
// Copyright 2013 <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package gettext implements a basic GNU's gettext library.
Example:
import (
"github.com/chai2010/gettext-go"
)
func main() {
gettext.SetLanguage("zh_CN")
// gettext.BindLocale(gettext.New("hello", "locale")) // from locale dir
// gettext.BindLocale(gettext.New("hello", "locale.zip")) // from locale zip file
// gettext.BindLocale(gettext.New("hello", "locale.zip", zipData)) // from embedded zip data
gettext.BindLocale(gettext.New("hello", "locale"))
// translate source text
fmt.Println(gettext.Gettext("Hello, world!"))
// Output: 你好, 世界!
// translate resource
fmt.Println(string(gettext.Getdata("poems.txt")))
// Output: ...
}
Translate directory struct("./examples/locale.zip"):
Root: "path" or "file.zip/zipBaseName"
+-default # locale: $(LC_MESSAGES) or $(LANG) or "default"
| +-LC_MESSAGES # just for `gettext.Gettext`
| | +-hello.mo # $(Root)/$(lang)/LC_MESSAGES/$(domain).mo
| | +-hello.po # $(Root)/$(lang)/LC_MESSAGES/$(domain).po
| | \-hello.json # $(Root)/$(lang)/LC_MESSAGES/$(domain).json
| |
| \-LC_RESOURCE # just for `gettext.Getdata`
| +-hello # domain map a dir in resource translate
| +-favicon.ico # $(Root)/$(lang)/LC_RESOURCE/$(domain)/$(filename)
| \-poems.txt
|
\-zh_CN # simple chinese translate
+-LC_MESSAGES
| +-hello.po # try "$(domain).po" first
| +-hello.mo # try "$(domain).mo" second
| \-hello.json # try "$(domain).json" third
|
\-LC_RESOURCE
+-hello
+-favicon.ico # $(lang)/$(domain)/favicon.ico
\-poems.txt # $(lang)/$(domain)/poems.txt
See:
http://en.wikipedia.org/wiki/Gettext
http://www.gnu.org/software/gettext/manual/html_node
http://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html
http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
http://www.gnu.org/software/gettext/manual/html_node/MO-Files.html
http://www.poedit.net/
Please report bugs to <chaishushan{AT}gmail.com>.
Thanks!
*/
package gettext
+84
View File
@@ -0,0 +1,84 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gettext
import (
"archive/zip"
"bytes"
"fmt"
)
type FileSystem interface {
LocaleList() []string
LoadMessagesFile(domain, lang, ext string) ([]byte, error)
LoadResourceFile(domain, lang, name string) ([]byte, error)
String() string
}
func NewFS(name string, x interface{}) FileSystem {
if x == nil {
if name != "" {
return OS(name)
}
return NilFS(name)
}
switch x := x.(type) {
case []byte:
if len(x) == 0 {
return OS(name)
}
if r, err := zip.NewReader(bytes.NewReader(x), int64(len(x))); err == nil {
return ZipFS(r, name)
}
if fs, err := newJson(x, name); err == nil {
return fs
}
case string:
if len(x) == 0 {
return OS(name)
}
if r, err := zip.NewReader(bytes.NewReader([]byte(x)), int64(len(x))); err == nil {
return ZipFS(r, name)
}
if fs, err := newJson([]byte(x), name); err == nil {
return fs
}
case FileSystem:
return x
}
return NilFS(name)
}
func OS(root string) FileSystem {
return newOsFS(root)
}
func ZipFS(r *zip.Reader, name string) FileSystem {
return newZipFS(r, name)
}
func NilFS(name string) FileSystem {
return &nilFS{name}
}
type nilFS struct {
name string
}
func (p *nilFS) LocaleList() []string {
return nil
}
func (p *nilFS) LoadMessagesFile(domain, lang, ext string) ([]byte, error) {
return nil, fmt.Errorf("not found")
}
func (p *nilFS) LoadResourceFile(domain, lang, name string) ([]byte, error) {
return nil, fmt.Errorf("not found")
}
func (p *nilFS) String() string {
return "gettext.nilfs(" + p.name + ")"
}
+66
View File
@@ -0,0 +1,66 @@
// Copyright 2020 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gettext
import (
"encoding/json"
"fmt"
"sort"
)
type jsonFS struct {
name string
x map[string]struct {
LC_MESSAGES map[string][]struct {
MsgContext string `json:"msgctxt"` // msgctxt context
MsgId string `json:"msgid"` // msgid untranslated-string
MsgIdPlural string `json:"msgid_plural"` // msgid_plural untranslated-string-plural
MsgStr []string `json:"msgstr"` // msgstr translated-string
}
LC_RESOURCE map[string]map[string]string
}
}
func isJsonData() bool {
return false
}
func newJson(jsonData []byte, name string) (*jsonFS, error) {
p := &jsonFS{name: name}
if err := json.Unmarshal(jsonData, &p.x); err != nil {
return nil, err
}
return p, nil
}
func (p *jsonFS) LocaleList() []string {
var ss []string
for lang := range p.x {
ss = append(ss, lang)
}
sort.Strings(ss)
return ss
}
func (p *jsonFS) LoadMessagesFile(domain, lang, ext string) ([]byte, error) {
if v, ok := p.x[lang]; ok {
if v, ok := v.LC_MESSAGES[domain+ext]; ok {
return json.Marshal(v)
}
}
return nil, fmt.Errorf("not found")
}
func (p *jsonFS) LoadResourceFile(domain, lang, name string) ([]byte, error) {
if v, ok := p.x[lang]; ok {
if v, ok := v.LC_RESOURCE[domain]; ok {
return []byte(v[name]), nil
}
}
return nil, fmt.Errorf("not found")
}
func (p *jsonFS) String() string {
return "gettext.nilfs(" + p.name + ")"
}
+91
View File
@@ -0,0 +1,91 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gettext
import (
"archive/zip"
"bytes"
"fmt"
"io/ioutil"
"os"
"sort"
"strings"
)
type osFS struct {
root string
}
func newOsFS(root string) FileSystem {
// locale zip file
if fi, err := os.Stat(root); err == nil && !fi.IsDir() {
if strings.HasSuffix(strings.ToLower(root), ".zip") {
if x, err := ioutil.ReadFile(root); err == nil {
if r, err := zip.NewReader(bytes.NewReader(x), int64(len(x))); err == nil {
return ZipFS(r, root)
}
}
}
if strings.HasSuffix(strings.ToLower(root), ".json") {
if x, err := ioutil.ReadFile(root); err == nil {
if fs, err := newJson(x, root); err == nil {
return fs
}
}
}
}
// locale dir
return &osFS{root: root}
}
func (p *osFS) LocaleList() []string {
list, err := ioutil.ReadDir(p.root)
if err != nil {
return nil
}
ssMap := make(map[string]bool)
for _, dir := range list {
if dir.IsDir() {
ssMap[dir.Name()] = true
}
}
var locales = make([]string, 0, len(ssMap))
for s := range ssMap {
locales = append(locales, s)
}
sort.Strings(locales)
return locales
}
func (p *osFS) LoadMessagesFile(domain, locale, ext string) ([]byte, error) {
trName := p.makeMessagesFileName(domain, locale, ext)
rcData, err := ioutil.ReadFile(trName)
if err != nil {
return nil, err
}
return rcData, nil
}
func (p *osFS) LoadResourceFile(domain, locale, name string) ([]byte, error) {
rcName := p.makeResourceFileName(domain, locale, name)
rcData, err := ioutil.ReadFile(rcName)
if err != nil {
return nil, err
}
return rcData, nil
}
func (p *osFS) String() string {
return "gettext.localfs(" + p.root + ")"
}
func (p *osFS) makeMessagesFileName(domain, lang, ext string) string {
return fmt.Sprintf("%s/%s/LC_MESSAGES/%s%s", p.root, lang, domain, ext)
}
func (p *osFS) makeResourceFileName(domain, lang, name string) string {
return fmt.Sprintf("%s/%s/LC_RESOURCE/%s/%s", p.root, lang, domain, name)
}
+142
View File
@@ -0,0 +1,142 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gettext
import (
"archive/zip"
"fmt"
"io/ioutil"
"sort"
"strings"
)
type zipFS struct {
root string
name string
r *zip.Reader
}
func newZipFS(r *zip.Reader, name string) *zipFS {
fs := &zipFS{r: r, name: name}
fs.root = fs.zipRoot()
return fs
}
func (p *zipFS) zipName() string {
name := p.name
if x := strings.LastIndexAny(name, `\/`); x != -1 {
name = name[x+1:]
}
name = strings.TrimSuffix(name, ".zip")
return name
}
func (p *zipFS) zipRoot() string {
var somepath string
for _, f := range p.r.File {
if x := strings.Index(f.Name, "LC_MESSAGES"); x != -1 {
somepath = f.Name
}
if x := strings.Index(f.Name, "LC_RESOURCE"); x != -1 {
somepath = f.Name
}
}
if somepath == "" {
return p.zipName()
}
ss := strings.Split(somepath, "/")
for i, s := range ss {
// $(root)/$(lang)/LC_MESSAGES
// $(root)/$(lang)/LC_RESOURCE
if (s == "LC_MESSAGES" || s == "LC_RESOURCE") && i >= 2 {
return strings.Join(ss[:i-1], "/")
}
}
return p.zipName()
}
func (p *zipFS) LocaleList() []string {
var locals []string
for s := range p.lsZip(p.r) {
locals = append(locals, s)
}
sort.Strings(locals)
return locals
}
func (p *zipFS) LoadMessagesFile(domain, lang, ext string) ([]byte, error) {
trName := p.makeMessagesFileName(domain, lang, ext)
for _, f := range p.r.File {
if f.Name != trName {
continue
}
rc, err := f.Open()
if err != nil {
return nil, err
}
rcData, err := ioutil.ReadAll(rc)
rc.Close()
return rcData, err
}
return nil, fmt.Errorf("not found")
}
func (p *zipFS) LoadResourceFile(domain, lang, name string) ([]byte, error) {
rcName := p.makeResourceFileName(domain, lang, name)
for _, f := range p.r.File {
if f.Name != rcName {
continue
}
rc, err := f.Open()
if err != nil {
return nil, err
}
rcData, err := ioutil.ReadAll(rc)
rc.Close()
return rcData, err
}
return nil, fmt.Errorf("not found")
}
func (p *zipFS) String() string {
return "gettext.zipfs(" + p.name + ")"
}
func (p *zipFS) makeMessagesFileName(domain, lang, ext string) string {
return fmt.Sprintf("%s/%s/LC_MESSAGES/%s%s", p.root, lang, domain, ext)
}
func (p *zipFS) makeResourceFileName(domain, lang, name string) string {
return fmt.Sprintf("%s/%s/LC_RESOURCE/%s/%s", p.root, lang, domain, name)
}
func (p *zipFS) lsZip(r *zip.Reader) map[string]bool {
ssMap := make(map[string]bool)
for _, f := range r.File {
if x := strings.Index(f.Name, "LC_MESSAGES"); x != -1 {
s := strings.TrimRight(f.Name[:x], `\/`)
if x = strings.LastIndexAny(s, `\/`); x != -1 {
s = s[x+1:]
}
if s != "" {
ssMap[s] = true
}
continue
}
if x := strings.Index(f.Name, "LC_RESOURCE"); x != -1 {
s := strings.TrimRight(f.Name[:x], `\/`)
if x = strings.LastIndexAny(s, `\/`); x != -1 {
s = s[x+1:]
}
if s != "" {
ssMap[s] = true
}
continue
}
}
return ssMap
}
+219
View File
@@ -0,0 +1,219 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gettext
var (
DefaultLanguage string = getDefaultLanguage() // use $(LC_MESSAGES) or $(LANG) or "default"
)
type Gettexter interface {
FileSystem() FileSystem
GetDomain() string
SetDomain(domain string) Gettexter
GetLanguage() string
SetLanguage(lang string) Gettexter
Gettext(msgid string) string
PGettext(msgctxt, msgid string) string
NGettext(msgid, msgidPlural string, n int) string
PNGettext(msgctxt, msgid, msgidPlural string, n int) string
DGettext(domain, msgid string) string
DPGettext(domain, msgctxt, msgid string) string
DNGettext(domain, msgid, msgidPlural string, n int) string
DPNGettext(domain, msgctxt, msgid, msgidPlural string, n int) string
Getdata(name string) []byte
DGetdata(domain, name string) []byte
}
// New create Interface use default language.
func New(domain, path string, data ...interface{}) Gettexter {
return newLocale(domain, path, data...)
}
var defaultGettexter struct {
lang string
domain string
Gettexter
}
func init() {
defaultGettexter.lang = getDefaultLanguage()
defaultGettexter.domain = "default"
defaultGettexter.Gettexter = newLocale("", "")
}
// BindLocale sets and queries program's domains.
//
// Examples:
// BindLocale(New("poedit", "locale")) // bind "poedit" domain
//
// Use zip file:
// BindLocale(New("poedit", "locale.zip")) // bind "poedit" domain
// BindLocale(New("poedit", "locale.zip", zipData)) // bind "poedit" domain
//
// Use FileSystem:
// BindLocale(New("poedit", "name", OS("path/to/dir"))) // bind "poedit" domain
// BindLocale(New("poedit", "name", OS("path/to.zip"))) // bind "poedit" domain
//
func BindLocale(g Gettexter) {
if g != nil {
defaultGettexter.Gettexter = g
defaultGettexter.SetLanguage(defaultGettexter.lang)
} else {
defaultGettexter.Gettexter = newLocale("", "")
defaultGettexter.SetLanguage(defaultGettexter.lang)
}
}
// SetLanguage sets and queries the program's current lang.
//
// If the lang is not empty string, set the new locale.
//
// If the lang is empty string, don't change anything.
//
// Returns is the current locale.
//
// Examples:
// SetLanguage("") // get locale: return DefaultLocale
// SetLanguage("zh_CN") // set locale: return zh_CN
// SetLanguage("") // get locale: return zh_CN
func SetLanguage(lang string) string {
defaultGettexter.SetLanguage(lang)
return defaultGettexter.GetLanguage()
}
// SetDomain sets and retrieves the current message domain.
//
// If the domain is not empty string, set the new domains.
//
// If the domain is empty string, don't change anything.
//
// Returns is the all used domains.
//
// Examples:
// SetDomain("poedit") // set domain: poedit
// SetDomain("") // get domain: return poedit
func SetDomain(domain string) string {
defaultGettexter.SetDomain(domain)
return defaultGettexter.GetDomain()
}
// Gettext attempt to translate a text string into the user's native language,
// by looking up the translation in a message catalog.
//
// It use the caller's function name as the msgctxt.
//
// Examples:
// func Foo() {
// msg := gettext.Gettext("Hello") // msgctxt is ""
// }
func Gettext(msgid string) string {
return defaultGettexter.Gettext(msgid)
}
// Getdata attempt to translate a resource file into the user's native language,
// by looking up the translation in a message catalog.
//
// Examples:
// func Foo() {
// Textdomain("hello")
// BindLocale("hello", "locale.zip", nilOrZipData)
// poems := gettext.Getdata("poems.txt")
// }
func Getdata(name string) []byte {
return defaultGettexter.Getdata(name)
}
// NGettext attempt to translate a text string into the user's native language,
// by looking up the appropriate plural form of the translation in a message
// catalog.
//
// It use the caller's function name as the msgctxt.
//
// Examples:
// func Foo() {
// msg := gettext.NGettext("%d people", "%d peoples", 2)
// }
func NGettext(msgid, msgidPlural string, n int) string {
return defaultGettexter.NGettext(msgid, msgidPlural, n)
}
// PGettext attempt to translate a text string into the user's native language,
// by looking up the translation in a message catalog.
//
// Examples:
// func Foo() {
// msg := gettext.PGettext("gettext-go.example", "Hello") // msgctxt is "gettext-go.example"
// }
func PGettext(msgctxt, msgid string) string {
return defaultGettexter.PGettext(msgctxt, msgid)
}
// PNGettext attempt to translate a text string into the user's native language,
// by looking up the appropriate plural form of the translation in a message
// catalog.
//
// Examples:
// func Foo() {
// msg := gettext.PNGettext("gettext-go.example", "%d people", "%d peoples", 2)
// }
func PNGettext(msgctxt, msgid, msgidPlural string, n int) string {
return defaultGettexter.PNGettext(msgctxt, msgid, msgidPlural, n)
}
// DGettext like Gettext(), but looking up the message in the specified domain.
//
// Examples:
// func Foo() {
// msg := gettext.DGettext("poedit", "Hello")
// }
func DGettext(domain, msgid string) string {
return defaultGettexter.DGettext(domain, msgid)
}
// DNGettext like NGettext(), but looking up the message in the specified domain.
//
// Examples:
// func Foo() {
// msg := gettext.PNGettext("poedit", "gettext-go.example", "%d people", "%d peoples", 2)
// }
func DNGettext(domain, msgid, msgidPlural string, n int) string {
return defaultGettexter.DNGettext(domain, msgid, msgidPlural, n)
}
// DPGettext like PGettext(), but looking up the message in the specified domain.
//
// Examples:
// func Foo() {
// msg := gettext.DPGettext("poedit", "gettext-go.example", "Hello")
// }
func DPGettext(domain, msgctxt, msgid string) string {
return defaultGettexter.DPGettext(domain, msgctxt, msgid)
}
// DPNGettext like PNGettext(), but looking up the message in the specified domain.
//
// Examples:
// func Foo() {
// msg := gettext.DPNGettext("poedit", "gettext-go.example", "%d people", "%d peoples", 2)
// }
func DPNGettext(domain, msgctxt, msgid, msgidPlural string, n int) string {
return defaultGettexter.DPNGettext(domain, msgctxt, msgid, msgidPlural, n)
}
// DGetdata like Getdata(), but looking up the resource in the specified domain.
//
// Examples:
// func Foo() {
// msg := gettext.DGetdata("hello", "poems.txt")
// }
func DGetdata(domain, name string) []byte {
return defaultGettexter.DGetdata(domain, name)
}
+205
View File
@@ -0,0 +1,205 @@
// Copyright 2020 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gettext
import (
"fmt"
"sync"
)
type _Locale struct {
mutex sync.Mutex
fs FileSystem
lang string
domain string
trMap map[string]*translator
trCurrent *translator
}
var _ Gettexter = (*_Locale)(nil)
func newLocale(domain, path string, data ...interface{}) *_Locale {
if domain == "" {
domain = "default"
}
p := &_Locale{
lang: DefaultLanguage,
domain: domain,
}
if len(data) > 0 {
p.fs = NewFS(path, data[0])
} else {
p.fs = NewFS(path, nil)
}
p.syncTrMap()
return p
}
func (p *_Locale) makeTrMapKey(domain, _Locale string) string {
return domain + "_$$$_" + _Locale
}
func (p *_Locale) FileSystem() FileSystem {
return p.fs
}
func (p *_Locale) GetLanguage() string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.lang
}
func (p *_Locale) SetLanguage(lang string) Gettexter {
p.mutex.Lock()
defer p.mutex.Unlock()
if lang == "" {
lang = DefaultLanguage
}
if lang == p.lang {
return p
}
p.lang = lang
p.syncTrMap()
return p
}
func (p *_Locale) GetDomain() string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.domain
}
func (p *_Locale) SetDomain(domain string) Gettexter {
p.mutex.Lock()
defer p.mutex.Unlock()
if domain == "" || domain == p.domain {
return p
}
p.domain = domain
p.syncTrMap()
return p
}
func (p *_Locale) syncTrMap() {
p.trMap = make(map[string]*translator)
trMapKey := p.makeTrMapKey(p.domain, p.lang)
if tr, ok := p.trMap[trMapKey]; ok {
p.trCurrent = tr
return
}
// try load po file
if data, err := p.fs.LoadMessagesFile(p.domain, p.lang, ".po"); err == nil {
if tr, err := newPoTranslator(fmt.Sprintf("%s_%s.po", p.domain, p.lang), data); err == nil {
p.trMap[trMapKey] = tr
p.trCurrent = tr
return
}
}
// try load mo file
if data, err := p.fs.LoadMessagesFile(p.domain, p.lang, ".mo"); err == nil {
if tr, err := newMoTranslator(fmt.Sprintf("%s_%s.mo", p.domain, p.lang), data); err == nil {
p.trMap[trMapKey] = tr
p.trCurrent = tr
return
}
}
// try load json file
if data, err := p.fs.LoadMessagesFile(p.domain, p.lang, ".json"); err == nil {
if tr, err := newJsonTranslator(p.lang, fmt.Sprintf("%s_%s.json", p.domain, p.lang), data); err == nil {
p.trMap[trMapKey] = tr
p.trCurrent = tr
return
}
}
// no po/mo file
p.trMap[trMapKey] = nilTranslator
p.trCurrent = nilTranslator
return
}
func (p *_Locale) Gettext(msgid string) string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.trCurrent.PGettext("", msgid)
}
func (p *_Locale) PGettext(msgctxt, msgid string) string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.trCurrent.PGettext(msgctxt, msgid)
}
func (p *_Locale) NGettext(msgid, msgidPlural string, n int) string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.trCurrent.PNGettext("", msgid, msgidPlural, n)
}
func (p *_Locale) PNGettext(msgctxt, msgid, msgidPlural string, n int) string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.trCurrent.PNGettext(msgctxt, msgid, msgidPlural, n)
}
func (p *_Locale) DGettext(domain, msgid string) string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.gettext(domain, "", msgid, "", 0)
}
func (p *_Locale) DNGettext(domain, msgid, msgidPlural string, n int) string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.gettext(domain, "", msgid, msgidPlural, n)
}
func (p *_Locale) DPGettext(domain, msgctxt, msgid string) string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.gettext(domain, msgctxt, msgid, "", 0)
}
func (p *_Locale) DPNGettext(domain, msgctxt, msgid, msgidPlural string, n int) string {
p.mutex.Lock()
defer p.mutex.Unlock()
return p.gettext(domain, msgctxt, msgid, msgidPlural, n)
}
func (p *_Locale) Getdata(name string) []byte {
return p.getdata(p.domain, name)
}
func (p *_Locale) DGetdata(domain, name string) []byte {
return p.getdata(domain, name)
}
func (p *_Locale) gettext(domain, msgctxt, msgid, msgidPlural string, n int) string {
if f, ok := p.trMap[p.makeTrMapKey(domain, p.lang)]; ok {
return f.PNGettext(msgctxt, msgid, msgidPlural, n)
}
return msgid
}
func (p *_Locale) getdata(domain, name string) []byte {
if data, err := p.fs.LoadResourceFile(domain, p.lang, name); err == nil {
return data
}
if p.lang != "default" {
if data, err := p.fs.LoadResourceFile(domain, "default", name); err == nil {
return data
}
}
return nil
}
+74
View File
@@ -0,0 +1,74 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package mo provides support for reading and writing GNU MO file.
Examples:
import (
"github.com/chai2010/gettext-go/mo"
)
func main() {
moFile, err := mo.LoadFile("test.mo")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v", moFile)
}
GNU MO file struct:
byte
+------------------------------------------+
0 | magic number = 0x950412de |
| |
4 | file format revision = 0 |
| |
8 | number of strings | == N
| |
12 | offset of table with original strings | == O
| |
16 | offset of table with translation strings | == T
| |
20 | size of hashing table | == S
| |
24 | offset of hashing table | == H
| |
. .
. (possibly more entries later) .
. .
| |
O | length & offset 0th string ----------------.
O + 8 | length & offset 1st string ------------------.
... ... | |
O + ((N-1)*8)| length & offset (N-1)th string | | |
| | | |
T | length & offset 0th translation ---------------.
T + 8 | length & offset 1st translation -----------------.
... ... | | | |
T + ((N-1)*8)| length & offset (N-1)th translation | | | | |
| | | | | |
H | start hash table | | | | |
... ... | | | |
H + S * 4 | end hash table | | | | |
| | | | | |
| NUL terminated 0th string <----------------' | | |
| | | | |
| NUL terminated 1st string <------------------' | |
| | | |
... ... | |
| | | |
| NUL terminated 0th translation <---------------' |
| | |
| NUL terminated 1st translation <-----------------'
| |
... ...
| |
+------------------------------------------+
The GNU MO file specification is at
http://www.gnu.org/software/gettext/manual/html_node/MO-Files.html.
*/
package mo
+105
View File
@@ -0,0 +1,105 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mo
import (
"bytes"
"encoding/binary"
"sort"
"strings"
)
type moHeader struct {
MagicNumber uint32
MajorVersion uint16
MinorVersion uint16
MsgIdCount uint32
MsgIdOffset uint32
MsgStrOffset uint32
HashSize uint32
HashOffset uint32
}
type moStrPos struct {
Size uint32 // must keep fields order
Addr uint32
}
func encodeFile(f *File) []byte {
hdr := &moHeader{
MagicNumber: MoMagicLittleEndian,
}
data := encodeData(hdr, f)
data = append(encodeHeader(hdr), data...)
return data
}
// encode data and init moHeader
func encodeData(hdr *moHeader, f *File) []byte {
msgList := []Message{f.MimeHeader.toMessage()}
for _, v := range f.Messages {
if len(v.MsgId) == 0 {
continue
}
if len(v.MsgStr) == 0 && len(v.MsgStrPlural) == 0 {
continue
}
msgList = append(msgList, v)
}
sort.Slice(msgList, func(i, j int) bool {
return msgList[i].less(&msgList[j])
})
var buf bytes.Buffer
var msgIdPosList = make([]moStrPos, len(msgList))
var msgStrPosList = make([]moStrPos, len(msgList))
for i, v := range msgList {
// write msgid
msgId := encodeMsgId(v)
msgIdPosList[i].Addr = uint32(buf.Len() + MoHeaderSize)
msgIdPosList[i].Size = uint32(len(msgId))
buf.WriteString(msgId)
// write msgstr
msgStr := encodeMsgStr(v)
msgStrPosList[i].Addr = uint32(buf.Len() + MoHeaderSize)
msgStrPosList[i].Size = uint32(len(msgStr))
buf.WriteString(msgStr)
}
hdr.MsgIdOffset = uint32(buf.Len() + MoHeaderSize)
binary.Write(&buf, binary.LittleEndian, msgIdPosList)
hdr.MsgStrOffset = uint32(buf.Len() + MoHeaderSize)
binary.Write(&buf, binary.LittleEndian, msgStrPosList)
hdr.MsgIdCount = uint32(len(msgList))
return buf.Bytes()
}
// must called after encodeData
func encodeHeader(hdr *moHeader) []byte {
var buf bytes.Buffer
binary.Write(&buf, binary.LittleEndian, hdr)
return buf.Bytes()
}
func encodeMsgId(v Message) string {
if v.MsgContext != "" && v.MsgIdPlural != "" {
return v.MsgContext + EotSeparator + v.MsgId + NulSeparator + v.MsgIdPlural
}
if v.MsgContext != "" && v.MsgIdPlural == "" {
return v.MsgContext + EotSeparator + v.MsgId
}
if v.MsgContext == "" && v.MsgIdPlural != "" {
return v.MsgId + NulSeparator + v.MsgIdPlural
}
return v.MsgId
}
func encodeMsgStr(v Message) string {
if v.MsgIdPlural != "" {
return strings.Join(v.MsgStrPlural, NulSeparator)
}
return v.MsgStr
}
+197
View File
@@ -0,0 +1,197 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mo
import (
"bytes"
"encoding/binary"
"fmt"
"io/ioutil"
"strings"
)
const (
MoHeaderSize = 28
MoMagicLittleEndian = 0x950412de
MoMagicBigEndian = 0xde120495
EotSeparator = "\x04" // msgctxt and msgid separator
NulSeparator = "\x00" // msgid and msgstr separator
)
// File represents an MO File.
//
// See http://www.gnu.org/software/gettext/manual/html_node/MO-Files.html
type File struct {
MagicNumber uint32
MajorVersion uint16
MinorVersion uint16
MsgIdCount uint32
MsgIdOffset uint32
MsgStrOffset uint32
HashSize uint32
HashOffset uint32
MimeHeader Header
Messages []Message
}
// Load loads mo file format data.
func Load(data []byte) (*File, error) {
return loadData(data)
}
// Load loads a named mo file.
func LoadFile(path string) (*File, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return loadData(data)
}
func loadData(data []byte) (*File, error) {
r := bytes.NewReader(data)
var magicNumber uint32
if err := binary.Read(r, binary.LittleEndian, &magicNumber); err != nil {
return nil, fmt.Errorf("gettext: %v", err)
}
var bo binary.ByteOrder
switch magicNumber {
case MoMagicLittleEndian:
bo = binary.LittleEndian
case MoMagicBigEndian:
bo = binary.BigEndian
default:
return nil, 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 nil, fmt.Errorf("gettext: %v", err)
}
if v := header.MajorVersion; v != 0 && v != 1 {
return nil, fmt.Errorf("gettext: %v", "invalid version number")
}
if v := header.MinorVersion; v != 0 && v != 1 {
return nil, 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 nil, fmt.Errorf("gettext: %v", err)
}
for i := 0; i < int(header.MsgIdCount); i++ {
if err := binary.Read(r, bo, &msgIdLen[i]); err != nil {
return nil, fmt.Errorf("gettext: %v", err)
}
if err := binary.Read(r, bo, &msgIdStart[i]); err != nil {
return nil, 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 nil, fmt.Errorf("gettext: %v", err)
}
for i := 0; i < int(header.MsgIdCount); i++ {
if err := binary.Read(r, bo, &msgStrLen[i]); err != nil {
return nil, fmt.Errorf("gettext: %v", err)
}
if err := binary.Read(r, bo, &msgStrStart[i]); err != nil {
return nil, fmt.Errorf("gettext: %v", err)
}
}
file := &File{
MagicNumber: magicNumber,
MajorVersion: header.MajorVersion,
MinorVersion: header.MinorVersion,
MsgIdCount: header.MsgIdCount,
MsgIdOffset: header.MsgIdOffset,
MsgStrOffset: header.MsgStrOffset,
HashSize: header.HashSize,
HashOffset: header.HashOffset,
}
for i := 0; i < int(header.MsgIdCount); i++ {
if _, err := r.Seek(int64(msgIdStart[i]), 0); err != nil {
return nil, fmt.Errorf("gettext: %v", err)
}
msgIdData := make([]byte, msgIdLen[i])
if _, err := r.Read(msgIdData); err != nil {
return nil, fmt.Errorf("gettext: %v", err)
}
if _, err := r.Seek(int64(msgStrStart[i]), 0); err != nil {
return nil, fmt.Errorf("gettext: %v", err)
}
msgStrData := make([]byte, msgStrLen[i])
if _, err := r.Read(msgStrData); err != nil {
return nil, fmt.Errorf("gettext: %v", err)
}
if len(msgIdData) == 0 {
var msg = Message{
MsgId: string(msgIdData),
MsgStr: string(msgStrData),
}
file.MimeHeader.fromMessage(&msg)
} else {
var msg = Message{
MsgId: string(msgIdData),
MsgStr: string(msgStrData),
}
// Is this a context message?
if idx := strings.Index(msg.MsgId, EotSeparator); idx != -1 {
msg.MsgContext, msg.MsgId = msg.MsgId[:idx], msg.MsgId[idx+1:]
}
// Is this a plural message?
if idx := strings.Index(msg.MsgId, NulSeparator); idx != -1 {
msg.MsgId, msg.MsgIdPlural = msg.MsgId[:idx], msg.MsgId[idx+1:]
msg.MsgStrPlural = strings.Split(msg.MsgStr, NulSeparator)
msg.MsgStr = ""
}
file.Messages = append(file.Messages, msg)
}
}
return file, nil
}
// Save saves a mo file.
func (f *File) Save(name string) error {
return ioutil.WriteFile(name, f.Data(), 0666)
}
// Save returns a mo file format data.
func (f *File) Data() []byte {
return encodeFile(f)
}
// String returns the po format file string.
func (f *File) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "# version: %d.%d\n", f.MajorVersion, f.MinorVersion)
fmt.Fprintf(&buf, "%s\n", f.MimeHeader.String())
fmt.Fprintf(&buf, "\n")
for k, v := range f.Messages {
fmt.Fprintf(&buf, `msgid "%v"`+"\n", k)
fmt.Fprintf(&buf, `msgstr "%s"`+"\n", v.MsgStr)
fmt.Fprintf(&buf, "\n")
}
return buf.String()
}
+109
View File
@@ -0,0 +1,109 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mo
import (
"bytes"
"fmt"
"strings"
)
// Header is the initial comments "SOME DESCRIPTIVE TITLE", "YEAR"
// and "FIRST AUTHOR <EMAIL@ADDRESS>, YEAR" ought to be replaced by sensible information.
//
// See http://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html#Header-Entry
type Header struct {
ProjectIdVersion string // Project-Id-Version: PACKAGE VERSION
ReportMsgidBugsTo string // Report-Msgid-Bugs-To: FIRST AUTHOR <EMAIL@ADDRESS>
POTCreationDate string // POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE
PORevisionDate string // PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
LastTranslator string // Last-Translator: FIRST AUTHOR <EMAIL@ADDRESS>
LanguageTeam string // Language-Team: golang-china
Language string // Language: zh_CN
MimeVersion string // MIME-Version: 1.0
ContentType string // Content-Type: text/plain; charset=UTF-8
ContentTransferEncoding string // Content-Transfer-Encoding: 8bit
PluralForms string // Plural-Forms: nplurals=2; plural=n == 1 ? 0 : 1;
XGenerator string // X-Generator: Poedit 1.5.5
UnknowFields map[string]string
}
func (p *Header) fromMessage(msg *Message) {
if msg.MsgId != "" || msg.MsgStr == "" {
return
}
lines := strings.Split(msg.MsgStr, "\n")
for i := 0; i < len(lines); i++ {
idx := strings.Index(lines[i], ":")
if idx < 0 {
continue
}
key := strings.TrimSpace(lines[i][:idx])
val := strings.TrimSpace(lines[i][idx+1:])
switch strings.ToUpper(key) {
case strings.ToUpper("Project-Id-Version"):
p.ProjectIdVersion = val
case strings.ToUpper("Report-Msgid-Bugs-To"):
p.ReportMsgidBugsTo = val
case strings.ToUpper("POT-Creation-Date"):
p.POTCreationDate = val
case strings.ToUpper("PO-Revision-Date"):
p.PORevisionDate = val
case strings.ToUpper("Last-Translator"):
p.LastTranslator = val
case strings.ToUpper("Language-Team"):
p.LanguageTeam = val
case strings.ToUpper("Language"):
p.Language = val
case strings.ToUpper("MIME-Version"):
p.MimeVersion = val
case strings.ToUpper("Content-Type"):
p.ContentType = val
case strings.ToUpper("Content-Transfer-Encoding"):
p.ContentTransferEncoding = val
case strings.ToUpper("Plural-Forms"):
p.PluralForms = val
case strings.ToUpper("X-Generator"):
p.XGenerator = val
default:
if p.UnknowFields == nil {
p.UnknowFields = make(map[string]string)
}
p.UnknowFields[key] = val
}
}
}
func (p *Header) toMessage() Message {
return Message{
MsgStr: p.String(),
}
}
// String returns the po format header string.
func (p Header) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, `msgid ""`+"\n")
fmt.Fprintf(&buf, `msgstr ""`+"\n")
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Project-Id-Version", p.ProjectIdVersion)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Report-Msgid-Bugs-To", p.ReportMsgidBugsTo)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "POT-Creation-Date", p.POTCreationDate)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "PO-Revision-Date", p.PORevisionDate)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Last-Translator", p.LastTranslator)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Language-Team", p.LanguageTeam)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Language", p.Language)
if p.MimeVersion != "" {
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "MIME-Version", p.MimeVersion)
}
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Content-Type", p.ContentType)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Content-Transfer-Encoding", p.ContentTransferEncoding)
if p.XGenerator != "" {
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "X-Generator", p.XGenerator)
}
for k, v := range p.UnknowFields {
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", k, v)
}
return buf.String()
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mo
import (
"bytes"
"fmt"
)
// A MO file is made up of many entries,
// each entry holding the relation between an original untranslated string
// and its corresponding translation.
//
// See http://www.gnu.org/software/gettext/manual/html_node/MO-Files.html
type Message struct {
MsgContext string // msgctxt context
MsgId string // msgid untranslated-string
MsgIdPlural string // msgid_plural untranslated-string-plural
MsgStr string // msgstr translated-string
MsgStrPlural []string // msgstr[0] translated-string-case-0
}
// String returns the po format entry string.
func (p Message) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "msgid %s", encodePoString(p.MsgId))
if p.MsgIdPlural != "" {
fmt.Fprintf(&buf, "msgid_plural %s", encodePoString(p.MsgIdPlural))
}
if p.MsgStr != "" {
fmt.Fprintf(&buf, "msgstr %s", encodePoString(p.MsgStr))
}
for i := 0; i < len(p.MsgStrPlural); i++ {
fmt.Fprintf(&buf, "msgstr[%d] %s", i, encodePoString(p.MsgStrPlural[i]))
}
return buf.String()
}
func (m_i *Message) less(m_j *Message) bool {
if a, b := m_i.MsgContext, m_j.MsgContext; a != b {
return a < b
}
if a, b := m_i.MsgId, m_j.MsgId; a != b {
return a < b
}
if a, b := m_i.MsgIdPlural, m_j.MsgIdPlural; a != b {
return a < b
}
return false
}
+110
View File
@@ -0,0 +1,110 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package mo
import (
"bytes"
"strings"
)
func decodePoString(text string) string {
lines := strings.Split(text, "\n")
for i := 0; i < len(lines); i++ {
left := strings.Index(lines[i], `"`)
right := strings.LastIndex(lines[i], `"`)
if left < 0 || right < 0 || left == right {
lines[i] = ""
continue
}
line := lines[i][left+1 : right]
data := make([]byte, 0, len(line))
for i := 0; i < len(line); i++ {
if line[i] != '\\' {
data = append(data, line[i])
continue
}
if i+1 >= len(line) {
break
}
switch line[i+1] {
case 'n': // \\n -> \n
data = append(data, '\n')
i++
case 't': // \\t -> \n
data = append(data, '\t')
i++
case '\\': // \\\ -> ?
data = append(data, '\\')
i++
}
}
lines[i] = string(data)
}
return strings.Join(lines, "")
}
func encodePoString(text string) string {
var buf bytes.Buffer
lines := strings.Split(text, "\n")
for i := 0; i < len(lines); i++ {
if lines[i] == "" {
if i != len(lines)-1 {
buf.WriteString(`"\n"` + "\n")
}
continue
}
buf.WriteRune('"')
for _, r := range lines[i] {
switch r {
case '\\':
buf.WriteString(`\\`)
case '"':
buf.WriteString(`\"`)
case '\n':
buf.WriteString(`\n`)
case '\t':
buf.WriteString(`\t`)
default:
buf.WriteRune(r)
}
}
buf.WriteString(`\n"` + "\n")
}
return buf.String()
}
func encodeCommentPoString(text string) string {
var buf bytes.Buffer
lines := strings.Split(text, "\n")
if len(lines) > 1 {
buf.WriteString(`""` + "\n")
}
for i := 0; i < len(lines); i++ {
if len(lines) > 0 {
buf.WriteString("#| ")
}
buf.WriteRune('"')
for _, r := range lines[i] {
switch r {
case '\\':
buf.WriteString(`\\`)
case '"':
buf.WriteString(`\"`)
case '\n':
buf.WriteString(`\n`)
case '\t':
buf.WriteString(`\t`)
default:
buf.WriteRune(r)
}
}
if i < len(lines)-1 {
buf.WriteString(`\n"` + "\n")
} else {
buf.WriteString(`"`)
}
}
return buf.String()
}
+36
View File
@@ -0,0 +1,36 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package plural provides standard plural formulas.
Examples:
import (
"github.com/chai2010/gettext-go/plural"
)
func main() {
enFormula := plural.Formula("en_US")
xxFormula := plural.Formula("zh_CN")
fmt.Printf("%s: %d\n", "en", enFormula(0))
fmt.Printf("%s: %d\n", "en", enFormula(1))
fmt.Printf("%s: %d\n", "en", enFormula(2))
fmt.Printf("%s: %d\n", "??", xxFormula(0))
fmt.Printf("%s: %d\n", "??", xxFormula(1))
fmt.Printf("%s: %d\n", "??", xxFormula(2))
fmt.Printf("%s: %d\n", "??", xxFormula(9))
// Output:
// en: 0
// en: 0
// en: 1
// ??: 0
// ??: 0
// ??: 1
// ??: 8
}
See http://www.gnu.org/software/gettext/manual/html_node/Plural-forms.html
*/
package plural
+181
View File
@@ -0,0 +1,181 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plural
import (
"strings"
)
// Formula provides the language's standard plural formula.
func Formula(lang string) func(n int) int {
if idx := index(lang); idx != -1 {
return formulaTable[fmtForms(FormsTable[idx].Value)]
}
if idx := index("??"); idx != -1 {
return formulaTable[fmtForms(FormsTable[idx].Value)]
}
return func(n int) int {
return n
}
}
func index(lang string) int {
for i := 0; i < len(FormsTable); i++ {
if strings.HasPrefix(lang, FormsTable[i].Lang) {
return i
}
}
return -1
}
func fmtForms(forms string) string {
forms = strings.TrimSpace(forms)
forms = strings.Replace(forms, " ", "", -1)
return forms
}
var formulaTable = map[string]func(n int) int{
fmtForms("nplurals=n; plural=n-1;"): func(n int) int {
if n > 0 {
return n - 1
}
return 0
},
fmtForms("nplurals=1; plural=0;"): func(n int) int {
return 0
},
fmtForms("nplurals=2; plural=(n != 1);"): func(n int) int {
if n <= 1 {
return 0
}
return 1
},
fmtForms("nplurals=2; plural=(n > 1);"): func(n int) int {
if n <= 1 {
return 0
}
return 1
},
fmtForms("nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"): func(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n != 0 {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;"): func(n int) int {
if n == 1 {
return 0
}
if n == 2 {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;"): func(n int) int {
if n == 1 {
return 0
}
if n == 0 || (n%100 > 0 && n%100 < 20) {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"): func(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n%10 >= 2 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"): func(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"): func(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"): func(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"): func(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"): func(n int) int {
if n%10 == 1 && n%100 != 11 {
return 0
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"): func(n int) int {
if n == 1 {
return 0
}
if n >= 2 && n <= 4 {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"): func(n int) int {
if n == 1 {
return 0
}
if n >= 2 && n <= 4 {
return 1
}
return 2
},
fmtForms("nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"): func(n int) int {
if n == 1 {
return 0
}
if n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) {
return 1
}
return 2
},
fmtForms("nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"): func(n int) int {
if n%100 == 1 {
return 0
}
if n%100 == 2 {
return 1
}
if n%100 == 3 || n%100 == 4 {
return 2
}
return 3
},
}
+55
View File
@@ -0,0 +1,55 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package plural
// FormsTable are standard hard-coded plural rules.
// The application developers and the translators need to understand them.
//
// See GNU's gettext library source code: gettext/gettext-tools/src/plural-table.c
var FormsTable = []struct {
Lang string
Language string
Value string
}{
{"??", "Unknown", "nplurals=1; plural=0;"},
{"ja", "Japanese", "nplurals=1; plural=0;"},
{"vi", "Vietnamese", "nplurals=1; plural=0;"},
{"ko", "Korean", "nplurals=1; plural=0;"},
{"en", "English", "nplurals=2; plural=(n != 1);"},
{"de", "German", "nplurals=2; plural=(n != 1);"},
{"nl", "Dutch", "nplurals=2; plural=(n != 1);"},
{"sv", "Swedish", "nplurals=2; plural=(n != 1);"},
{"da", "Danish", "nplurals=2; plural=(n != 1);"},
{"no", "Norwegian", "nplurals=2; plural=(n != 1);"},
{"nb", "Norwegian Bokmal", "nplurals=2; plural=(n != 1);"},
{"nn", "Norwegian Nynorsk", "nplurals=2; plural=(n != 1);"},
{"fo", "Faroese", "nplurals=2; plural=(n != 1);"},
{"es", "Spanish", "nplurals=2; plural=(n != 1);"},
{"pt", "Portuguese", "nplurals=2; plural=(n != 1);"},
{"it", "Italian", "nplurals=2; plural=(n != 1);"},
{"bg", "Bulgarian", "nplurals=2; plural=(n != 1);"},
{"el", "Greek", "nplurals=2; plural=(n != 1);"},
{"fi", "Finnish", "nplurals=2; plural=(n != 1);"},
{"et", "Estonian", "nplurals=2; plural=(n != 1);"},
{"he", "Hebrew", "nplurals=2; plural=(n != 1);"},
{"eo", "Esperanto", "nplurals=2; plural=(n != 1);"},
{"hu", "Hungarian", "nplurals=2; plural=(n != 1);"},
{"tr", "Turkish", "nplurals=2; plural=(n != 1);"},
{"pt_BR", "Brazilian", "nplurals=2; plural=(n > 1);"},
{"fr", "French", "nplurals=2; plural=(n > 1);"},
{"lv", "Latvian", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n != 0 ? 1 : 2);"},
{"ga", "Irish", "nplurals=3; plural=n==1 ? 0 : n==2 ? 1 : 2;"},
{"ro", "Romanian", "nplurals=3; plural=n==1 ? 0 : (n==0 || (n%100 > 0 && n%100 < 20)) ? 1 : 2;"},
{"lt", "Lithuanian", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && (n%100<10 || n%100>=20) ? 1 : 2);"},
{"ru", "Russian", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"},
{"uk", "Ukrainian", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"},
{"be", "Belarusian", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"},
{"sr", "Serbian", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"},
{"hr", "Croatian", "nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"},
{"cs", "Czech", "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"},
{"sk", "Slovak", "nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2;"},
{"pl", "Polish", "nplurals=3; plural=(n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2);"},
{"sl", "Slovenian", "nplurals=4; plural=(n%100==1 ? 0 : n%100==2 ? 1 : n%100==3 || n%100==4 ? 2 : 3);"},
}
+270
View File
@@ -0,0 +1,270 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package po
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
)
// Comment represents every message's comments.
type Comment struct {
StartLine int // comment start line
TranslatorComment string // # translator-comments // TrimSpace
ExtractedComment string // #. extracted-comments
ReferenceFile []string // #: src/msgcmp.c:338 src/po-lex.c:699
ReferenceLine []int // #: src/msgcmp.c:338 src/po-lex.c:699
Flags []string // #, fuzzy,c-format,range:0..10
PrevMsgContext string // #| msgctxt previous-context
PrevMsgId string // #| msgid previous-untranslated-string
}
func (p *Comment) less(q *Comment) bool {
if p.StartLine != 0 || q.StartLine != 0 {
return p.StartLine < q.StartLine
}
if a, b := len(p.ReferenceFile), len(q.ReferenceFile); a != b {
return a < b
}
for i := 0; i < len(p.ReferenceFile); i++ {
if a, b := p.ReferenceFile[i], q.ReferenceFile[i]; a != b {
return a < b
}
if a, b := p.ReferenceLine[i], q.ReferenceLine[i]; a != b {
return a < b
}
}
return false
}
func (p *Comment) readPoComment(r *lineReader) (err error) {
*p = Comment{}
if err = r.skipBlankLine(); err != nil {
return err
}
defer func(oldPos int) {
newPos := r.currentPos()
if newPos != oldPos && err == io.EOF {
err = nil
}
}(r.currentPos())
p.StartLine = r.currentPos() + 1
for {
var s string
if s, _, err = r.currentLine(); err != nil {
return
}
if len(s) == 0 || s[0] != '#' {
return
}
if err = p.readTranslatorComment(r); err != nil {
return
}
if err = p.readExtractedComment(r); err != nil {
return
}
if err = p.readReferenceComment(r); err != nil {
return
}
if err = p.readFlagsComment(r); err != nil {
return
}
if err = p.readPrevMsgContext(r); err != nil {
return
}
if err = p.readPrevMsgId(r); err != nil {
return
}
}
}
func (p *Comment) readTranslatorComment(r *lineReader) (err error) {
const prefix = "# " // .,:|
for {
var s string
if s, _, err = r.readLine(); err != nil {
return err
}
if len(s) < 1 || s[0] != '#' {
r.unreadLine()
return nil
}
if len(s) >= 2 {
switch s[1] {
case '.', ',', ':', '|':
r.unreadLine()
return nil
}
}
if p.TranslatorComment != "" {
p.TranslatorComment += "\n"
}
p.TranslatorComment += strings.TrimSpace(s[1:])
}
}
func (p *Comment) readExtractedComment(r *lineReader) (err error) {
const prefix = "#."
for {
var s string
if s, _, err = r.readLine(); err != nil {
return err
}
if len(s) < len(prefix) || s[:len(prefix)] != prefix {
r.unreadLine()
return nil
}
if p.ExtractedComment != "" {
p.ExtractedComment += "\n"
}
p.ExtractedComment += strings.TrimSpace(s[len(prefix):])
}
}
func (p *Comment) readReferenceComment(r *lineReader) (err error) {
const prefix = "#:"
for {
var s string
if s, _, err = r.readLine(); err != nil {
return err
}
if len(s) < len(prefix) || s[:len(prefix)] != prefix {
r.unreadLine()
return nil
}
ss := strings.Split(strings.TrimSpace(s[len(prefix):]), " ")
for i := 0; i < len(ss); i++ {
idx := strings.Index(ss[i], ":")
if idx <= 0 {
continue
}
name := strings.TrimSpace(ss[i][:idx])
line, _ := strconv.Atoi(strings.TrimSpace(ss[i][idx+1:]))
p.ReferenceFile = append(p.ReferenceFile, name)
p.ReferenceLine = append(p.ReferenceLine, line)
}
}
}
func (p *Comment) readFlagsComment(r *lineReader) (err error) {
const prefix = "#,"
for {
var s string
if s, _, err = r.readLine(); err != nil {
return err
}
if len(s) < len(prefix) || s[:len(prefix)] != prefix {
r.unreadLine()
return nil
}
ss := strings.Split(strings.TrimSpace(s[len(prefix):]), ",")
for i := 0; i < len(ss); i++ {
p.Flags = append(p.Flags, strings.TrimSpace(ss[i]))
}
}
}
func (p *Comment) readPrevMsgContext(r *lineReader) (err error) {
var s string
if s, _, err = r.currentLine(); err != nil {
return
}
if !rePrevMsgContextComments.MatchString(s) {
return
}
p.PrevMsgContext, err = p.readString(r)
return
}
func (p *Comment) readPrevMsgId(r *lineReader) (err error) {
var s string
if s, _, err = r.currentLine(); err != nil {
return
}
if !rePrevMsgIdComments.MatchString(s) {
return
}
p.PrevMsgId, err = p.readString(r)
return
}
func (p *Comment) readString(r *lineReader) (msg string, err error) {
var s string
if s, _, err = r.readLine(); err != nil {
return
}
msg += decodePoString(s)
for {
if s, _, err = r.readLine(); err != nil {
return
}
if !reStringLineComments.MatchString(s) {
r.unreadLine()
break
}
msg += decodePoString(s)
}
return
}
// GetFuzzy gets the fuzzy flag.
func (p *Comment) GetFuzzy() bool {
for _, s := range p.Flags {
if s == "fuzzy" {
return true
}
}
return false
}
// SetFuzzy sets the fuzzy flag.
func (p *Comment) SetFuzzy(fuzzy bool) {
//
}
// String returns the po format comment string.
func (p Comment) String() string {
var buf bytes.Buffer
if p.TranslatorComment != "" {
ss := strings.Split(p.TranslatorComment, "\n")
for i := 0; i < len(ss); i++ {
fmt.Fprintf(&buf, "# %s\n", ss[i])
}
}
if p.ExtractedComment != "" {
ss := strings.Split(p.ExtractedComment, "\n")
for i := 0; i < len(ss); i++ {
fmt.Fprintf(&buf, "#. %s\n", ss[i])
}
}
if a, b := len(p.ReferenceFile), len(p.ReferenceLine); a != 0 && a == b {
fmt.Fprintf(&buf, "#:")
for i := 0; i < len(p.ReferenceFile); i++ {
fmt.Fprintf(&buf, " %s:%d", p.ReferenceFile[i], p.ReferenceLine[i])
}
fmt.Fprintf(&buf, "\n")
}
if len(p.Flags) != 0 {
fmt.Fprintf(&buf, "#, %s", p.Flags[0])
for i := 1; i < len(p.Flags); i++ {
fmt.Fprintf(&buf, ", %s", p.Flags[i])
}
fmt.Fprintf(&buf, "\n")
}
if p.PrevMsgContext != "" {
s := encodeCommentPoString(p.PrevMsgContext)
fmt.Fprintf(&buf, "#| msgctxt %s\n", s)
}
if p.PrevMsgId != "" {
s := encodeCommentPoString(p.PrevMsgId)
fmt.Fprintf(&buf, "#| msgid %s\n", s)
}
return buf.String()
}
+24
View File
@@ -0,0 +1,24 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
/*
Package po provides support for reading and writing GNU PO file.
Examples:
import (
"github.com/chai2010/gettext-go/po"
)
func main() {
poFile, err := po.LoadFile("test.po")
if err != nil {
log.Fatal(err)
}
fmt.Printf("%v", poFile)
}
The GNU PO file specification is at
http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html.
*/
package po
+81
View File
@@ -0,0 +1,81 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package po
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"sort"
)
// File represents an PO File.
//
// See http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
type File struct {
MimeHeader Header
Messages []Message
}
// Load loads po file format data.
func Load(data []byte) (*File, error) {
return loadData(data)
}
// LoadFile loads a named po file.
func LoadFile(path string) (*File, error) {
data, err := ioutil.ReadFile(path)
if err != nil {
return nil, err
}
return loadData(data)
}
func loadData(data []byte) (*File, error) {
r := newLineReader(string(data))
var file File
for {
var msg Message
if err := msg.readPoEntry(r); err != nil {
if err == io.EOF {
return &file, nil
}
return nil, err
}
if msg.MsgId == "" {
file.MimeHeader.parseHeader(&msg)
continue
}
file.Messages = append(file.Messages, msg)
}
}
// Save saves a po file.
func (f *File) Save(name string) error {
return ioutil.WriteFile(name, []byte(f.String()), 0666)
}
// Save returns a po file format data.
func (f *File) Data() []byte {
// sort the massge as ReferenceFile/ReferenceLine field
var messages []Message
messages = append(messages, f.Messages...)
sort.Slice(messages, func(i, j int) bool {
return messages[i].less(&messages[j])
})
var buf bytes.Buffer
fmt.Fprintf(&buf, "%s\n", f.MimeHeader.String())
for i := 0; i < len(messages); i++ {
fmt.Fprintf(&buf, "%s\n", messages[i].String())
}
return buf.Bytes()
}
// String returns the po format file string.
func (f *File) String() string {
return string(f.Data())
}
+106
View File
@@ -0,0 +1,106 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package po
import (
"bytes"
"fmt"
"strings"
)
// Header is the initial comments "SOME DESCRIPTIVE TITLE", "YEAR"
// and "FIRST AUTHOR <EMAIL@ADDRESS>, YEAR" ought to be replaced by sensible information.
//
// See http://www.gnu.org/software/gettext/manual/html_node/Header-Entry.html#Header-Entry
type Header struct {
Comment // Header Comments
ProjectIdVersion string // Project-Id-Version: PACKAGE VERSION
ReportMsgidBugsTo string // Report-Msgid-Bugs-To: FIRST AUTHOR <EMAIL@ADDRESS>
POTCreationDate string // POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE
PORevisionDate string // PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE
LastTranslator string // Last-Translator: FIRST AUTHOR <EMAIL@ADDRESS>
LanguageTeam string // Language-Team: golang-china
Language string // Language: zh_CN
MimeVersion string // MIME-Version: 1.0
ContentType string // Content-Type: text/plain; charset=UTF-8
ContentTransferEncoding string // Content-Transfer-Encoding: 8bit
PluralForms string // Plural-Forms: nplurals=2; plural=n == 1 ? 0 : 1;
XGenerator string // X-Generator: Poedit 1.5.5
UnknowFields map[string]string
}
func (p *Header) parseHeader(msg *Message) {
if msg.MsgId != "" || msg.MsgStr == "" {
return
}
lines := strings.Split(msg.MsgStr, "\n")
for i := 0; i < len(lines); i++ {
idx := strings.Index(lines[i], ":")
if idx < 0 {
continue
}
key := strings.TrimSpace(lines[i][:idx])
val := strings.TrimSpace(lines[i][idx+1:])
switch strings.ToUpper(key) {
case strings.ToUpper("Project-Id-Version"):
p.ProjectIdVersion = val
case strings.ToUpper("Report-Msgid-Bugs-To"):
p.ReportMsgidBugsTo = val
case strings.ToUpper("POT-Creation-Date"):
p.POTCreationDate = val
case strings.ToUpper("PO-Revision-Date"):
p.PORevisionDate = val
case strings.ToUpper("Last-Translator"):
p.LastTranslator = val
case strings.ToUpper("Language-Team"):
p.LanguageTeam = val
case strings.ToUpper("Language"):
p.Language = val
case strings.ToUpper("MIME-Version"):
p.MimeVersion = val
case strings.ToUpper("Content-Type"):
p.ContentType = val
case strings.ToUpper("Content-Transfer-Encoding"):
p.ContentTransferEncoding = val
case strings.ToUpper("Plural-Forms"):
p.PluralForms = val
case strings.ToUpper("X-Generator"):
p.XGenerator = val
default:
if p.UnknowFields == nil {
p.UnknowFields = make(map[string]string)
}
p.UnknowFields[key] = val
}
}
p.Comment = msg.Comment
}
// String returns the po format header string.
func (p Header) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%s", p.Comment.String())
fmt.Fprintf(&buf, `msgid ""`+"\n")
fmt.Fprintf(&buf, `msgstr ""`+"\n")
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Project-Id-Version", p.ProjectIdVersion)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Report-Msgid-Bugs-To", p.ReportMsgidBugsTo)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "POT-Creation-Date", p.POTCreationDate)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "PO-Revision-Date", p.PORevisionDate)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Last-Translator", p.LastTranslator)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Language-Team", p.LanguageTeam)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Language", p.Language)
if p.MimeVersion != "" {
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "MIME-Version", p.MimeVersion)
}
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Content-Type", p.ContentType)
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "Content-Transfer-Encoding", p.ContentTransferEncoding)
if p.XGenerator != "" {
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", "X-Generator", p.XGenerator)
}
for k, v := range p.UnknowFields {
fmt.Fprintf(&buf, `"%s: %s\n"`+"\n", k, v)
}
return buf.String()
}
+62
View File
@@ -0,0 +1,62 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package po
import (
"io"
"strings"
)
type lineReader struct {
lines []string
pos int
}
func newLineReader(data string) *lineReader {
data = strings.Replace(data, "\r", "", -1)
lines := strings.Split(data, "\n")
return &lineReader{lines: lines}
}
func (r *lineReader) skipBlankLine() error {
for ; r.pos < len(r.lines); r.pos++ {
if strings.TrimSpace(r.lines[r.pos]) != "" {
break
}
}
if r.pos >= len(r.lines) {
return io.EOF
}
return nil
}
func (r *lineReader) currentPos() int {
return r.pos
}
func (r *lineReader) currentLine() (s string, pos int, err error) {
if r.pos >= len(r.lines) {
err = io.EOF
return
}
s, pos = r.lines[r.pos], r.pos
return
}
func (r *lineReader) readLine() (s string, pos int, err error) {
if r.pos >= len(r.lines) {
err = io.EOF
return
}
s, pos = r.lines[r.pos], r.pos
r.pos++
return
}
func (r *lineReader) unreadLine() {
if r.pos >= 0 {
r.pos--
}
}
+193
View File
@@ -0,0 +1,193 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package po
import (
"bytes"
"fmt"
"io"
"strconv"
"strings"
)
// A PO file is made up of many entries,
// each entry holding the relation between an original untranslated string
// and its corresponding translation.
//
// See http://www.gnu.org/software/gettext/manual/html_node/PO-Files.html
type Message struct {
Comment // Coments
MsgContext string // msgctxt context
MsgId string // msgid untranslated-string
MsgIdPlural string // msgid_plural untranslated-string-plural
MsgStr string // msgstr translated-string
MsgStrPlural []string // msgstr[0] translated-string-case-0
}
func (p *Message) less(q *Message) bool {
if p.Comment.less(&q.Comment) {
return true
}
if a, b := p.MsgContext, q.MsgContext; a != b {
return a < b
}
if a, b := p.MsgId, q.MsgId; a != b {
return a < b
}
if a, b := p.MsgIdPlural, q.MsgIdPlural; a != b {
return a < b
}
return false
}
func (p *Message) readPoEntry(r *lineReader) (err error) {
*p = Message{}
if err = r.skipBlankLine(); err != nil {
return
}
defer func(oldPos int) {
newPos := r.currentPos()
if newPos != oldPos && err == io.EOF {
err = nil
}
}(r.currentPos())
if err = p.Comment.readPoComment(r); err != nil {
return
}
for {
var s string
if s, _, err = r.currentLine(); err != nil {
return
}
if p.isInvalidLine(s) {
err = fmt.Errorf("gettext: line %d, %v", r.currentPos(), "invalid line")
return
}
if reComment.MatchString(s) || reBlankLine.MatchString(s) {
return
}
if err = p.readMsgContext(r); err != nil {
return
}
if err = p.readMsgId(r); err != nil {
return
}
if err = p.readMsgIdPlural(r); err != nil {
return
}
if err = p.readMsgStrOrPlural(r); err != nil {
return
}
}
}
func (p *Message) readMsgContext(r *lineReader) (err error) {
var s string
if s, _, err = r.currentLine(); err != nil {
return
}
if !reMsgContext.MatchString(s) {
return
}
p.MsgContext, err = p.readString(r)
return
}
func (p *Message) readMsgId(r *lineReader) (err error) {
var s string
if s, _, err = r.currentLine(); err != nil {
return
}
if !reMsgId.MatchString(s) {
return
}
p.MsgId, err = p.readString(r)
return
}
func (p *Message) readMsgIdPlural(r *lineReader) (err error) {
var s string
if s, _, err = r.currentLine(); err != nil {
return
}
if !reMsgIdPlural.MatchString(s) {
return
}
p.MsgIdPlural, err = p.readString(r)
return nil
}
func (p *Message) readMsgStrOrPlural(r *lineReader) (err error) {
var s string
if s, _, err = r.currentLine(); err != nil {
return
}
if !reMsgStr.MatchString(s) && !reMsgStrPlural.MatchString(s) {
return
}
if reMsgStrPlural.MatchString(s) {
left, right := strings.Index(s, `[`), strings.LastIndex(s, `]`)
idx, _ := strconv.Atoi(s[left+1 : right])
s, err = p.readString(r)
if n := len(p.MsgStrPlural); (idx + 1) > n {
p.MsgStrPlural = append(p.MsgStrPlural, make([]string, (idx+1)-n)...)
}
p.MsgStrPlural[idx] = s
} else {
p.MsgStr, err = p.readString(r)
}
return nil
}
func (p *Message) readString(r *lineReader) (msg string, err error) {
var s string
if s, _, err = r.readLine(); err != nil {
return
}
msg += decodePoString(s)
for {
if s, _, err = r.readLine(); err != nil {
return
}
if !reStringLine.MatchString(s) {
r.unreadLine()
break
}
msg += decodePoString(s)
}
return
}
// String returns the po format entry string.
func (p Message) String() string {
var buf bytes.Buffer
fmt.Fprintf(&buf, "%s", p.Comment.String())
if p.MsgContext != "" {
fmt.Fprintf(&buf, "msgctxt %s", encodePoString(p.MsgContext))
}
fmt.Fprintf(&buf, "msgid %s", encodePoString(p.MsgId))
if p.MsgIdPlural != "" {
fmt.Fprintf(&buf, "msgid_plural %s", encodePoString(p.MsgIdPlural))
}
if len(p.MsgStrPlural) == 0 {
if p.MsgStr != "" {
fmt.Fprintf(&buf, "msgstr %s", encodePoString(p.MsgStr))
} else {
fmt.Fprintf(&buf, "msgstr %s", `""`+"\n")
}
} else {
for i := 0; i < len(p.MsgStrPlural); i++ {
if p.MsgStrPlural[i] != "" {
fmt.Fprintf(&buf, "msgstr[%d] %s", i, encodePoString(p.MsgStrPlural[i]))
} else {
fmt.Fprintf(&buf, "msgstr[%d] %s", i, `""`+"\n")
}
}
}
return buf.String()
}
+58
View File
@@ -0,0 +1,58 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package po
import (
"regexp"
)
var (
reComment = regexp.MustCompile(`^#`) // #
reExtractedComments = regexp.MustCompile(`^#\.`) // #.
reReferenceComments = regexp.MustCompile(`^#:`) // #:
reFlagsComments = regexp.MustCompile(`^#,`) // #, fuzzy,c-format
rePrevMsgContextComments = regexp.MustCompile(`^#\|\s+msgctxt`) // #| msgctxt
rePrevMsgIdComments = regexp.MustCompile(`^#\|\s+msgid`) // #| msgid
reStringLineComments = regexp.MustCompile(`^#\|\s+".*"\s*$`) // #| "message"
reMsgContext = regexp.MustCompile(`^msgctxt\s+".*"\s*$`) // msgctxt
reMsgId = regexp.MustCompile(`^msgid\s+".*"\s*$`) // msgid
reMsgIdPlural = regexp.MustCompile(`^msgid_plural\s+".*"\s*$`) // msgid_plural
reMsgStr = regexp.MustCompile(`^msgstr\s*".*"\s*$`) // msgstr
reMsgStrPlural = regexp.MustCompile(`^msgstr\s*(\[\d+\])\s*".*"\s*$`) // msgstr[0]
reStringLine = regexp.MustCompile(`^\s*".*"\s*$`) // "message"
reBlankLine = regexp.MustCompile(`^\s*$`) //
)
func (p *Message) isInvalidLine(s string) bool {
if reComment.MatchString(s) {
return false
}
if reBlankLine.MatchString(s) {
return false
}
if reMsgContext.MatchString(s) {
return false
}
if reMsgId.MatchString(s) {
return false
}
if reMsgIdPlural.MatchString(s) {
return false
}
if reMsgStr.MatchString(s) {
return false
}
if reMsgStrPlural.MatchString(s) {
return false
}
if reStringLine.MatchString(s) {
return false
}
return true
}
+114
View File
@@ -0,0 +1,114 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package po
import (
"bytes"
"strings"
)
func decodePoString(text string) string {
lines := strings.Split(text, "\n")
for i := 0; i < len(lines); i++ {
left := strings.Index(lines[i], `"`)
right := strings.LastIndex(lines[i], `"`)
if left < 0 || right < 0 || left == right {
lines[i] = ""
continue
}
line := lines[i][left+1 : right]
data := make([]byte, 0, len(line))
for i := 0; i < len(line); i++ {
if line[i] != '\\' {
data = append(data, line[i])
continue
}
if i+1 >= len(line) {
break
}
switch line[i+1] {
case 'n': // \\n -> \n
data = append(data, '\n')
i++
case 't': // \\t -> \n
data = append(data, '\t')
i++
case '\\': // \\\ -> ?
data = append(data, '\\')
i++
}
}
lines[i] = string(data)
}
return strings.Join(lines, "")
}
func encodePoString(text string) string {
var buf bytes.Buffer
lines := strings.Split(text, "\n")
for i := 0; i < len(lines); i++ {
if lines[i] == "" {
if i != len(lines)-1 {
buf.WriteString(`"\n"` + "\n")
}
continue
}
buf.WriteRune('"')
for _, r := range lines[i] {
switch r {
case '\\':
buf.WriteString(`\\`)
case '"':
buf.WriteString(`\"`)
case '\n':
buf.WriteString(`\n`)
case '\t':
buf.WriteString(`\t`)
default:
buf.WriteRune(r)
}
}
if i < len(lines)-1 {
buf.WriteString(`\n"` + "\n")
} else {
buf.WriteString(`"` + "\n")
}
}
return buf.String()
}
func encodeCommentPoString(text string) string {
var buf bytes.Buffer
lines := strings.Split(text, "\n")
if len(lines) > 1 {
buf.WriteString(`""` + "\n")
}
for i := 0; i < len(lines); i++ {
if len(lines) > 0 {
buf.WriteString("#| ")
}
buf.WriteRune('"')
for _, r := range lines[i] {
switch r {
case '\\':
buf.WriteString(`\\`)
case '"':
buf.WriteString(`\"`)
case '\n':
buf.WriteString(`\n`)
case '\t':
buf.WriteString(`\t`)
default:
buf.WriteRune(r)
}
}
if i < len(lines)-1 {
buf.WriteString(`\n"` + "\n")
} else {
buf.WriteString(`"`)
}
}
return buf.String()
}
+175
View File
@@ -0,0 +1,175 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gettext
import (
"encoding/json"
"github.com/chai2010/gettext-go/mo"
"github.com/chai2010/gettext-go/plural"
"github.com/chai2010/gettext-go/po"
)
var nilTranslator = &translator{
MessageMap: make(map[string]mo.Message),
PluralFormula: plural.Formula("??"),
}
type translator struct {
MessageMap map[string]mo.Message
PluralFormula func(n int) int
}
func newMoTranslator(name string, data []byte) (*translator, error) {
var (
f *mo.File
err error
)
if len(data) != 0 {
f, err = mo.Load(data)
} else {
f, err = mo.LoadFile(name)
}
if err != nil {
return nil, err
}
var tr = &translator{
MessageMap: make(map[string]mo.Message),
}
for _, v := range f.Messages {
tr.MessageMap[tr.makeMapKey(v.MsgContext, v.MsgId)] = v
}
if lang := f.MimeHeader.Language; lang != "" {
tr.PluralFormula = plural.Formula(lang)
} else {
tr.PluralFormula = plural.Formula("??")
}
return tr, nil
}
func newPoTranslator(name string, data []byte) (*translator, error) {
var (
f *po.File
err error
)
if len(data) != 0 {
f, err = po.Load(data)
} else {
f, err = po.LoadFile(name)
}
if err != nil {
return nil, err
}
var tr = &translator{
MessageMap: make(map[string]mo.Message),
}
for _, v := range f.Messages {
tr.MessageMap[tr.makeMapKey(v.MsgContext, v.MsgId)] = mo.Message{
MsgContext: v.MsgContext,
MsgId: v.MsgId,
MsgIdPlural: v.MsgIdPlural,
MsgStr: v.MsgStr,
MsgStrPlural: v.MsgStrPlural,
}
}
if lang := f.MimeHeader.Language; lang != "" {
tr.PluralFormula = plural.Formula(lang)
} else {
tr.PluralFormula = plural.Formula("??")
}
return tr, nil
}
func newJsonTranslator(lang, name string, jsonData []byte) (*translator, error) {
var msgList []struct {
MsgContext string `json:"msgctxt"` // msgctxt context
MsgId string `json:"msgid"` // msgid untranslated-string
MsgIdPlural string `json:"msgid_plural"` // msgid_plural untranslated-string-plural
MsgStr []string `json:"msgstr"` // msgstr translated-string
}
if err := json.Unmarshal(jsonData, &msgList); err != nil {
return nil, err
}
var tr = &translator{
MessageMap: make(map[string]mo.Message),
PluralFormula: plural.Formula(lang),
}
for _, v := range msgList {
var v_MsgStr string
var v_MsgStrPlural = v.MsgStr
if len(v.MsgStr) != 0 {
v_MsgStr = v.MsgStr[0]
}
tr.MessageMap[tr.makeMapKey(v.MsgContext, v.MsgId)] = mo.Message{
MsgContext: v.MsgContext,
MsgId: v.MsgId,
MsgIdPlural: v.MsgIdPlural,
MsgStr: v_MsgStr,
MsgStrPlural: v_MsgStrPlural,
}
}
return tr, nil
}
func (p *translator) PGettext(msgctxt, msgid string) string {
return p.findMsgStr(msgctxt, msgid)
}
func (p *translator) PNGettext(msgctxt, msgid, msgidPlural string, n int) string {
n = p.PluralFormula(n)
if ss := p.findMsgStrPlural(msgctxt, msgid, msgidPlural); len(ss) != 0 {
if n >= len(ss) {
n = len(ss) - 1
}
if ss[n] != "" {
return ss[n]
}
}
if msgidPlural != "" && n > 0 {
return msgidPlural
}
return msgid
}
func (p *translator) findMsgStr(msgctxt, msgid string) string {
key := p.makeMapKey(msgctxt, msgid)
if v, ok := p.MessageMap[key]; ok {
if v.MsgStr != "" {
return v.MsgStr
}
}
return msgid
}
func (p *translator) findMsgStrPlural(msgctxt, msgid, msgidPlural string) []string {
key := p.makeMapKey(msgctxt, msgid)
if v, ok := p.MessageMap[key]; ok {
if len(v.MsgIdPlural) != 0 {
if len(v.MsgStrPlural) != 0 {
return v.MsgStrPlural
} else {
return nil
}
} else {
if len(v.MsgStr) != 0 {
return []string{v.MsgStr}
} else {
return nil
}
}
}
return nil
}
func (p *translator) makeMapKey(msgctxt, msgid string) string {
if msgctxt != "" {
return msgctxt + mo.EotSeparator + msgid
}
return msgid
}
+34
View File
@@ -0,0 +1,34 @@
// Copyright 2013 ChaiShushan <chaishushan{AT}gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package gettext
import (
"os"
"strings"
)
func getDefaultLanguage() string {
if v := os.Getenv("LC_MESSAGES"); v != "" {
return simplifiedLanguage(v)
}
if v := os.Getenv("LANG"); v != "" {
return simplifiedLanguage(v)
}
return "default"
}
func simplifiedLanguage(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)
}