gopkg.in/ubuntu-core/snappy.v0@v0.0.0-20210902073436-25a8614f10a6/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 // keep escaped ", replace inner " with \", replace \n with \\n 127 rep := strings.NewReplacer(`\"`, `\"`, `"`, `\"`, "\n", "\\n") 128 s = rep.Replace(s) 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 readContent(fname string) (content []byte, err error) { 174 // If no search directories have been specified or we have an 175 // absolute path, just try to read the contents directly. 176 if len(opts.Directories) == 0 || filepath.IsAbs(fname) { 177 return ioutil.ReadFile(fname) 178 } 179 180 // Otherwise, search for the file in each of the configured 181 // directories. 182 for _, dir := range opts.Directories { 183 content, err = ioutil.ReadFile(filepath.Join(dir, fname)) 184 if !os.IsNotExist(err) { 185 break 186 } 187 } 188 return content, err 189 } 190 191 func processSingleGoSource(fset *token.FileSet, fname string) error { 192 fnameContent, err := readContent(fname) 193 if err != nil { 194 return err 195 } 196 197 // Create the AST by parsing src. 198 f, err := parser.ParseFile(fset, fname, fnameContent, parser.ParseComments) 199 if err != nil { 200 return err 201 } 202 203 ast.Inspect(f, func(n ast.Node) bool { 204 return inspectNodeForTranslations(fset, f, n) 205 }) 206 207 return nil 208 } 209 210 var formatTime = func() string { 211 return time.Now().Format("2006-01-02 15:04-0700") 212 } 213 214 // mustFprintf will write the given format string to the given 215 // writer. Any error will make it panic. 216 func mustFprintf(w io.Writer, format string, a ...interface{}) { 217 _, err := fmt.Fprintf(w, format, a...) 218 if err != nil { 219 panic(fmt.Sprintf("cannot write output: %v", err)) 220 } 221 } 222 223 func writePotFile(out io.Writer) { 224 225 header := fmt.Sprintf(`# SOME DESCRIPTIVE TITLE. 226 # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 227 # This file is distributed under the same license as the PACKAGE package. 228 # FIRST AUTHOR <EMAIL@ADDRESS>, YEAR. 229 # 230 #, fuzzy 231 msgid "" 232 msgstr "Project-Id-Version: %s\n" 233 "Report-Msgid-Bugs-To: %s\n" 234 "POT-Creation-Date: %s\n" 235 "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 236 "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" 237 "Language-Team: LANGUAGE <LL@li.org>\n" 238 "Language: \n" 239 "MIME-Version: 1.0\n" 240 "Content-Type: text/plain; charset=CHARSET\n" 241 "Content-Transfer-Encoding: 8bit\n" 242 243 `, opts.PackageName, opts.MsgIDBugsAddress, formatTime()) 244 mustFprintf(out, "%s", header) 245 246 // yes, this is the way to do it in go 247 sortedKeys := []string{} 248 for k := range msgIDs { 249 sortedKeys = append(sortedKeys, k) 250 } 251 if opts.SortOutput { 252 sort.Strings(sortedKeys) 253 } 254 255 // FIXME: use template here? 256 for _, k := range sortedKeys { 257 msgidList := msgIDs[k] 258 for _, msgid := range msgidList { 259 if opts.AddComments || opts.AddCommentsTag != "" { 260 mustFprintf(out, "%s", msgid.comment) 261 } 262 } 263 if !opts.NoLocation { 264 mustFprintf(out, "#:") 265 for _, msgid := range msgidList { 266 mustFprintf(out, " %s:%d", msgid.fname, msgid.line) 267 } 268 mustFprintf(out, "\n") 269 } 270 msgid := msgidList[0] 271 if msgid.formatHint != "" { 272 mustFprintf(out, "#, %s\n", msgid.formatHint) 273 } 274 var formatOutput = func(in string) string { 275 // split string with \n into multiple lines 276 // to make the output nicer 277 out := strings.Replace(in, "\\n", "\\n\"\n \"", -1) 278 // cleanup too aggressive splitting (empty "" lines) 279 return strings.TrimSuffix(out, "\"\n \"") 280 } 281 mustFprintf(out, "msgid \"%v\"\n", formatOutput(k)) 282 if msgid.msgidPlural != "" { 283 mustFprintf(out, "msgid_plural \"%v\"\n", formatOutput(msgid.msgidPlural)) 284 mustFprintf(out, "msgstr[0] \"\"\n") 285 mustFprintf(out, "msgstr[1] \"\"\n") 286 } else { 287 mustFprintf(out, "msgstr \"\"\n") 288 } 289 mustFprintf(out, "\n") 290 } 291 292 } 293 294 // FIXME: this must be setable via go-flags 295 var opts struct { 296 FilesFrom string `short:"f" long:"files-from" description:"get list of input files from FILE"` 297 298 Directories []string `short:"D" long:"directory" description:"add DIRECTORY to list for input files search"` 299 300 Output string `short:"o" long:"output" description:"output to specified file"` 301 302 AddComments bool `short:"c" long:"add-comments" description:"place all comment blocks preceding keyword lines in output file"` 303 304 AddCommentsTag string `long:"add-comments-tag" description:"place comment blocks starting with TAG and prceding keyword lines in output file"` 305 306 SortOutput bool `short:"s" long:"sort-output" description:"generate sorted output"` 307 308 NoLocation bool `long:"no-location" description:"do not write '#: filename:line' lines"` 309 310 MsgIDBugsAddress string `long:"msgid-bugs-address" default:"EMAIL" description:"set report address for msgid bugs"` 311 312 PackageName string `long:"package-name" description:"set package name in output"` 313 314 Keyword string `short:"k" long:"keyword" default:"gettext.Gettext" description:"look for WORD as the keyword for singular strings"` 315 KeywordPlural string `long:"keyword-plural" default:"gettext.NGettext" description:"look for WORD as the keyword for plural strings"` 316 } 317 318 func main() { 319 // parse args 320 args, err := flags.ParseArgs(&opts, os.Args) 321 if err != nil { 322 log.Fatalf("ParseArgs failed %s", err) 323 } 324 325 var files []string 326 if opts.FilesFrom != "" { 327 content, err := ioutil.ReadFile(opts.FilesFrom) 328 if err != nil { 329 log.Fatalf("cannot read file %v: %v", opts.FilesFrom, err) 330 } 331 content = bytes.TrimSpace(content) 332 files = strings.Split(string(content), "\n") 333 } else { 334 files = args[1:] 335 } 336 if err := processFiles(files); err != nil { 337 log.Fatalf("processFiles failed with: %s", err) 338 } 339 340 out := os.Stdout 341 if opts.Output != "" { 342 var err error 343 out, err = os.Create(opts.Output) 344 if err != nil { 345 log.Fatalf("failed to create %s: %s", opts.Output, err) 346 } 347 } 348 writePotFile(out) 349 }