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