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 }