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