1// Copyright 2017 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package pipeline
6
7import (
8	"fmt"
9	"go/build"
10	"io"
11	"path/filepath"
12	"regexp"
13	"sort"
14	"strings"
15	"text/template"
16
17	"golang.org/x/text/collate"
18	"golang.org/x/text/feature/plural"
19	"golang.org/x/text/internal"
20	"golang.org/x/text/internal/catmsg"
21	"golang.org/x/text/internal/gen"
22	"golang.org/x/text/language"
23	"golang.org/x/tools/go/loader"
24)
25
26var transRe = regexp.MustCompile(`messages\.(.*)\.json`)
27
28// Generate writes a Go file that defines a Catalog with translated messages.
29// Translations are retrieved from s.Messages, not s.Translations, so it
30// is assumed Merge has been called.
31func (s *State) Generate() error {
32	path := s.Config.GenPackage
33	if path == "" {
34		path = "."
35	}
36	isDir := path[0] == '.'
37	prog, err := loadPackages(&loader.Config{}, []string{path})
38	if err != nil {
39		return wrap(err, "could not load package")
40	}
41	pkgs := prog.InitialPackages()
42	if len(pkgs) != 1 {
43		return errorf("more than one package selected: %v", pkgs)
44	}
45	pkg := pkgs[0].Pkg.Name()
46
47	cw, err := s.generate()
48	if err != nil {
49		return err
50	}
51	if !isDir {
52		gopath := filepath.SplitList(build.Default.GOPATH)[0]
53		path = filepath.Join(gopath, "src", filepath.FromSlash(pkgs[0].Pkg.Path()))
54	}
55	if filepath.IsAbs(s.Config.GenFile) {
56		path = s.Config.GenFile
57	} else {
58		path = filepath.Join(path, s.Config.GenFile)
59	}
60	cw.WriteGoFile(path, pkg) // TODO: WriteGoFile should return error.
61	return err
62}
63
64// WriteGen writes a Go file with the given package name to w that defines a
65// Catalog with translated messages. Translations are retrieved from s.Messages,
66// not s.Translations, so it is assumed Merge has been called.
67func (s *State) WriteGen(w io.Writer, pkg string) error {
68	cw, err := s.generate()
69	if err != nil {
70		return err
71	}
72	_, err = cw.WriteGo(w, pkg, "")
73	return err
74}
75
76// Generate is deprecated; use (*State).Generate().
77func Generate(w io.Writer, pkg string, extracted *Messages, trans ...Messages) (n int, err error) {
78	s := State{
79		Extracted:    *extracted,
80		Translations: trans,
81	}
82	cw, err := s.generate()
83	if err != nil {
84		return 0, err
85	}
86	return cw.WriteGo(w, pkg, "")
87}
88
89func (s *State) generate() (*gen.CodeWriter, error) {
90	// Build up index of translations and original messages.
91	translations := map[language.Tag]map[string]Message{}
92	languages := []language.Tag{}
93	usedKeys := map[string]int{}
94
95	for _, loc := range s.Messages {
96		tag := loc.Language
97		if _, ok := translations[tag]; !ok {
98			translations[tag] = map[string]Message{}
99			languages = append(languages, tag)
100		}
101		for _, m := range loc.Messages {
102			if !m.Translation.IsEmpty() {
103				for _, id := range m.ID {
104					if _, ok := translations[tag][id]; ok {
105						warnf("Duplicate translation in locale %q for message %q", tag, id)
106					}
107					translations[tag][id] = m
108				}
109			}
110		}
111	}
112
113	// Verify completeness and register keys.
114	internal.SortTags(languages)
115
116	langVars := []string{}
117	for _, tag := range languages {
118		langVars = append(langVars, strings.Replace(tag.String(), "-", "_", -1))
119		dict := translations[tag]
120		for _, msg := range s.Extracted.Messages {
121			for _, id := range msg.ID {
122				if trans, ok := dict[id]; ok && !trans.Translation.IsEmpty() {
123					if _, ok := usedKeys[msg.Key]; !ok {
124						usedKeys[msg.Key] = len(usedKeys)
125					}
126					break
127				}
128				// TODO: log missing entry.
129				warnf("%s: Missing entry for %q.", tag, id)
130			}
131		}
132	}
133
134	cw := gen.NewCodeWriter()
135
136	x := &struct {
137		Fallback  language.Tag
138		Languages []string
139	}{
140		Fallback:  s.Extracted.Language,
141		Languages: langVars,
142	}
143
144	if err := lookup.Execute(cw, x); err != nil {
145		return nil, wrap(err, "error")
146	}
147
148	keyToIndex := []string{}
149	for k := range usedKeys {
150		keyToIndex = append(keyToIndex, k)
151	}
152	sort.Strings(keyToIndex)
153	fmt.Fprint(cw, "var messageKeyToIndex = map[string]int{\n")
154	for _, k := range keyToIndex {
155		fmt.Fprintf(cw, "%q: %d,\n", k, usedKeys[k])
156	}
157	fmt.Fprint(cw, "}\n\n")
158
159	for i, tag := range languages {
160		dict := translations[tag]
161		a := make([]string, len(usedKeys))
162		for _, msg := range s.Extracted.Messages {
163			for _, id := range msg.ID {
164				if trans, ok := dict[id]; ok && !trans.Translation.IsEmpty() {
165					m, err := assemble(&msg, &trans.Translation)
166					if err != nil {
167						return nil, wrap(err, "error")
168					}
169					_, leadWS, trailWS := trimWS(msg.Key)
170					if leadWS != "" || trailWS != "" {
171						m = catmsg.Affix{
172							Message: m,
173							Prefix:  leadWS,
174							Suffix:  trailWS,
175						}
176					}
177					// TODO: support macros.
178					data, err := catmsg.Compile(tag, nil, m)
179					if err != nil {
180						return nil, wrap(err, "error")
181					}
182					key := usedKeys[msg.Key]
183					if d := a[key]; d != "" && d != data {
184						warnf("Duplicate non-consistent translation for key %q, picking the one for message %q", msg.Key, id)
185					}
186					a[key] = string(data)
187					break
188				}
189			}
190		}
191		index := []uint32{0}
192		p := 0
193		for _, s := range a {
194			p += len(s)
195			index = append(index, uint32(p))
196		}
197
198		cw.WriteVar(langVars[i]+"Index", index)
199		cw.WriteConst(langVars[i]+"Data", strings.Join(a, ""))
200	}
201	return cw, nil
202}
203
204func assemble(m *Message, t *Text) (msg catmsg.Message, err error) {
205	keys := []string{}
206	for k := range t.Var {
207		keys = append(keys, k)
208	}
209	sort.Strings(keys)
210	var a []catmsg.Message
211	for _, k := range keys {
212		t := t.Var[k]
213		m, err := assemble(m, &t)
214		if err != nil {
215			return nil, err
216		}
217		a = append(a, &catmsg.Var{Name: k, Message: m})
218	}
219	if t.Select != nil {
220		s, err := assembleSelect(m, t.Select)
221		if err != nil {
222			return nil, err
223		}
224		a = append(a, s)
225	}
226	if t.Msg != "" {
227		sub, err := m.Substitute(t.Msg)
228		if err != nil {
229			return nil, err
230		}
231		a = append(a, catmsg.String(sub))
232	}
233	switch len(a) {
234	case 0:
235		return nil, errorf("generate: empty message")
236	case 1:
237		return a[0], nil
238	default:
239		return catmsg.FirstOf(a), nil
240
241	}
242}
243
244func assembleSelect(m *Message, s *Select) (msg catmsg.Message, err error) {
245	cases := []string{}
246	for c := range s.Cases {
247		cases = append(cases, c)
248	}
249	sortCases(cases)
250
251	caseMsg := []interface{}{}
252	for _, c := range cases {
253		cm := s.Cases[c]
254		m, err := assemble(m, &cm)
255		if err != nil {
256			return nil, err
257		}
258		caseMsg = append(caseMsg, c, m)
259	}
260
261	ph := m.Placeholder(s.Arg)
262
263	switch s.Feature {
264	case "plural":
265		// TODO: only printf-style selects are supported as of yet.
266		return plural.Selectf(ph.ArgNum, ph.String, caseMsg...), nil
267	}
268	return nil, errorf("unknown feature type %q", s.Feature)
269}
270
271func sortCases(cases []string) {
272	// TODO: implement full interface.
273	sort.Slice(cases, func(i, j int) bool {
274		switch {
275		case cases[i] != "other" && cases[j] == "other":
276			return true
277		case cases[i] == "other" && cases[j] != "other":
278			return false
279		}
280		// the following code relies on '<' < '=' < any letter.
281		return cmpNumeric(cases[i], cases[j]) == -1
282	})
283}
284
285var cmpNumeric = collate.New(language.Und, collate.Numeric).CompareString
286
287var lookup = template.Must(template.New("gen").Parse(`
288import (
289	"golang.org/x/text/language"
290	"golang.org/x/text/message"
291	"golang.org/x/text/message/catalog"
292)
293
294type dictionary struct {
295	index []uint32
296	data  string
297}
298
299func (d *dictionary) Lookup(key string) (data string, ok bool) {
300	p, ok := messageKeyToIndex[key]
301	if !ok {
302		return "", false
303	}
304	start, end := d.index[p], d.index[p+1]
305	if start == end {
306		return "", false
307	}
308	return d.data[start:end], true
309}
310
311func init() {
312	dict := map[string]catalog.Dictionary{
313		{{range .Languages}}"{{.}}": &dictionary{index: {{.}}Index, data: {{.}}Data },
314		{{end}}
315	}
316	fallback := language.MustParse("{{.Fallback}}")
317	cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
318	if err != nil {
319		panic(err)
320	}
321	message.DefaultCatalog = cat
322}
323
324`))
325