| // 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" |
| "os" |
| "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. |
| // Translations are retrieved from s.Messages, not s.Translations, so it |
| // is assumed Merge has been called. |
| 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 := filepath.SplitList(build.Default.GOPATH)[0] |
| path = filepath.Join(gopath, "src", filepath.FromSlash(pkgs[0].Pkg.Path())) |
| } |
| if len(s.Config.GenFile) == 0 { |
| cw.WriteGo(os.Stdout, pkg, "") |
| return nil |
| } |
| if filepath.IsAbs(s.Config.GenFile) { |
| path = s.Config.GenFile |
| } else { |
| 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. Translations are retrieved from s.Messages, |
| // not s.Translations, so it is assumed Merge has been called. |
| 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) { |
| // Build up index of translations and original messages. |
| translations := map[language.Tag]map[string]Message{} |
| languages := []language.Tag{} |
| usedKeys := map[string]int{} |
| |
| for _, loc := range s.Messages { |
| 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) |
| |
| langVars := []string{} |
| 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 { |
| switch { |
| case cases[i] != "other" && cases[j] == "other": |
| return true |
| case cases[i] == "other" && cases[j] != "other": |
| return false |
| } |
| // 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, ok := messageKeyToIndex[key] |
| if !ok { |
| return "", false |
| } |
| 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 |
| } |
| |
| `)) |