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