github.com/bugraaydogar/snapd@v0.0.0-20210315170335-8c70bb858939/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 }