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  `))