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	"io"
10	"regexp"
11	"sort"
12	"strings"
13	"text/template"
14
15	"golang.org/x/text/collate"
16	"golang.org/x/text/feature/plural"
17	"golang.org/x/text/internal"
18	"golang.org/x/text/internal/catmsg"
19	"golang.org/x/text/internal/gen"
20	"golang.org/x/text/language"
21)
22
23var transRe = regexp.MustCompile(`messages\.(.*)\.json`)
24
25// Generate writes a Go file with the given package name to w, which defines a
26// Catalog with translated messages.
27func Generate(w io.Writer, pkg string, extracted *Locale, trans ...*Locale) (n int, err error) {
28	// TODO: add in external input. Right now we assume that all files are
29	// manually created and stored in the textdata directory.
30
31	// Build up index of translations and original messages.
32	translations := map[language.Tag]map[string]Message{}
33	languages := []language.Tag{}
34	langVars := []string{}
35	usedKeys := map[string]int{}
36
37	for _, loc := range trans {
38		tag := loc.Language
39		if _, ok := translations[tag]; !ok {
40			translations[tag] = map[string]Message{}
41			languages = append(languages, tag)
42		}
43		for _, m := range loc.Messages {
44			if !m.Translation.IsEmpty() {
45				for _, id := range m.ID {
46					if _, ok := translations[tag][id]; ok {
47						logf("Duplicate translation in locale %q for message %q", tag, id)
48					}
49					translations[tag][id] = m
50				}
51			}
52		}
53	}
54
55	// Verify completeness and register keys.
56	internal.SortTags(languages)
57
58	for _, tag := range languages {
59		langVars = append(langVars, strings.Replace(tag.String(), "-", "_", -1))
60		dict := translations[tag]
61		for _, msg := range extracted.Messages {
62			for _, id := range msg.ID {
63				if trans, ok := dict[id]; ok && !trans.Translation.IsEmpty() {
64					if _, ok := usedKeys[msg.Key]; !ok {
65						usedKeys[msg.Key] = len(usedKeys)
66					}
67					break
68				}
69				// TODO: log missing entry.
70				logf("%s: Missing entry for %q.", tag, id)
71			}
72		}
73	}
74
75	cw := gen.NewCodeWriter()
76
77	x := &struct {
78		Fallback  language.Tag
79		Languages []string
80	}{
81		Fallback:  extracted.Language,
82		Languages: langVars,
83	}
84
85	if err := lookup.Execute(cw, x); err != nil {
86		return 0, wrap(err, "error")
87	}
88
89	keyToIndex := []string{}
90	for k := range usedKeys {
91		keyToIndex = append(keyToIndex, k)
92	}
93	sort.Strings(keyToIndex)
94	fmt.Fprint(cw, "var messageKeyToIndex = map[string]int{\n")
95	for _, k := range keyToIndex {
96		fmt.Fprintf(cw, "%q: %d,\n", k, usedKeys[k])
97	}
98	fmt.Fprint(cw, "}\n\n")
99
100	for i, tag := range languages {
101		dict := translations[tag]
102		a := make([]string, len(usedKeys))
103		for _, msg := range extracted.Messages {
104			for _, id := range msg.ID {
105				if trans, ok := dict[id]; ok && !trans.Translation.IsEmpty() {
106					m, err := assemble(&msg, &trans.Translation)
107					if err != nil {
108						return 0, wrap(err, "error")
109					}
110					// TODO: support macros.
111					data, err := catmsg.Compile(tag, nil, m)
112					if err != nil {
113						return 0, wrap(err, "error")
114					}
115					key := usedKeys[msg.Key]
116					if d := a[key]; d != "" && d != data {
117						logf("Duplicate non-consistent translation for key %q, picking the one for message %q", msg.Key, id)
118					}
119					a[key] = string(data)
120					break
121				}
122			}
123		}
124		index := []uint32{0}
125		p := 0
126		for _, s := range a {
127			p += len(s)
128			index = append(index, uint32(p))
129		}
130
131		cw.WriteVar(langVars[i]+"Index", index)
132		cw.WriteConst(langVars[i]+"Data", strings.Join(a, ""))
133	}
134	return cw.WriteGo(w, pkg, "")
135}
136
137func assemble(m *Message, t *Text) (msg catmsg.Message, err error) {
138	keys := []string{}
139	for k := range t.Var {
140		keys = append(keys, k)
141	}
142	sort.Strings(keys)
143	var a []catmsg.Message
144	for _, k := range keys {
145		t := t.Var[k]
146		m, err := assemble(m, &t)
147		if err != nil {
148			return nil, err
149		}
150		a = append(a, &catmsg.Var{Name: k, Message: m})
151	}
152	if t.Select != nil {
153		s, err := assembleSelect(m, t.Select)
154		if err != nil {
155			return nil, err
156		}
157		a = append(a, s)
158	}
159	if t.Msg != "" {
160		sub, err := m.Substitute(t.Msg)
161		if err != nil {
162			return nil, err
163		}
164		a = append(a, catmsg.String(sub))
165	}
166	switch len(a) {
167	case 0:
168		return nil, errorf("generate: empty message")
169	case 1:
170		return a[0], nil
171	default:
172		return catmsg.FirstOf(a), nil
173
174	}
175}
176
177func assembleSelect(m *Message, s *Select) (msg catmsg.Message, err error) {
178	cases := []string{}
179	for c := range s.Cases {
180		cases = append(cases, c)
181	}
182	sortCases(cases)
183
184	caseMsg := []interface{}{}
185	for _, c := range cases {
186		cm := s.Cases[c]
187		m, err := assemble(m, &cm)
188		if err != nil {
189			return nil, err
190		}
191		caseMsg = append(caseMsg, c, m)
192	}
193
194	ph := m.Placeholder(s.Arg)
195
196	switch s.Feature {
197	case "plural":
198		// TODO: only printf-style selects are supported as of yet.
199		return plural.Selectf(ph.ArgNum, ph.String, caseMsg...), nil
200	}
201	return nil, errorf("unknown feature type %q", s.Feature)
202}
203
204func sortCases(cases []string) {
205	// TODO: implement full interface.
206	sort.Slice(cases, func(i, j int) bool {
207		if cases[j] == "other" && cases[i] != "other" {
208			return true
209		}
210		// the following code relies on '<' < '=' < any letter.
211		return cmpNumeric(cases[i], cases[j]) == -1
212	})
213}
214
215var cmpNumeric = collate.New(language.Und, collate.Numeric).CompareString
216
217var lookup = template.Must(template.New("gen").Parse(`
218import (
219	"golang.org/x/text/language"
220	"golang.org/x/text/message"
221	"golang.org/x/text/message/catalog"
222)
223
224type dictionary struct {
225	index []uint32
226	data  string
227}
228
229func (d *dictionary) Lookup(key string) (data string, ok bool) {
230	p := messageKeyToIndex[key]
231	start, end := d.index[p], d.index[p+1]
232	if start == end {
233		return "", false
234	}
235	return d.data[start:end], true
236}
237
238func init() {
239	dict := map[string]catalog.Dictionary{
240		{{range .Languages}}"{{.}}": &dictionary{index: {{.}}Index, data: {{.}}Data },
241		{{end}}
242	}
243	fallback := language.MustParse("{{.Fallback}}")
244	cat, err := catalog.NewFromMap(dict, catalog.Fallback(fallback))
245	if err != nil {
246		panic(err)
247	}
248	message.DefaultCatalog = cat
249}
250
251`))
252