From 7c1823dacdd5c35ce0107715003ad0786e55a65f Mon Sep 17 00:00:00 2001 From: decentral1se Date: Sat, 20 Sep 2025 23:05:42 +0200 Subject: [PATCH] feat: fork --- .gitignore | 1 + README.md | 4 + go.mod | 17 ++ go.sum | 23 +++ main.go | 348 +++++++++++++++++++++++++++++++++ main_test.go | 537 +++++++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 930 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 go.mod create mode 100644 go.sum create mode 100644 main.go create mode 100644 main_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..41212ee --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +/xgettext-go diff --git a/README.md b/README.md new file mode 100644 index 0000000..2f22650 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# xgettext-go + +* [Forked from here](https://github.com/canonical/snapd/tree/master/i18n/xgettext-go) +* [The reason why](https://git.coopcloud.tech/toolshed/abra/issues/647) diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..6549b65 --- /dev/null +++ b/go.mod @@ -0,0 +1,17 @@ +module git.coopcloud.tech/toolshed/xgettext-go + +go 1.25.1 + +require ( + github.com/jessevdk/go-flags v1.6.1 + github.com/snapcore/snapd v0.0.0-20250919210715-9cb4b26eed4e + gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c +) + +require ( + github.com/godbus/dbus/v5 v5.1.0 // indirect + github.com/kr/pretty v0.2.2-0.20200810074440-814ac30b4b18 // indirect + github.com/kr/text v0.1.0 // indirect + golang.org/x/sys v0.21.0 // indirect + gopkg.in/yaml.v2 v2.4.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..81a0e73 --- /dev/null +++ b/go.sum @@ -0,0 +1,23 @@ +github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= +github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.2.2-0.20200810074440-814ac30b4b18 h1:fth7xdJYakAjo/XH38edyXuBEqYGJ8Me0RPolN1ZiQE= +github.com/kr/pretty v0.2.2-0.20200810074440-814ac30b4b18/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/snapcore/snapd v0.0.0-20250919210715-9cb4b26eed4e h1:GiuazaPktUEeR6sSi+38LE366fOnGs2reqzcmB8sr2s= +github.com/snapcore/snapd v0.0.0-20250919210715-9cb4b26eed4e/go.mod h1:z8KQXflnXuM2z1Xi7uJ9PJ+QEE3kxEapNK8jNN71fXI= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637 h1:yiW+nvdHb9LVqSHQBXfZCieqV4fzYhNBql77zY0ykqs= +gopkg.in/tomb.v2 v2.0.0-20161208151619-d5d1b5820637/go.mod h1:BHsqpu/nsuzkT5BpiH1EMZPLyqSMM8JbIavyFACoFNk= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/main.go b/main.go new file mode 100644 index 0000000..09467ca --- /dev/null +++ b/main.go @@ -0,0 +1,348 @@ +package main + +import ( + "bytes" + "fmt" + "go/ast" + "go/parser" + "go/token" + "io" + "log" + "os" + "path/filepath" + "sort" + "strings" + "time" + + "github.com/jessevdk/go-flags" +) + +type msgID struct { + msgidPlural string + comment string + fname string + line int + formatHint string +} + +var msgIDs map[string][]msgID + +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(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", opts.AddCommentsTag) + if !strings.HasPrefix(formatedComment, needle) { + formatedComment = "" + } + + return formatedComment +} + +func constructValue(val any) 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:] + return left + right + default: + panic(fmt.Sprintf("unknown type: %v", val)) + } +} + +func inspectNodeForTranslations(fset *token.FileSet, f *ast.File, n ast.Node) bool { + // FIXME: this assume we always have a "gettext.Gettext" style keyword + l := strings.Split(opts.Keyword, ".") + gettextSelector := l[0] + gettextFuncName := l[1] + + l = strings.Split(opts.KeywordPlural, ".") + gettextSelectorPlural := l[0] + gettextFuncNamePlural := l[1] + + switch x := n.(type) { + case *ast.CallExpr: + if sel, ok := x.Fun.(*ast.SelectorExpr); ok { + i18nStr := "" + i18nStrPlural := "" + 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]) + } + + formatI18nStr := func(s string) string { + if s == "" { + return "" + } + // the "`" is special + if s[0] == '`' { + // keep escaped ", replace inner " with \", replace \n with \\n + rep := strings.NewReplacer(`\"`, `\"`, `"`, `\"`, "\n", "\\n") + s = rep.Replace(s) + } + // strip leading and trailing " (or `) + s = s[1 : len(s)-1] + return s + } + + // FIXME: too simplistic(?), no %% is considered + formatHint := "" + if strings.Contains(i18nStr, "%") || strings.Contains(i18nStrPlural, "%") { + // well, not quite correct but close enough + formatHint = "c-format" + } + + if i18nStr != "" { + msgidStr := formatI18nStr(i18nStr) + posCall := fset.Position(n.Pos()) + msgIDs[msgidStr] = append(msgIDs[msgidStr], msgID{ + formatHint: formatHint, + msgidPlural: formatI18nStr(i18nStrPlural), + fname: posCall.Filename, + line: posCall.Line, + comment: findCommentsForTranslation(fset, f, posCall), + }) + } + } + } + + return true +} + +func processFiles(args []string) error { + // go over the input files + msgIDs = make(map[string][]msgID) + + fset := token.NewFileSet() + for _, fname := range args { + if err := processSingleGoSource(fset, fname); err != nil { + return err + } + } + + return nil +} + +func readContent(fname string) (content []byte, err error) { + // If no search directories have been specified or we have an + // absolute path, just try to read the contents directly. + if len(opts.Directories) == 0 || filepath.IsAbs(fname) { + return os.ReadFile(fname) + } + + // Otherwise, search for the file in each of the configured + // directories. + for _, dir := range opts.Directories { + content, err = os.ReadFile(filepath.Join(dir, fname)) + if !os.IsNotExist(err) { + break + } + } + return content, err +} + +func processSingleGoSource(fset *token.FileSet, fname string) error { + fnameContent, err := readContent(fname) + if err != nil { + return err + } + + // Create the AST by parsing src. + f, err := parser.ParseFile(fset, fname, fnameContent, parser.ParseComments) + if err != nil { + return err + } + + ast.Inspect(f, func(n ast.Node) bool { + return inspectNodeForTranslations(fset, f, n) + }) + + return nil +} + +var formatTime = func() string { + return time.Now().Format("2006-01-02 15:04-0700") +} + +// mustFprintf will write the given format string to the given +// writer. Any error will make it panic. +func mustFprintf(w io.Writer, format string, a ...any) { + _, err := fmt.Fprintf(w, format, a...) + if err != nil { + panic(fmt.Sprintf("cannot write output: %v", err)) + } +} + +func writePotFile(out io.Writer) { + + header := fmt.Sprintf(`# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "Project-Id-Version: %s\n" + "Report-Msgid-Bugs-To: %s\n" + "POT-Creation-Date: %s\n" + "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + "Last-Translator: FULL NAME \n" + "Language-Team: LANGUAGE \n" + "Language: \n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=CHARSET\n" + "Content-Transfer-Encoding: 8bit\n" + +`, opts.PackageName, opts.MsgIDBugsAddress, formatTime()) + mustFprintf(out, "%s", header) + + // yes, this is the way to do it in go + sortedKeys := []string{} + for k := range msgIDs { + sortedKeys = append(sortedKeys, k) + } + if opts.SortOutput { + sort.Strings(sortedKeys) + } + + // FIXME: use template here? + for _, k := range sortedKeys { + msgidList := msgIDs[k] + for _, msgid := range msgidList { + if opts.AddComments || opts.AddCommentsTag != "" { + mustFprintf(out, "%s", msgid.comment) + } + } + if !opts.NoLocation { + mustFprintf(out, "#:") + for _, msgid := range msgidList { + mustFprintf(out, " %s:%d", msgid.fname, msgid.line) + } + mustFprintf(out, "\n") + } + msgid := msgidList[0] + if msgid.formatHint != "" { + mustFprintf(out, "#, %s\n", msgid.formatHint) + } + var formatOutput = func(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) + return strings.TrimSuffix(out, "\"\n \"") + } + mustFprintf(out, "msgid \"%v\"\n", formatOutput(k)) + if msgid.msgidPlural != "" { + mustFprintf(out, "msgid_plural \"%v\"\n", formatOutput(msgid.msgidPlural)) + mustFprintf(out, "msgstr[0] \"\"\n") + mustFprintf(out, "msgstr[1] \"\"\n") + } else { + mustFprintf(out, "msgstr \"\"\n") + } + mustFprintf(out, "\n") + } + +} + +// FIXME: this must be setable via go-flags +var opts struct { + FilesFrom string `short:"f" long:"files-from" description:"get list of input files from FILE"` + + Directories []string `short:"D" long:"directory" description:"add DIRECTORY to list for input files search"` + + Output string `short:"o" long:"output" description:"output to specified file"` + + AddComments bool `short:"c" long:"add-comments" description:"place all comment blocks preceding keyword lines in output file"` + + AddCommentsTag string `long:"add-comments-tag" description:"place comment blocks starting with TAG and prceding keyword lines in output file"` + + SortOutput bool `short:"s" long:"sort-output" description:"generate sorted output"` + + NoLocation bool `long:"no-location" description:"do not write '#: filename:line' lines"` + + MsgIDBugsAddress string `long:"msgid-bugs-address" default:"EMAIL" description:"set report address for msgid bugs"` + + PackageName string `long:"package-name" description:"set package name in output"` + + Keyword string `short:"k" long:"keyword" default:"gettext.Gettext" description:"look for WORD as the keyword for singular strings"` + KeywordPlural string `long:"keyword-plural" default:"gettext.NGettext" description:"look for WORD as the keyword for plural strings"` +} + +func main() { + // parse args + args, err := flags.ParseArgs(&opts, os.Args) + if err != nil { + log.Fatalf("ParseArgs failed %s", err) + } + + var files []string + if opts.FilesFrom != "" { + content, err := os.ReadFile(opts.FilesFrom) + if err != nil { + log.Fatalf("cannot read file %v: %v", opts.FilesFrom, err) + } + content = bytes.TrimSpace(content) + files = strings.Split(string(content), "\n") + } else { + files = args[1:] + } + if err := processFiles(files); err != nil { + log.Fatalf("processFiles failed with: %s", err) + } + + out := os.Stdout + if opts.Output != "" { + var err error + out, err = os.Create(opts.Output) + if err != nil { + log.Fatalf("failed to create %s: %s", opts.Output, err) + } + } + writePotFile(out) +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..bb5a1ba --- /dev/null +++ b/main_test.go @@ -0,0 +1,537 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "bytes" + "fmt" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func TestT(t *testing.T) { TestingT(t) } + +type xgettextTestSuite struct { +} + +var _ = Suite(&xgettextTestSuite{}) + +// test helper +func makeGoSourceFile(c *C, content []byte) string { + fname := filepath.Join(c.MkDir(), "foo.go") + err := os.WriteFile(fname, []byte(content), 0644) + c.Assert(err, IsNil) + + return fname +} + +func (s *xgettextTestSuite) SetUpTest(c *C) { + // our test defaults + opts.NoLocation = false + opts.AddCommentsTag = "TRANSLATORS:" + opts.Keyword = "i18n.G" + opts.KeywordPlural = "i18n.NG" + opts.SortOutput = true + opts.PackageName = "snappy" + opts.MsgIDBugsAddress = "snappy-devel@lists.ubuntu.com" + + // mock time + formatTime = func() string { + return "2015-06-30 14:48+0200" + } +} + +func (s *xgettextTestSuite) TestFormatComment(c *C) { + var tests = []struct { + in string + out string + }{ + {in: "// foo ", out: "#. foo\n"}, + {in: "/* foo */", out: "#. foo\n"}, + {in: "/* foo\n */", out: "#. foo\n"}, + {in: "/* foo\nbar */", out: "#. foo\n#. bar\n"}, + } + + for _, test := range tests { + c.Assert(formatComment(test.in), Equals, test.out) + } +} + +func (s *xgettextTestSuite) TestProcessFilesSimple(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + // TRANSLATORS: foo comment + i18n.G("foo") +} +`)) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + c.Assert(msgIDs, DeepEquals, map[string][]msgID{ + "foo": { + { + comment: "#. TRANSLATORS: foo comment\n", + fname: fname, + line: 5, + }, + }, + }) +} + +func (s *xgettextTestSuite) TestProcessFilesMultiple(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + // TRANSLATORS: foo comment + i18n.G("foo") + + // TRANSLATORS: bar comment + i18n.G("foo") +} +`)) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + c.Assert(msgIDs, DeepEquals, map[string][]msgID{ + "foo": { + { + comment: "#. TRANSLATORS: foo comment\n", + fname: fname, + line: 5, + }, + { + comment: "#. TRANSLATORS: bar comment\n", + fname: fname, + line: 8, + }, + }, + }) +} + +const header = `# SOME DESCRIPTIVE TITLE. +# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER +# This file is distributed under the same license as the PACKAGE package. +# FIRST AUTHOR , YEAR. +# +#, fuzzy +msgid "" +msgstr "Project-Id-Version: snappy\n" + "Report-Msgid-Bugs-To: snappy-devel@lists.ubuntu.com\n" + "POT-Creation-Date: 2015-06-30 14:48+0200\n" + "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" + "Last-Translator: FULL NAME \n" + "Language-Team: LANGUAGE \n" + "Language: \n" + "MIME-Version: 1.0\n" + "Content-Type: text/plain; charset=CHARSET\n" + "Content-Transfer-Encoding: 8bit\n" +` + +func (s *xgettextTestSuite) TestWriteOutputSimple(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + comment: "#. foo\n", + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#. foo +#: fname:2 +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputMultiple(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + comment: "#. comment1\n", + }, + { + fname: "fname", + line: 4, + comment: "#. comment2\n", + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#. comment1 +#. comment2 +#: fname:2 fname:4 +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputNoComment(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: fname:2 +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputNoLocation(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + }, + }, + } + + opts.NoLocation = true + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputFormatHint(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + fname: "fname", + line: 2, + formatHint: "c-format", + }, + }, + } + + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: fname:2 +#, c-format +msgid "foo" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputPlural(c *C) { + msgIDs = map[string][]msgID{ + "foo": { + { + msgidPlural: "plural", + fname: "fname", + line: 2, + }, + }, + } + + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: fname:2 +msgid "foo" +msgid_plural "plural" +msgstr[0] "" +msgstr[1] "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputSorted(c *C) { + msgIDs = map[string][]msgID{ + "aaa": { + { + fname: "fname", + line: 2, + }, + }, + "zzz": { + { + fname: "fname", + line: 2, + }, + }, + } + + opts.SortOutput = true + // we need to run this a bunch of times as the ordering might + // be right by pure chance + for i := 0; i < 10; i++ { + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: fname:2 +msgid "aaa" +msgstr "" + +#: fname:2 +msgid "zzz" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) + } +} + +func (s *xgettextTestSuite) TestIntegration(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + // TRANSLATORS: foo comment + // with multiple lines + i18n.G("foo") + + // this comment has no translators tag + i18n.G("abc") + + // TRANSLATORS: plural + i18n.NG("singular", "plural", 99) + + i18n.G("zz %s") +} +`)) + + // a real integration test :) + outName := filepath.Join(c.MkDir(), "snappy.pot") + os.Args = []string{"test-binary", + "--output", outName, + "--keyword", "i18n.G", + "--keyword-plural", "i18n.NG", + "--msgid-bugs-address", "snappy-devel@lists.ubuntu.com", + "--package-name", "snappy", + fname, + } + main() + + // verify its what we expect + c.Assert(outName, testutil.FileEquals, fmt.Sprintf(`%s +#: %[2]s:9 +msgid "abc" +msgstr "" + +#. TRANSLATORS: foo comment +#. with multiple lines +#: %[2]s:6 +msgid "foo" +msgstr "" + +#. TRANSLATORS: plural +#: %[2]s:12 +msgid "singular" +msgid_plural "plural" +msgstr[0] "" +msgstr[1] "" + +#: %[2]s:14 +#, c-format +msgid "zz %%s" +msgstr "" + +`, header, fname)) +} + +func (s *xgettextTestSuite) TestProcessFilesConcat(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + // TRANSLATORS: foo comment + i18n.G("foo\n" + "bar\n" + "baz") +} +`)) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + c.Assert(msgIDs, DeepEquals, map[string][]msgID{ + "foo\\nbar\\nbaz": { + { + comment: "#. TRANSLATORS: foo comment\n", + fname: fname, + line: 5, + }, + }, + }) +} + +func (s *xgettextTestSuite) TestProcessFilesWithQuote(c *C) { + fname := makeGoSourceFile(c, []byte(fmt.Sprintf(`package main + +func main() { + i18n.G(%[1]s foo "bar"%[1]s) +} +`, "`"))) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: %[2]s:4 +msgid " foo \"bar\"" +msgstr "" + +`, header, fname) + c.Check(out.String(), Equals, expected) + +} + +func (s *xgettextTestSuite) TestWriteOutputMultilines(c *C) { + msgIDs = map[string][]msgID{ + "foo\\nbar\\nbaz": { + { + fname: "fname", + line: 2, + comment: "#. foo\n", + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + expected := fmt.Sprintf(`%s +#. foo +#: fname:2 +msgid "foo\n" + "bar\n" + "baz" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestWriteOutputTidy(c *C) { + msgIDs = map[string][]msgID{ + "foo\\nbar\\nbaz": { + { + fname: "fname", + line: 2, + }, + }, + "zzz\\n": { + { + fname: "fname", + line: 4, + }, + }, + } + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + expected := fmt.Sprintf(`%s +#: fname:2 +msgid "foo\n" + "bar\n" + "baz" +msgstr "" + +#: fname:4 +msgid "zzz\n" +msgstr "" + +`, header) + c.Assert(out.String(), Equals, expected) +} + +func (s *xgettextTestSuite) TestProcessFilesWithDoubleQuote(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + i18n.G("foo \"bar\"") +} +`)) + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: %[2]s:4 +msgid "foo \"bar\"" +msgstr "" + +`, header, fname) + c.Check(out.String(), Equals, expected) + +} + +func (s *xgettextTestSuite) TestDontEscapeAlreadyEscapedQuoteInBacktick(c *C) { + fname := makeGoSourceFile(c, []byte(`package main + +func main() { + i18n.G(`+"`"+`Some text: "{\"key\":\"value\"}"`+"`"+`) +} +`)) + + err := processFiles([]string{fname}) + c.Assert(err, IsNil) + + out := bytes.NewBuffer([]byte("")) + writePotFile(out) + + expected := fmt.Sprintf(`%s +#: %[2]s:4 +msgid "Some text: \"{\"key\":\"value\"}\"" +msgstr "" + +`, header, fname) + c.Check(out.String(), Equals, expected) +}