github.com/liquid-dev/text@v0.3.3-liquid/message/pipeline/generate.go (about) 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 5 package pipeline 6 7 import ( 8 "fmt" 9 "go/build" 10 "io" 11 "path/filepath" 12 "regexp" 13 "sort" 14 "strings" 15 "text/template" 16 17 "github.com/liquid-dev/text/collate" 18 "github.com/liquid-dev/text/feature/plural" 19 "github.com/liquid-dev/text/internal" 20 "github.com/liquid-dev/text/internal/catmsg" 21 "github.com/liquid-dev/text/internal/gen" 22 "github.com/liquid-dev/text/language" 23 "github.com/liquid-dev/tools/go/loader" 24 ) 25 26 var 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. 31 func (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. 67 func (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(). 77 func 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 89 func (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 204 func 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 244 func 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 271 func 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 285 var cmpNumeric = collate.New(language.Und, collate.Numeric).CompareString 286 287 var lookup = template.Must(template.New("gen").Parse(` 288 import ( 289 "github.com/liquid-dev/text/language" 290 "github.com/liquid-dev/text/message" 291 "github.com/liquid-dev/text/message/catalog" 292 ) 293 294 type dictionary struct { 295 index []uint32 296 data string 297 } 298 299 func (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 311 func 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 `))