feat: keyword-ctx
Some checks failed
continuous-integration/drone/push Build is failing

See toolshed/abra#647
This commit is contained in:
2025-09-28 10:44:16 +02:00
parent 7c1823dacd
commit 49cdbdf34e
4 changed files with 235 additions and 29 deletions

13
.drone.yml Normal file
View File

@ -0,0 +1,13 @@
---
kind: pipeline
name: coopcloud.tech/xgettext-go
steps:
- name: unit tests
image: golang:1.25
commands:
- go test -v ./...
trigger:
action:
exclude:
- synchronized

View File

@ -1,4 +1,6 @@
# xgettext-go
[![Build Status](https://build.coopcloud.tech/api/badges/toolshed/xgettext-go/status.svg?ref=refs/heads/main)](https://build.coopcloud.tech/toolshed/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)

131
main.go
View File

@ -20,6 +20,7 @@ import (
type msgID struct {
msgidPlural string
comment string
context string
fname string
line int
formatHint string
@ -102,16 +103,27 @@ func inspectNodeForTranslations(fset *token.FileSet, f *ast.File, n ast.Node) bo
gettextSelectorPlural := l[0]
gettextFuncNamePlural := l[1]
l = strings.Split(opts.KeywordContext, ".")
gettextSelectorContext := l[0]
gettextFuncNameContext := l[1]
switch x := n.(type) {
case *ast.CallExpr:
if sel, ok := x.Fun.(*ast.SelectorExpr); ok {
i18nStr := ""
i18nStrPlural := ""
i18nStrContext := ""
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 == gettextFuncNameContext && sel.X.(*ast.Ident).Name == gettextSelectorContext {
i18nStr = x.Args[0].(*ast.BasicLit).Value
i18nStrContext = x.Args[1].(*ast.BasicLit).Value
}
if sel.Sel.Name == gettextFuncName && sel.X.(*ast.Ident).Name == gettextSelector {
i18nStr = constructValue(x.Args[0])
}
@ -144,6 +156,7 @@ func inspectNodeForTranslations(fset *token.FileSet, f *ast.File, n ast.Node) bo
msgIDs[msgidStr] = append(msgIDs[msgidStr], msgID{
formatHint: formatHint,
msgidPlural: formatI18nStr(i18nStrPlural),
context: formatI18nStr(i18nStrContext),
fname: posCall.Filename,
line: posCall.Line,
comment: findCommentsForTranslation(fset, f, posCall),
@ -219,6 +232,79 @@ func mustFprintf(w io.Writer, format string, a ...any) {
}
}
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)
return strings.TrimSuffix(out, "\"\n \"")
}
func writeMsg(out io.Writer, k string, msgidList []msgID) {
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)
}
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")
}
func writeMsgWithContext(out io.Writer, k string, msgidList []msgID) {
for _, msgid := range msgidList {
if opts.AddComments || opts.AddCommentsTag != "" {
mustFprintf(out, "%s", msgid.comment)
}
if !opts.NoLocation {
mustFprintf(out, "#: %s:%d\n", msgid.fname, msgid.line)
}
if msgid.formatHint != "" {
mustFprintf(out, "#, %s\n", msgid.formatHint)
}
if msgid.context != "" {
mustFprintf(out, "msgctxt \"%v\"\n", formatOutput(msgid.context))
}
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")
}
}
func writePotFile(out io.Writer) {
header := fmt.Sprintf(`# SOME DESCRIPTIVE TITLE.
@ -253,39 +339,25 @@ msgstr "Project-Id-Version: %s\n"
// FIXME: use template here?
for _, k := range sortedKeys {
var msgIDsWithoutContext []msgID
var msgIDsWithContext []msgID
msgidList := msgIDs[k]
for _, msgid := range msgidList {
if opts.AddComments || opts.AddCommentsTag != "" {
mustFprintf(out, "%s", msgid.comment)
if msgid.context == "" {
msgIDsWithoutContext = append(msgIDsWithoutContext, msgid)
} else {
msgIDsWithContext = append(msgIDsWithContext, msgid)
}
}
if !opts.NoLocation {
mustFprintf(out, "#:")
for _, msgid := range msgidList {
mustFprintf(out, " %s:%d", msgid.fname, msgid.line)
}
mustFprintf(out, "\n")
if len(msgIDsWithoutContext) > 0 {
writeMsg(out, k, msgIDsWithoutContext)
}
msgid := msgidList[0]
if msgid.formatHint != "" {
mustFprintf(out, "#, %s\n", msgid.formatHint)
if len(msgIDsWithContext) > 0 {
writeMsgWithContext(out, k, msgIDsWithContext)
}
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")
}
}
@ -310,8 +382,9 @@ var opts struct {
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"`
Keyword string `short:"k" long:"keyword" default:"gettext.Gettext" description:"look for WORD as the keyword for singular strings"`
KeywordContext string `long:"keyword-ctx" default:"gettext.PGettext" description:"look for WORD as the keyword for singular strings with context"`
KeywordPlural string `long:"keyword-plural" default:"gettext.NGettext" description:"look for WORD as the keyword for plural strings"`
}
func main() {

View File

@ -54,6 +54,7 @@ func (s *xgettextTestSuite) SetUpTest(c *C) {
opts.AddCommentsTag = "TRANSLATORS:"
opts.Keyword = "i18n.G"
opts.KeywordPlural = "i18n.NG"
opts.KeywordContext = "i18n.GC"
opts.SortOutput = true
opts.PackageName = "snappy"
opts.MsgIDBugsAddress = "snappy-devel@lists.ubuntu.com"
@ -102,6 +103,29 @@ func main() {
})
}
func (s *xgettextTestSuite) TestProcessFilesSimpleWithContext(c *C) {
fname := makeGoSourceFile(c, []byte(`package main
func main() {
// TRANSLATORS: foo comment
i18n.GC("foo", "foo context")
}
`))
err := processFiles([]string{fname})
c.Assert(err, IsNil)
c.Assert(msgIDs, DeepEquals, map[string][]msgID{
"foo": {
{
comment: "#. TRANSLATORS: foo comment\n",
context: "foo context",
fname: fname,
line: 5,
},
},
})
}
func (s *xgettextTestSuite) TestProcessFilesMultiple(c *C) {
fname := makeGoSourceFile(c, []byte(`package main
@ -132,6 +156,38 @@ func main() {
})
}
func (s *xgettextTestSuite) TestProcessFilesMultipleWithContext(c *C) {
fname := makeGoSourceFile(c, []byte(`package main
func main() {
// TRANSLATORS: foo comment
i18n.GC("foo", "foo context")
// TRANSLATORS: bar comment
i18n.GC("foo", "foo context 2")
}
`))
err := processFiles([]string{fname})
c.Assert(err, IsNil)
c.Assert(msgIDs, DeepEquals, map[string][]msgID{
"foo": {
{
comment: "#. TRANSLATORS: foo comment\n",
context: "foo context",
fname: fname,
line: 5,
},
{
comment: "#. TRANSLATORS: bar comment\n",
context: "foo context 2",
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.
@ -174,6 +230,31 @@ msgstr ""
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputSimpleWithContext(c *C) {
msgIDs = map[string][]msgID{
"foo": {
{
fname: "fname",
line: 2,
comment: "#. foo\n",
context: "foo context",
},
},
}
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#. foo
#: fname:2
msgctx "foo context"
msgid "foo"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputMultiple(c *C) {
msgIDs = map[string][]msgID{
"foo": {
@ -203,6 +284,43 @@ msgstr ""
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputMultipleWithContext(c *C) {
msgIDs = map[string][]msgID{
"foo": {
{
fname: "fname",
context: "context1",
line: 2,
comment: "#. comment1\n",
},
{
fname: "fname",
context: "context2",
line: 4,
comment: "#. comment2\n",
},
},
}
out := bytes.NewBuffer([]byte(""))
writePotFile(out)
expected := fmt.Sprintf(`%s
#. comment1
#: fname:2
msgctx "context1"
msgid "foo"
msgstr ""
#. comment2
#: fname:4
msgctx "context2"
msgid "foo"
msgstr ""
`, header)
c.Assert(out.String(), Equals, expected)
}
func (s *xgettextTestSuite) TestWriteOutputNoComment(c *C) {
msgIDs = map[string][]msgID{
"foo": {