package gotext import ( "fmt" "flag" "go/ast" "go/parser" "go/token" "io" "io/ioutil" "os" "sort" "strings" "time" "unsafe" ) /* #define _XOPEN_SOURCE 700 #include #include #include */ import "C" const ( LC_ALL = uint(C.LC_ALL) LC_COLLATE = uint(C.LC_COLLATE) LC_CTYPE = uint(C.LC_CTYPE) LC_MESSAGES = uint(C.LC_MESSAGES) LC_MONETARY = uint(C.LC_MONETARY) LC_NUMERIC = uint(C.LC_NUMERIC) LC_TIME = uint(C.LC_TIME) ) var ( formatTime = func() string { if false { // FIXME return time.Now().Format("2006-01-02 15:04-0700") } return "2015-06-30T14:48:00-03:00" } ) type translatableErrorT struct{ err string } type msgIDT struct{ msgidPlural string comment string fname string line int formatHint string } type argsT struct{ allArgs []string subArgs []string commentTag string keyword string keywordPlural string } type envT struct{ args argsT in io.Reader out io.Writer err io.Writer } func (e *translatableErrorT) Error() string { return Gettext(e.err) } func Error(err string) error { return &translatableErrorT{ err: err, } } func formatComment(com string) string { out := "" for _, rawline := range strings.Split(com, "\n") { line := rawline line = strings.TrimPrefix(line, "//") line = strings.TrimPrefix(line, "/*") line = strings.TrimSuffix(line, "*/") line = strings.TrimSpace(line) if line != "" { out += fmt.Sprintf("#. %s\n", line) } } return out } func findCommentsForTranslation( args argsT, fset *token.FileSet, f *ast.File, posCall token.Position, ) string { com := "" for _, cg := range f.Comments { // search for all comments in the previous line for i := len(cg.List) - 1; i >= 0; i-- { c := cg.List[i] posComment := fset.Position(c.End()) //println(posCall.Line, posComment.Line, c.Text) if posCall.Line == posComment.Line+1 { posCall = posComment com = fmt.Sprintf("%s\n%s", c.Text, com) } } } // only return if we have a matching prefix formatedComment := formatComment(com) needle := fmt.Sprintf("#. %s", args.commentTag) if !strings.HasPrefix(formatedComment, needle) { formatedComment = "" } return formatedComment } func constructValue(val interface{}) string { switch val.(type) { case *ast.BasicLit: return val.(*ast.BasicLit).Value // this happens for constructs like: // gettext.Gettext("foo" + "bar") case *ast.BinaryExpr: // we only support string concat if val.(*ast.BinaryExpr).Op != token.ADD { return "" } left := constructValue(val.(*ast.BinaryExpr).X) // strip right " (or `) left = left[0 : len(left)-1] right := constructValue(val.(*ast.BinaryExpr).Y) // strip left " (or `) right = right[1:len(right)] return left + right default: panic(fmt.Sprintf("unknown type: %v", val)) } } func inspectNodeForTranslations( args argsT, msgIDs map[string][]msgIDT, fset *token.FileSet, f *ast.File, n ast.Node, ) bool { // FIXME: this assume we always have a "gettext.Gettext" style keyword gettextSelector := "" gettextFuncName := "" l := strings.Split(args.keyword, ".") if len(l) > 1 { gettextSelector = l[0] gettextFuncName = l[1] } else { gettextFuncName = l[0] } gettextSelectorPlural := "" gettextFuncNamePlural := "" l = strings.Split(args.keywordPlural, ".") if len(l) > 1 { gettextSelectorPlural = l[0] gettextFuncNamePlural = l[1] } else { gettextFuncNamePlural = l[0] } switch x := n.(type) { case *ast.CallExpr: i18nStr := "" i18nStrPlural := "" //if sel, ok := x.Fun.(*ast.Ident); ok { //} switch sel := x.Fun.(type) { case *ast.Ident: if sel.Name == gettextFuncNamePlural { i18nStr = x.Args[0].(*ast.BasicLit).Value i18nStrPlural = x.Args[1].(*ast.BasicLit).Value } if sel.Name == gettextFuncName { i18nStr = constructValue(x.Args[0]) } case *ast.SelectorExpr: if sel.Sel.Name == gettextFuncNamePlural && sel.X.(*ast.Ident).Name == gettextSelectorPlural { i18nStr = x.Args[0].(*ast.BasicLit).Value i18nStrPlural = x.Args[1].(*ast.BasicLit).Value } if sel.Sel.Name == gettextFuncName && sel.X.(*ast.Ident).Name == gettextSelector { i18nStr = constructValue(x.Args[0]) } } if i18nStr == "" { break } // FIXME: too simplistic(?), no %% is considered formatHint := "" if strings.Contains(i18nStr, "%") || strings.Contains(i18nStrPlural, "%") { // well, not quite correct but close enough formatHint = "c-format" } msgidStr := formatI18nStr(i18nStr) posCall := fset.Position(n.Pos()) msgIDs[msgidStr] = append(msgIDs[msgidStr], msgIDT{ formatHint: formatHint, msgidPlural: formatI18nStr(i18nStrPlural), fname: posCall.Filename, line: posCall.Line, comment: findCommentsForTranslation( args, fset, f, posCall, ), }) } return true } func formatI18nStr(s string) string { if s == "" { return "" } // the "`" is special if s[0] == '`' { // replace inner " with \" s = strings.Replace(s, "\"", "\\\"", -1) // replace \n with \\n s = strings.Replace(s, "\n", "\\n", -1) } // strip leading and trailing " (or `) s = s[1 : len(s)-1] return s } func processFiles(args argsT) (map[string][]msgIDT, error) { // go over the input files msgIDs := map[string][]msgIDT{} fset := token.NewFileSet() for _, fname := range args.subArgs { err := processSingleGoSource(args, msgIDs, fset, fname) if err != nil { return nil, err } } return msgIDs, nil } func processSingleGoSource( args argsT, msgIDs map[string][]msgIDT, fset *token.FileSet, fname string, ) error { fnameContent, err := ioutil.ReadFile(fname) if err != nil { panic(err) } // Create the AST by parsing src. f, err := parser.ParseFile(fset, fname, fnameContent, parser.ParseComments) if err != nil { panic(err) } ast.Inspect(f, func(n ast.Node) bool { return inspectNodeForTranslations(args, msgIDs, fset, f, n) }) return nil } func formatOutput(in string) string { // split string with \n into multiple lines // to make the output nicer out := strings.Replace(in, "\\n", "\\n\"\n\"", -1) // cleanup too aggressive splitting (empty "" lines) out = strings.TrimSuffix(out, "\"\n\"") if strings.Count(out, "\n") == 0 { return out } else { return "\"\n\"" + out } } func writePotFile(out io.Writer, msgIDs map[string][]msgIDT) { const header = `msgid "" msgstr "" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" ` fmt.Fprintf(out, "%s", header) // FIXME: sort by location, not by key // yes, this is the way to do it in go sortedKeys := []string{} for k := range msgIDs { sortedKeys = append(sortedKeys, k) } sort.Strings(sortedKeys) // FIXME: use template here? for _, k := range sortedKeys { msgidList := msgIDs[k] for _, msgid := range msgidList { fmt.Fprintf(out, "%s", msgid.comment) } fmt.Fprintf(out, "#:") for _, msgid := range msgidList { fmt.Fprintf(out, " %s:%d", msgid.fname, msgid.line) } fmt.Fprintf(out, "\n") msgid := msgidList[0] if msgid.formatHint != "" { fmt.Fprintf(out, "#, %s\n", msgid.formatHint) } fmt.Fprintf(out, "msgid \"%v\"\n", formatOutput(k)) if msgid.msgidPlural != "" { fmt.Fprintf(out, "msgid_plural \"%v\"\n", formatOutput(msgid.msgidPlural)) fmt.Fprintf(out, "msgstr[0] \"\"\n") fmt.Fprintf(out, "msgstr[1] \"\"\n") } else { fmt.Fprintf(out, "msgstr \"\"\n") } fmt.Fprintf(out, "\n") } } func usage(argv0 string, w io.Writer) { fmt.Fprintf( w, "Usage: %s\n", argv0, ) } func getopt(allArgs []string, w io.Writer) (argsT, int) { argv0 := allArgs[0] argv := allArgs[1:] fs := flag.NewFlagSet("", flag.ContinueOnError) fs.Usage = func() {} fs.SetOutput(w) commentTag := fs.String( "A", "TRANSLATORS", "place comment blocks starting with TAG and prceding keyword lines in output file", ) keyword := fs.String( "k", "gt.Gettext", "look for WORD as the keyword for singular strings", ) keywordPlural := fs.String( "K", "gt.NGettext", "look for WORD as the keyword for plural strings", ) if fs.Parse(argv) != nil { usage(argv0, w) return argsT{}, 2 } subArgs := fs.Args() return argsT{ allArgs: allArgs, subArgs: subArgs, commentTag: *commentTag, keyword: *keyword, keywordPlural: *keywordPlural, }, 0 } func run(env envT) int { msgIDs, err := processFiles(env.args) if err != nil { fmt.Fprintln(env.err, err) return 1 } writePotFile(os.Stdout, msgIDs) return 0 } // SetLocale sets the program's current locale. func SetLocale(category uint, locale string) string { clocale := C.CString(locale) defer C.free(unsafe.Pointer(clocale)) return C.GoString(C.setlocale(C.int(category), clocale)) } // BindTextdomain sets the directory containing message catalogs. func BindTextdomain(domainname string, dirname string) string { cdirname := C.CString(dirname) defer C.free(unsafe.Pointer(cdirname)) cdomainname := C.CString(domainname) defer C.free(unsafe.Pointer(cdomainname)) return C.GoString(C.bindtextdomain(cdomainname, cdirname)) } // BindTextdomainCodeset sets the output codeset for message catalogs on the // given domainname. func BindTextdomainCodeset(domainname string, codeset string) string { cdomainname := C.CString(domainname) defer C.free(unsafe.Pointer(cdomainname)) ccodeset := C.CString(codeset) defer C.free(unsafe.Pointer(ccodeset)) return C.GoString(C.bind_textdomain_codeset(cdomainname, ccodeset)) } // Textdomain sets or retrieves the current message domain. func Textdomain(domainname string) string { cdomainname := C.CString(domainname) defer C.free(unsafe.Pointer(cdomainname)) return C.GoString(C.textdomain(cdomainname)) } // Gettext attempts to translate a text string into the user's system language, // by looking up the translation in a message catalog. func Gettext(msgid string) string { cmsgid := C.CString(msgid) defer C.free(unsafe.Pointer(cmsgid)) return C.GoString(C.gettext(cmsgid)) } // DGettext is like Gettext(), but looks up the message in the specified // domain. func DGettext(domain string, msgid string) string { cdomain := cDomainName(domain) defer C.free(unsafe.Pointer(cdomain)) cmsgid := C.CString(msgid) defer C.free(unsafe.Pointer(cmsgid)) return C.GoString(C.dgettext(cdomain, cmsgid)) } // DCGettext is like Gettext(), but looks up the message in the specified // domain and category. func DCGettext(domain string, msgid string, category uint) string { cdomain := cDomainName(domain) defer C.free(unsafe.Pointer(cdomain)) cmsgid := C.CString(msgid) defer C.free(unsafe.Pointer(cmsgid)) return C.GoString(C.dcgettext(cdomain, cmsgid, C.int(category))) } // NGettext attempts to translate a text string into the user's system // language, by looking up the appropriate plural form of the translation in a // message catalog. func NGettext(msgid string, msgidPlural string, n uint64) string { cmsgid := C.CString(msgid) defer C.free(unsafe.Pointer(cmsgid)) cmsgidPlural := C.CString(msgidPlural) defer C.free(unsafe.Pointer(cmsgidPlural)) return C.GoString(C.ngettext(cmsgid, cmsgidPlural, C.ulong(n))) } // Sprintf is like fmt.Sprintf() but without %!(EXTRA) errors. func Sprintf(format string, a ...interface{}) string { expects := strings.Count(format, "%") - strings.Count(format, "%%") if expects > 0 { arguments := make([]interface{}, expects) for i := 0; i < expects; i++ { if len(a) > i { arguments[i] = a[i] } } return fmt.Sprintf(format, arguments...) } return format } // DNGettext is like NGettext(), but looks up the message in the specified // domain. func DNGettext(domainname string, msgid string, msgidPlural string, n uint64) string { cdomainname := cDomainName(domainname) cmsgid := C.CString(msgid) cmsgidPlural := C.CString(msgidPlural) defer func() { C.free(unsafe.Pointer(cdomainname)) C.free(unsafe.Pointer(cmsgid)) C.free(unsafe.Pointer(cmsgidPlural)) }() return C.GoString(C.dngettext(cdomainname, cmsgid, cmsgidPlural, C.ulong(n))) } // DCNGettext is like NGettext(), but looks up the message in the specified // domain and category. func DCNGettext(domainname string, msgid string, msgidPlural string, n uint64, category uint) string { cdomainname := cDomainName(domainname) cmsgid := C.CString(msgid) cmsgidPlural := C.CString(msgidPlural) defer func() { C.free(unsafe.Pointer(cdomainname)) C.free(unsafe.Pointer(cmsgid)) C.free(unsafe.Pointer(cmsgidPlural)) }() return C.GoString(C.dcngettext(cdomainname, cmsgid, cmsgidPlural, C.ulong(n), C.int(category))) } // cDomainName returns the domain name CString that can be nil. func cDomainName(domain string) *C.char { if domain == "" { return nil } // The caller is responsible for freeing this up. return C.CString(domain) } func Init(name string, localedir string) { SetLocale(LC_ALL, ""); BindTextdomain(name, localedir); Textdomain(name) } func Main() { args, rc := getopt(os.Args, os.Stderr) if rc != 0 { os.Exit(0) } os.Exit(run(envT{ args: args, in: os.Stdin, out: os.Stdout, err: os.Stderr, })) }