1
2
3
4
5
6
7
8 package pipeline
9
10 import (
11 "bytes"
12 "encoding/json"
13 "fmt"
14 "go/build"
15 "go/parser"
16 "io/ioutil"
17 "log"
18 "os"
19 "path/filepath"
20 "regexp"
21 "strings"
22 "text/template"
23 "unicode"
24
25 "golang.org/x/text/internal"
26 "golang.org/x/text/language"
27 "golang.org/x/text/runes"
28 "golang.org/x/tools/go/loader"
29 )
30
31 const (
32 extractFile = "extracted.gotext.json"
33 outFile = "out.gotext.json"
34 gotextSuffix = "gotext.json"
35 )
36
37
38 type Config struct {
39
40
41
42 Supported []language.Tag
43
44
45
46 SourceLanguage language.Tag
47
48 Packages []string
49
50
51
52
53 Dir string
54
55
56
57
58
59
60
61
62
63 TranslationsPattern string
64
65
66
67 OutPattern string
68
69
70
71 Format string
72
73 Ext string
74
75
76
77
78
79
80
81
82
83
84
85
86
87 GenFile string
88
89
90
91 GenPackage string
92
93
94 DeclareVar string
95
96
97
98
99 SetDefault bool
100
101
102
103
104
105
106
107 }
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124 type State struct {
125 Config Config
126
127 Package string
128 program *loader.Program
129
130 Extracted Messages `json:"messages"`
131
132
133
134
135 Messages []Messages
136
137
138 Translations []Messages
139 }
140
141 func (s *State) dir() string {
142 if d := s.Config.Dir; d != "" {
143 return d
144 }
145 return "./locales"
146 }
147
148 func outPattern(s *State) (string, error) {
149 c := s.Config
150 pat := c.OutPattern
151 if pat == "" {
152 pat = "{{.Dir}}/{{.Language}}/out.{{.Ext}}"
153 }
154
155 ext := c.Ext
156 if ext == "" {
157 ext = c.Format
158 }
159 if ext == "" {
160 ext = gotextSuffix
161 }
162 t, err := template.New("").Parse(pat)
163 if err != nil {
164 return "", wrap(err, "error parsing template")
165 }
166 buf := bytes.Buffer{}
167 err = t.Execute(&buf, map[string]string{
168 "Dir": s.dir(),
169 "Language": "%s",
170 "Ext": ext,
171 })
172 return filepath.FromSlash(buf.String()), wrap(err, "incorrect OutPattern")
173 }
174
175 var transRE = regexp.MustCompile(`.*\.` + gotextSuffix)
176
177
178 func (s *State) Import() error {
179 outPattern, err := outPattern(s)
180 if err != nil {
181 return err
182 }
183 re := transRE
184 if pat := s.Config.TranslationsPattern; pat != "" {
185 if re, err = regexp.Compile(pat); err != nil {
186 return wrapf(err, "error parsing regexp %q", s.Config.TranslationsPattern)
187 }
188 }
189 x := importer{s, outPattern, re}
190 return x.walkImport(s.dir(), s.Config.SourceLanguage)
191 }
192
193 type importer struct {
194 state *State
195 outPattern string
196 transFile *regexp.Regexp
197 }
198
199 func (i *importer) walkImport(path string, tag language.Tag) error {
200 files, err := ioutil.ReadDir(path)
201 if err != nil {
202 return nil
203 }
204 for _, f := range files {
205 name := f.Name()
206 tag := tag
207 if f.IsDir() {
208 if t, err := language.Parse(name); err == nil {
209 tag = t
210 }
211
212 if err := i.walkImport(filepath.Join(path, name), tag); err != nil {
213 return err
214 }
215 continue
216 }
217 for _, l := range strings.Split(name, ".") {
218 if t, err := language.Parse(l); err == nil {
219 tag = t
220 }
221 }
222 file := filepath.Join(path, name)
223
224 if fmt.Sprintf(i.outPattern, tag) == file {
225 continue
226 }
227
228 if !i.transFile.MatchString(name) {
229 continue
230 }
231 b, err := ioutil.ReadFile(file)
232 if err != nil {
233 return wrap(err, "read file failed")
234 }
235 var translations Messages
236 if err := json.Unmarshal(b, &translations); err != nil {
237 return wrap(err, "parsing translation file failed")
238 }
239 i.state.Translations = append(i.state.Translations, translations)
240 }
241 return nil
242 }
243
244
245 func (s *State) Merge() error {
246 if s.Messages != nil {
247 panic("already merged")
248 }
249
250
251
252
253
254
255 msgs := []*Message{}
256 keyToIDs := map[string]*Message{}
257 for _, m := range s.Extracted.Messages {
258 m := m
259 if prev, ok := keyToIDs[m.Key]; ok {
260 if err := checkEquivalence(&m, prev); err != nil {
261 warnf("Key %q matches conflicting messages: %v and %v", m.Key, prev.ID, m.ID)
262
263
264 }
265
266 continue
267 }
268 i := len(msgs)
269 msgs = append(msgs, &m)
270 keyToIDs[m.Key] = msgs[i]
271 }
272
273
274
275 idMap := map[string]bool{}
276 filtered := []*Message{}
277 for _, m := range msgs {
278 found := false
279 for _, id := range m.ID {
280 found = found || idMap[id]
281 }
282 if !found {
283 filtered = append(filtered, m)
284 }
285 for _, id := range m.ID {
286 idMap[id] = true
287 }
288 }
289
290
291 translations := map[language.Tag]map[string]Message{}
292 languages := append([]language.Tag{}, s.Config.Supported...)
293
294 for _, t := range s.Translations {
295 tag := t.Language
296 if _, ok := translations[tag]; !ok {
297 translations[tag] = map[string]Message{}
298 languages = append(languages, tag)
299 }
300 for _, m := range t.Messages {
301 if !m.Translation.IsEmpty() {
302 for _, id := range m.ID {
303 if _, ok := translations[tag][id]; ok {
304 warnf("Duplicate translation in locale %q for message %q", tag, id)
305 }
306 translations[tag][id] = m
307 }
308 }
309 }
310 }
311 languages = internal.UniqueTags(languages)
312
313 for _, tag := range languages {
314 ms := Messages{Language: tag}
315 for _, orig := range filtered {
316 m := *orig
317 m.Key = ""
318 m.Position = ""
319
320 for _, id := range m.ID {
321 if t, ok := translations[tag][id]; ok {
322 m.Translation = t.Translation
323 if t.TranslatorComment != "" {
324 m.TranslatorComment = t.TranslatorComment
325 m.Fuzzy = t.Fuzzy
326 }
327 break
328 }
329 }
330 if tag == s.Config.SourceLanguage && m.Translation.IsEmpty() {
331 m.Translation = m.Message
332 if m.TranslatorComment == "" {
333 m.TranslatorComment = "Copied from source."
334 m.Fuzzy = true
335 }
336 }
337
338
339 ms.Messages = append(ms.Messages, m)
340 }
341 s.Messages = append(s.Messages, ms)
342 }
343 return nil
344 }
345
346
347 func (s *State) Export() error {
348 path, err := outPattern(s)
349 if err != nil {
350 return wrap(err, "export failed")
351 }
352 for _, out := range s.Messages {
353
354 data, err := json.MarshalIndent(out, "", " ")
355 if err != nil {
356 return wrap(err, "JSON marshal failed")
357 }
358 file := fmt.Sprintf(path, out.Language)
359 if err := os.MkdirAll(filepath.Dir(file), 0755); err != nil {
360 return wrap(err, "dir create failed")
361 }
362 if err := ioutil.WriteFile(file, data, 0644); err != nil {
363 return wrap(err, "write failed")
364 }
365 }
366 return nil
367 }
368
369 var (
370 ws = runes.In(unicode.White_Space).Contains
371 notWS = runes.NotIn(unicode.White_Space).Contains
372 )
373
374 func trimWS(s string) (trimmed, leadWS, trailWS string) {
375 trimmed = strings.TrimRightFunc(s, ws)
376 trailWS = s[len(trimmed):]
377 if i := strings.IndexFunc(trimmed, notWS); i > 0 {
378 leadWS = trimmed[:i]
379 trimmed = trimmed[i:]
380 }
381 return trimmed, leadWS, trailWS
382 }
383
384
385 var (
386 wrap = func(err error, msg string) error {
387 if err == nil {
388 return nil
389 }
390 return fmt.Errorf("%s: %v", msg, err)
391 }
392 wrapf = func(err error, msg string, args ...interface{}) error {
393 if err == nil {
394 return nil
395 }
396 return wrap(err, fmt.Sprintf(msg, args...))
397 }
398 errorf = fmt.Errorf
399 )
400
401 func warnf(format string, args ...interface{}) {
402
403 log.Printf(format, args...)
404 }
405
406 func loadPackages(conf *loader.Config, args []string) (*loader.Program, error) {
407 if len(args) == 0 {
408 args = []string{"."}
409 }
410
411 conf.Build = &build.Default
412 conf.ParserMode = parser.ParseComments
413
414
415 args, err := conf.FromArgs(args, false)
416 if err != nil {
417 return nil, wrap(err, "loading packages failed")
418 }
419
420
421 return conf.Load()
422 }
423
View as plain text