github.com/hugh712/snapd@v0.0.0-20200910133618-1a99902bd583/i18n/xgettext-go/main.go (about)

     1  package main
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"go/ast"
     7  	"go/parser"
     8  	"go/token"
     9  	"io"
    10  	"io/ioutil"
    11  	"log"
    12  	"os"
    13  	"sort"
    14  	"strings"
    15  	"time"
    16  
    17  	"github.com/jessevdk/go-flags"
    18  )
    19  
    20  type msgID struct {
    21  	msgidPlural string
    22  	comment     string
    23  	fname       string
    24  	line        int
    25  	formatHint  string
    26  }
    27  
    28  var msgIDs map[string][]msgID
    29  
    30  func formatComment(com string) string {
    31  	out := ""
    32  	for _, rawline := range strings.Split(com, "\n") {
    33  		line := rawline
    34  		line = strings.TrimPrefix(line, "//")
    35  		line = strings.TrimPrefix(line, "/*")
    36  		line = strings.TrimSuffix(line, "*/")
    37  		line = strings.TrimSpace(line)
    38  		if line != "" {
    39  			out += fmt.Sprintf("#. %s\n", line)
    40  		}
    41  	}
    42  
    43  	return out
    44  }
    45  
    46  func findCommentsForTranslation(fset *token.FileSet, f *ast.File, posCall token.Position) string {
    47  	com := ""
    48  	for _, cg := range f.Comments {
    49  		// search for all comments in the previous line
    50  		for i := len(cg.List) - 1; i >= 0; i-- {
    51  			c := cg.List[i]
    52  
    53  			posComment := fset.Position(c.End())
    54  			//println(posCall.Line, posComment.Line, c.Text)
    55  			if posCall.Line == posComment.Line+1 {
    56  				posCall = posComment
    57  				com = fmt.Sprintf("%s\n%s", c.Text, com)
    58  			}
    59  		}
    60  	}
    61  
    62  	// only return if we have a matching prefix
    63  	formatedComment := formatComment(com)
    64  	needle := fmt.Sprintf("#. %s", opts.AddCommentsTag)
    65  	if !strings.HasPrefix(formatedComment, needle) {
    66  		formatedComment = ""
    67  	}
    68  
    69  	return formatedComment
    70  }
    71  
    72  func constructValue(val interface{}) string {
    73  	switch val.(type) {
    74  	case *ast.BasicLit:
    75  		return val.(*ast.BasicLit).Value
    76  	// this happens for constructs like:
    77  	//  gettext.Gettext("foo" + "bar")
    78  	case *ast.BinaryExpr:
    79  		// we only support string concat
    80  		if val.(*ast.BinaryExpr).Op != token.ADD {
    81  			return ""
    82  		}
    83  		left := constructValue(val.(*ast.BinaryExpr).X)
    84  		// strip right " (or `)
    85  		left = left[0 : len(left)-1]
    86  		right := constructValue(val.(*ast.BinaryExpr).Y)
    87  		// strip left " (or `)
    88  		right = right[1:]
    89  		return left + right
    90  	default:
    91  		panic(fmt.Sprintf("unknown type: %v", val))
    92  	}
    93  }
    94  
    95  func inspectNodeForTranslations(fset *token.FileSet, f *ast.File, n ast.Node) bool {
    96  	// FIXME: this assume we always have a "gettext.Gettext" style keyword
    97  	l := strings.Split(opts.Keyword, ".")
    98  	gettextSelector := l[0]
    99  	gettextFuncName := l[1]
   100  
   101  	l = strings.Split(opts.KeywordPlural, ".")
   102  	gettextSelectorPlural := l[0]
   103  	gettextFuncNamePlural := l[1]
   104  
   105  	switch x := n.(type) {
   106  	case *ast.CallExpr:
   107  		if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
   108  			i18nStr := ""
   109  			i18nStrPlural := ""
   110  			if sel.Sel.Name == gettextFuncNamePlural && sel.X.(*ast.Ident).Name == gettextSelectorPlural {
   111  				i18nStr = x.Args[0].(*ast.BasicLit).Value
   112  				i18nStrPlural = x.Args[1].(*ast.BasicLit).Value
   113  			}
   114  
   115  			if sel.Sel.Name == gettextFuncName && sel.X.(*ast.Ident).Name == gettextSelector {
   116  				i18nStr = constructValue(x.Args[0])
   117  			}
   118  
   119  			formatI18nStr := func(s string) string {
   120  				if s == "" {
   121  					return ""
   122  				}
   123  				// the "`" is special
   124  				if s[0] == '`' {
   125  					// replace inner " with \"
   126  					s = strings.Replace(s, "\"", "\\\"", -1)
   127  					// replace \n with \\n
   128  					s = strings.Replace(s, "\n", "\\n", -1)
   129  				}
   130  				// strip leading and trailing " (or `)
   131  				s = s[1 : len(s)-1]
   132  				return s
   133  			}
   134  
   135  			// FIXME: too simplistic(?), no %% is considered
   136  			formatHint := ""
   137  			if strings.Contains(i18nStr, "%") || strings.Contains(i18nStrPlural, "%") {
   138  				// well, not quite correct but close enough
   139  				formatHint = "c-format"
   140  			}
   141  
   142  			if i18nStr != "" {
   143  				msgidStr := formatI18nStr(i18nStr)
   144  				posCall := fset.Position(n.Pos())
   145  				msgIDs[msgidStr] = append(msgIDs[msgidStr], msgID{
   146  					formatHint:  formatHint,
   147  					msgidPlural: formatI18nStr(i18nStrPlural),
   148  					fname:       posCall.Filename,
   149  					line:        posCall.Line,
   150  					comment:     findCommentsForTranslation(fset, f, posCall),
   151  				})
   152  			}
   153  		}
   154  	}
   155  
   156  	return true
   157  }
   158  
   159  func processFiles(args []string) error {
   160  	// go over the input files
   161  	msgIDs = make(map[string][]msgID)
   162  
   163  	fset := token.NewFileSet()
   164  	for _, fname := range args {
   165  		if err := processSingleGoSource(fset, fname); err != nil {
   166  			return err
   167  		}
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  func processSingleGoSource(fset *token.FileSet, fname string) error {
   174  	fnameContent, err := ioutil.ReadFile(fname)
   175  	if err != nil {
   176  		return err
   177  	}
   178  
   179  	// Create the AST by parsing src.
   180  	f, err := parser.ParseFile(fset, fname, fnameContent, parser.ParseComments)
   181  	if err != nil {
   182  		return err
   183  	}
   184  
   185  	ast.Inspect(f, func(n ast.Node) bool {
   186  		return inspectNodeForTranslations(fset, f, n)
   187  	})
   188  
   189  	return nil
   190  }
   191  
   192  var formatTime = func() string {
   193  	return time.Now().Format("2006-01-02 15:04-0700")
   194  }
   195  
   196  // mustFprintf will write the given format string to the given
   197  // writer. Any error will make it panic.
   198  func mustFprintf(w io.Writer, format string, a ...interface{}) {
   199  	_, err := fmt.Fprintf(w, format, a...)
   200  	if err != nil {
   201  		panic(fmt.Sprintf("cannot write output: %v", err))
   202  	}
   203  }
   204  
   205  func writePotFile(out io.Writer) {
   206  
   207  	header := fmt.Sprintf(`# SOME DESCRIPTIVE TITLE.
   208  # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
   209  # This file is distributed under the same license as the PACKAGE package.
   210  # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
   211  #
   212  #, fuzzy
   213  msgid   ""
   214  msgstr  "Project-Id-Version: %s\n"
   215          "Report-Msgid-Bugs-To: %s\n"
   216          "POT-Creation-Date: %s\n"
   217          "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
   218          "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
   219          "Language-Team: LANGUAGE <LL@li.org>\n"
   220          "Language: \n"
   221          "MIME-Version: 1.0\n"
   222          "Content-Type: text/plain; charset=CHARSET\n"
   223          "Content-Transfer-Encoding: 8bit\n"
   224  
   225  `, opts.PackageName, opts.MsgIDBugsAddress, formatTime())
   226  	mustFprintf(out, "%s", header)
   227  
   228  	// yes, this is the way to do it in go
   229  	sortedKeys := []string{}
   230  	for k := range msgIDs {
   231  		sortedKeys = append(sortedKeys, k)
   232  	}
   233  	if opts.SortOutput {
   234  		sort.Strings(sortedKeys)
   235  	}
   236  
   237  	// FIXME: use template here?
   238  	for _, k := range sortedKeys {
   239  		msgidList := msgIDs[k]
   240  		for _, msgid := range msgidList {
   241  			if opts.AddComments || opts.AddCommentsTag != "" {
   242  				mustFprintf(out, "%s", msgid.comment)
   243  			}
   244  		}
   245  		if !opts.NoLocation {
   246  			mustFprintf(out, "#:")
   247  			for _, msgid := range msgidList {
   248  				mustFprintf(out, " %s:%d", msgid.fname, msgid.line)
   249  			}
   250  			mustFprintf(out, "\n")
   251  		}
   252  		msgid := msgidList[0]
   253  		if msgid.formatHint != "" {
   254  			mustFprintf(out, "#, %s\n", msgid.formatHint)
   255  		}
   256  		var formatOutput = func(in string) string {
   257  			// split string with \n into multiple lines
   258  			// to make the output nicer
   259  			out := strings.Replace(in, "\\n", "\\n\"\n        \"", -1)
   260  			// cleanup too aggressive splitting (empty "" lines)
   261  			return strings.TrimSuffix(out, "\"\n        \"")
   262  		}
   263  		mustFprintf(out, "msgid   \"%v\"\n", formatOutput(k))
   264  		if msgid.msgidPlural != "" {
   265  			mustFprintf(out, "msgid_plural   \"%v\"\n", formatOutput(msgid.msgidPlural))
   266  			mustFprintf(out, "msgstr[0]  \"\"\n")
   267  			mustFprintf(out, "msgstr[1]  \"\"\n")
   268  		} else {
   269  			mustFprintf(out, "msgstr  \"\"\n")
   270  		}
   271  		mustFprintf(out, "\n")
   272  	}
   273  
   274  }
   275  
   276  // FIXME: this must be setable via go-flags
   277  var opts struct {
   278  	FilesFrom string `short:"f" long:"files-from" description:"get list of input files from FILE"`
   279  
   280  	Output string `short:"o" long:"output" description:"output to specified file"`
   281  
   282  	AddComments bool `short:"c" long:"add-comments" description:"place all comment blocks preceding keyword lines in output file"`
   283  
   284  	AddCommentsTag string `long:"add-comments-tag" description:"place comment blocks starting with TAG and prceding keyword lines in output file"`
   285  
   286  	SortOutput bool `short:"s" long:"sort-output" description:"generate sorted output"`
   287  
   288  	NoLocation bool `long:"no-location" description:"do not write '#: filename:line' lines"`
   289  
   290  	MsgIDBugsAddress string `long:"msgid-bugs-address" default:"EMAIL" description:"set report address for msgid bugs"`
   291  
   292  	PackageName string `long:"package-name" description:"set package name in output"`
   293  
   294  	Keyword       string `short:"k" long:"keyword" default:"gettext.Gettext" description:"look for WORD as the keyword for singular strings"`
   295  	KeywordPlural string `long:"keyword-plural" default:"gettext.NGettext" description:"look for WORD as the keyword for plural strings"`
   296  }
   297  
   298  func main() {
   299  	// parse args
   300  	args, err := flags.ParseArgs(&opts, os.Args)
   301  	if err != nil {
   302  		log.Fatalf("ParseArgs failed %s", err)
   303  	}
   304  
   305  	var files []string
   306  	if opts.FilesFrom != "" {
   307  		content, err := ioutil.ReadFile(opts.FilesFrom)
   308  		if err != nil {
   309  			log.Fatalf("cannot read file %v: %v", opts.FilesFrom, err)
   310  		}
   311  		content = bytes.TrimSpace(content)
   312  		files = strings.Split(string(content), "\n")
   313  	} else {
   314  		files = args[1:]
   315  	}
   316  	if err := processFiles(files); err != nil {
   317  		log.Fatalf("processFiles failed with: %s", err)
   318  	}
   319  
   320  	out := os.Stdout
   321  	if opts.Output != "" {
   322  		var err error
   323  		out, err = os.Create(opts.Output)
   324  		if err != nil {
   325  			log.Fatalf("failed to create %s: %s", opts.Output, err)
   326  		}
   327  	}
   328  	writePotFile(out)
   329  }