working commit
This commit is contained in:
+5
@@ -0,0 +1,5 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.14"
|
||||
- tip
|
||||
+27
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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)
|
||||
}
|
||||
Reference in New Issue
Block a user