// Copyright 2016 The Go Authors. All rights reserved. // Use of this source code is governed by a BSD-style // license that can be found in the LICENSE file. package main import ( "bytes" "encoding/json" "fmt" "go/ast" "go/build" "go/constant" "go/format" "go/parser" "go/types" "io/ioutil" "os" "path" "path/filepath" "strings" "golang.org/x/tools/go/loader" ) // TODO: // - merge information into existing files // - handle different file formats (PO, XLIFF) // - handle features (gender, plural) // - message rewriting var cmdExtract = &Command{ Run: runExtract, UsageLine: "extract *", Short: "extract strings to be translated from code", } func runExtract(cmd *Command, args []string) error { if len(args) == 0 { args = []string{"."} } conf := loader.Config{ Build: &build.Default, ParserMode: parser.ParseComments, } // Use the initial packages from the command line. args, err := conf.FromArgs(args, false) if err != nil { return err } // Load, parse and type-check the whole program. iprog, err := conf.Load() if err != nil { return err } // print returns Go syntax for the specified node. print := func(n ast.Node) string { var buf bytes.Buffer format.Node(&buf, conf.Fset, n) return buf.String() } var translations []Translation for _, info := range iprog.InitialPackages() { for _, f := range info.Files { // Associate comments with nodes. cmap := ast.NewCommentMap(iprog.Fset, f, f.Comments) getComment := func(n ast.Node) string { cs := cmap.Filter(n).Comments() if len(cs) > 0 { return strings.TrimSpace(cs[0].Text()) } return "" } // Find function calls. ast.Inspect(f, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } // Skip calls of functions other than // (*message.Printer).{Sp,Fp,P}rintf. sel, ok := call.Fun.(*ast.SelectorExpr) if !ok { return true } meth := info.Selections[sel] if meth == nil || meth.Kind() != types.MethodVal { return true } // TODO: remove cheap hack and check if the type either // implements some interface or is specifically of type // "golang.org/x/text/message".Printer. m, ok := extractFuncs[path.Base(meth.Recv().String())] if !ok { return true } // argn is the index of the format string. argn, ok := m[meth.Obj().Name()] if !ok || argn >= len(call.Args) { return true } // Skip calls with non-constant format string. fmtstr := info.Types[call.Args[argn]].Value if fmtstr == nil || fmtstr.Kind() != constant.String { return true } posn := conf.Fset.Position(call.Lparen) filepos := fmt.Sprintf("%s:%d:%d", filepath.Base(posn.Filename), posn.Line, posn.Column) // TODO: identify the type of the format argument. If it is not // a string, multiple keys may be defined. var key []string // TODO: replace substitutions (%v) with a translator friendly // notation. For instance: // "%d files remaining" -> "{numFiles} files remaining", or // "%d files remaining" -> "{arg1} files remaining" // Alternatively, this could be done at a later stage. msg := constant.StringVal(fmtstr) // Construct a Translation unit. c := Translation{ Key: key, Position: filepath.Join(info.Pkg.Path(), filepos), Original: Text{Msg: msg}, ExtractedComment: getComment(call.Args[0]), // TODO(fix): this doesn't get the before comment. // Comment: getComment(call), } for i, arg := range call.Args[argn+1:] { var val string if v := info.Types[arg].Value; v != nil { val = v.ExactString() } posn := conf.Fset.Position(arg.Pos()) filepos := fmt.Sprintf("%s:%d:%d", filepath.Base(posn.Filename), posn.Line, posn.Column) c.Args = append(c.Args, Argument{ ID: i + 1, Type: info.Types[arg].Type.String(), UnderlyingType: info.Types[arg].Type.Underlying().String(), Expr: print(arg), Value: val, Comment: getComment(arg), Position: filepath.Join(info.Pkg.Path(), filepos), // TODO report whether it implements // interfaces plural.Interface, // gender.Interface. }) } translations = append(translations, c) return true }) } } data, err := json.MarshalIndent(translations, "", " ") if err != nil { return err } for _, tag := range getLangs() { // TODO: merge with existing files, don't overwrite. os.MkdirAll(*dir, 0744) file := filepath.Join(*dir, fmt.Sprintf("gotext_%v.out.json", tag)) if err := ioutil.WriteFile(file, data, 0744); err != nil { return fmt.Errorf("could not create file: %v", err) } } return nil } // extractFuncs indicates the types and methods for which to extract strings, // and which argument to extract. // TODO: use the types in conf.Import("golang.org/x/text/message") to extract // the correct instances. var extractFuncs = map[string]map[string]int{ // TODO: Printer -> *golang.org/x/text/message.Printer "message.Printer": { "Printf": 0, "Sprintf": 0, "Fprintf": 1, }, }