315 lines
7.3 KiB
Go
315 lines
7.3 KiB
Go
|
// Copyright 2017 The Go Authors. All rights reserved.
|
||
|
// Use of this source code is governed by a BSD-style
|
||
|
// license that can be found in the LICENSE file.
|
||
|
|
||
|
package pipeline
|
||
|
|
||
|
import (
|
||
|
"fmt"
|
||
|
"go/build"
|
||
|
"io"
|
||
|
"path/filepath"
|
||
|
"regexp"
|
||
|
"sort"
|
||
|
"strings"
|
||
|
"text/template"
|
||
|
|
||
|
"golang.org/x/text/collate"
|
||
|
"golang.org/x/text/feature/plural"
|
||
|
"golang.org/x/text/internal"
|
||
|
"golang.org/x/text/internal/catmsg"
|
||
|
"golang.org/x/text/internal/gen"
|
||
|
"golang.org/x/text/language"
|
||
|
"golang.org/x/tools/go/loader"
|
||
|
)
|
||
|
|
||
|
var transRe = regexp.MustCompile(`messages\.(.*)\.json`)
|
||
|
|
||
|
// Generate writes a Go file that defines a Catalog with translated messages.
|
||
|
func (s *State) Generate() error {
|
||
|
path := s.Config.GenPackage
|
||
|
if path == "" {
|
||
|
path = "."
|
||
|
}
|
||
|
isDir := path[0] == '.'
|
||
|
prog, err := loadPackages(&loader.Config{}, []string{path})
|
||
|
if err != nil {
|
||
|
return wrap(err, "could not load package")
|
||
|
}
|
||
|
pkgs := prog.InitialPackages()
|
||
|
if len(pkgs) != 1 {
|
||
|
return errorf("more than one package selected: %v", pkgs)
|
||
|
}
|
||
|
pkg := pkgs[0].Pkg.Name()
|
||
|
|
||
|
cw, err := s.generate()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
if !isDir {
|
||
|
gopath := build.Default.GOPATH
|
||
|
path = filepath.Join(gopath, filepath.FromSlash(pkgs[0].Pkg.Path()))
|
||
|
}
|
||
|
path = filepath.Join(path, s.Config.GenFile)
|
||
|
cw.WriteGoFile(path, pkg) // TODO: WriteGoFile should return error.
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// WriteGen writes a Go file with the given package name to w that defines a
|
||
|
// Catalog with translated messages.
|
||
|
func (s *State) WriteGen(w io.Writer, pkg string) error {
|
||
|
cw, err := s.generate()
|
||
|
if err != nil {
|
||
|
return err
|
||
|
}
|
||
|
_, err = cw.WriteGo(w, pkg, "")
|
||
|
return err
|
||
|
}
|
||
|
|
||
|
// Generate is deprecated; use (*State).Generate().
|
||
|
func Generate(w io.Writer, pkg string, extracted *Messages, trans ...Messages) (n int, err error) {
|
||
|
s := State{
|
||
|
Extracted: *extracted,
|
||
|
Translations: trans,
|
||
|
}
|
||
|
cw, err := s.generate()
|
||
|
if err != nil {
|
||
|
return 0, err
|
||
|
}
|
||
|
return cw.WriteGo(w, pkg, "")
|
||
|
}
|
||
|
|
||
|
func (s *State) generate() (*gen.CodeWriter, error) {
|
||
|
// TODO: add in external input. Right now we assume that all files are
|
||
|
// manually created and stored in the textdata directory.
|
||
|
|
||
|
// Build up index of translations and original messages.
|
||
|
translations := map[language.Tag]map[string]Message{}
|
||
|
languages := []language.Tag{}
|
||
|
langVars := []string{}
|
||
|
usedKeys := map[string]int{}
|
||
|
|
||
|
for _, loc := range s.Translations {
|
||
|
tag := loc.Language
|
||
|
if _, ok := translations[tag]; !ok {
|
||
|
translations[tag] = map[string]Message{}
|
||
|
languages = append(languages, tag)
|
||
|
}
|
||
|
for _, m := range loc.Messages {
|
||
|
if !m.Translation.IsEmpty() {
|
||
|
for _, id := range m.ID {
|
||
|
if _, ok := translations[tag][id]; ok {
|
||
|
warnf("Duplicate translation in locale %q for message %q", tag, id)
|
||
|
}
|
||
|
translations[tag][id] = m
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Verify completeness and register keys.
|
||
|
internal.SortTags(languages)
|
||
|
|
||
|
for _, tag := range languages {
|
||
|
langVars = append(langVars, strings.Replace(tag.String(), "-", "_", -1))
|
||
|
dict := translations[tag]
|
||
|
for _, msg := range s.Extracted.Messages {
|
||
|
for _, id := range msg.ID {
|
||
|
if trans, ok := dict[id]; ok && !trans.Translation.IsEmpty() {
|
||
|
if _, ok := usedKeys[msg.Key]; !ok {
|
||
|
usedKeys[msg.Key] = len(usedKeys)
|
||
|
}
|
||
|
break
|
||
|
}
|
||
|
// TODO: log missing entry.
|
||
|
warnf("%s: Missing entry for %q.", tag, id)
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
cw := gen.NewCodeWriter()
|
||
|
|
||
|
x := &struct {
|
||
|
Fallback language.Tag
|
||
|
Languages []string
|
||
|
}{
|
||
|
Fallback: s.Extracted.Language,
|
||
|
Languages: langVars,
|
||
|
}
|
||
|
|
||
|
if err := lookup.Execute(cw, x); err != nil {
|
||
|
return nil, wrap(err, "error")
|
||
|
}
|
||
|
|
||
|
keyToIndex := []string{}
|
||
|
for k := range usedKeys {
|
||
|
keyToIndex = append(keyToIndex, k)
|
||
|
}
|
||
|
sort.Strings(keyToIndex)
|
||
|
fmt.Fprint(cw, "var messageKeyToIndex = map[string]int{\n")
|
||
|
for _, k := range keyToIndex {
|
||
|
fmt.Fprintf(cw, "%q: %d,\n", k, usedKeys[k])
|
||
|
}
|
||
|
fmt.Fprint(cw, "}\n\n")
|
||
|
|
||
|
for i, tag := range languages {
|
||
|
dict := translations[tag]
|
||
|
a := make([]string, len(usedKeys))
|
||
|
for _, msg := range s.Extracted.Messages {
|
||
|
for _, id := range msg.ID {
|
||
|
if trans, ok := dict[id]; ok && !trans.Translation.IsEmpty() {
|
||
|
m, err := assemble(&msg, &trans.Translation)
|
||
|
if err != nil {
|
||
|
return nil, wrap(err, "error")
|
||
|
}
|
||
|
_, leadWS, trailWS := trimWS(msg.Key)
|
||
|
if leadWS != "" || trailWS != "" {
|
||
|
m = catmsg.Affix{
|
||
|
Message: m,
|
||
|
Prefix: leadWS,
|
||
|
Suffix: trailWS,
|
||
|
}
|
||
|
}
|
||
|
// TODO: support macros.
|
||
|
data, err := catmsg.Compile(tag, nil, m)
|
||
|
if err != nil {
|
||
|
return nil, wrap(err, "error")
|
||
|
}
|
||
|
key := usedKeys[msg.Key]
|
||
|
if d := a[key]; d != "" && d != data {
|
||
|
warnf("Duplicate non-consistent translation for key %q, picking the one for message %q", msg.Key, id)
|
||
|
}
|
||
|
a[key] = string(data)
|
||
|
break
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
index := []uint32{0}
|
||
|
p := 0
|
||
|
for _, s := range a {
|
||
|
p += len(s)
|
||
|
index = append(index, uint32(p))
|
||
|
}
|
||
|
|
||
|
cw.WriteVar(langVars[i]+"Index", index)
|
||
|
cw.WriteConst(langVars[i]+"Data", strings.Join(a, ""))
|
||
|
}
|
||
|
return cw, nil
|
||
|
}
|
||
|
|
||
|
func assemble(m *Message, t *Text) (msg catmsg.Message, err error) {
|
||
|
keys := []string{}
|
||
|
for k := range t.Var {
|
||
|
keys = append(keys, k)
|
||
|
}
|
||
|
sort.Strings(keys)
|
||
|
var a []catmsg.Message
|
||
|
for _, k := range keys {
|
||
|
t := t.Var[k]
|
||
|
m, err := assemble(m, &t)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
a = append(a, &catmsg.Var{Name: k, Message: m})
|
||
|
}
|
||
|
if t.Select != nil {
|
||
|
s, err := assembleSelect(m, t.Select)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
a = append(a, s)
|
||
|
}
|
||
|
if t.Msg != "" {
|
||
|
sub, err := m.Substitute(t.Msg)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
a = append(a, catmsg.String(sub))
|
||
|
}
|
||
|
switch len(a) {
|
||
|
case 0:
|
||
|
return nil, errorf("generate: empty message")
|
||
|
case 1:
|
||
|
return a[0], nil
|
||
|
default:
|
||
|
return catmsg.FirstOf(a), nil
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
func assembleSelect(m *Message, s *Select) (msg catmsg.Message, err error) {
|
||
|
cases := []string{}
|
||
|
for c := range s.Cases {
|
||
|
cases = append(cases, c)
|
||
|
}
|
||
|
sortCases(cases)
|
||
|
|
||
|
caseMsg := []interface{}{}
|
||
|
for _, c := range cases {
|
||
|
cm := s.Cases[c]
|
||
|
m, err := assemble(m, &cm)
|
||
|
if err != nil {
|
||
|
return nil, err
|
||
|
}
|
||
|
caseMsg = append(caseMsg, c, m)
|
||
|
}
|
||
|
|
||
|
ph := m.Placeholder(s.Arg)
|
||
|
|
||
|
switch s.Feature {
|
||
|
case "plural":
|
||
|
// TODO: only printf-style selects are supported as of yet.
|
||
|
return plural.Selectf(ph.ArgNum, ph.String, caseMsg...), nil
|
||
|
}
|
||
|
return nil, errorf("unknown feature type %q", s.Feature)
|
||
|
}
|
||
|
|
||
|
func sortCases(cases []string) {
|
||
|
// TODO: implement full interface.
|
||
|
sort.Slice(cases, func(i, j int) bool {
|
||
|
if cases[j] == "other" && cases[i] != "other" {
|
||
|
return true
|
||
|
}
|
||
|
// the following code relies on '<' < '=' < any letter.
|
||
|
return cmpNumeric(cases[i], cases[j]) == -1
|
||
|
})
|
||
|
}
|
||
|
|
||
|
var cmpNumeric = collate.New(language.Und, collate.Numeric).CompareString
|
||
|
|
||
|
var lookup = template.Must(template.New("gen").Parse(`
|
||
|
import (
|
||
|
"golang.org/x/text/language"
|
||
|
"golang.org/x/text/message"
|
||
|
"golang.org/x/text/message/catalog"
|
||
|
)
|
||
|
|
||
|
type dictionary struct {
|
||
|
index []uint32
|
||
|
data string
|
||
|
}
|
||
|
|
||
|
func (d *dictionary) Lookup(key string) (data string, ok bool) {
|
||
|
p := messageKeyToIndex[key]
|
||
|
start, end := d.index[p], d.index[p+1]
|
||
|
if start == end {
|
||
|
return "", false
|
||
|
}
|
||
|
return d.data[start:end], true
|
||
|
}
|
||
|
|
||
|
func init() {
|
||
|
dict := map[string]catalog.Dictionary{
|
||
|
{{range .Languages}}"{{.}}": &dictionary{index: {{.}}Index, data: {{.}}Data },
|
||
|
{{end}}
|
||
|
}
|
||
|
fallback := language.MustParse("{{.Fallback}}")
|
||
|
cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
|
||
|
if err != nil {
|
||
|
panic(err)
|
||
|
}
|
||
|
message.DefaultCatalog = cat
|
||
|
}
|
||
|
|
||
|
`))
|