Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 113 additions & 67 deletions go-xgettext/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,6 @@ import (
"go/parser"
"go/token"
"io"
"io/ioutil"
"log"
"os"
"sort"
Expand All @@ -50,11 +49,11 @@ var (
noLocation = flag.Bool("no-location", false, "Do not write '#: filename:line' lines.")
msgIDBugsAddress = flag.String("msgid-bugs-address", "EMAIL", "set report address for msgid bugs.")
packageName = flag.String("package-name", "", "Set package name in output.")
deterministic = flag.Bool("deterministic", false, "Generate deterministic output (e.g. no timestamps)")

keyword = flag.String("keyword", "gettext.Gettext", "Look for WORD as the keyword for singular strings.")
keywordPlural = flag.String("keyword-plural", "gettext.NGettext", "Look for WORD as the keyword for plural strings.")
keywordContextual = flag.String("keyword-contextual", "gettext.CGettext", "Look for WORD as the keyword for contextual strings.")
keywordPluralContextual = flag.String("keyword-plural-contextual", "gettext.CNGettext", "Look for WORD as the keyword for plural contextual strings.")
keyword = flag.String("keyword", "gettext.Gettext", "Look for WORD as the keyword for singular strings.")
keywordPlural = flag.String("keyword-plural", "gettext.NGettext", "Look for WORD as the keyword for plural strings.")
keywordContextual = flag.String("keyword-contextual", "gettext.CGettext", "Look for WORD as the keyword for contextual strings.")

skipArgs = flag.Int("skip-args", 0, "Number of arguments to skip in gettext function call before considering a text message argument.")

Expand All @@ -69,25 +68,41 @@ const (
)

type keywordDef struct {
Type string `json:"type"`
Name string `json:"name"`
SkipArgs int `json:"skipArgs"`
Type string `json:"type"`
Name string `json:"name"`
SkipArgs int `json:"skipArgs"`
FormatHint string `json:"formatHint"`
FormatHintArgs int `json:"formatHintArgs"`
ForceContext string `json:"forceContext"`
}

type keywords map[string]*keywordDef

type allKeywordsConfig []*keywordDef
type msgKey struct {
msgctxt string
msgtext string
}

type msgID struct {
type msgData struct {
msgidPlural string
msgctxt string
comment string
fname string
line int
formatHint string
}

var msgIDs map[string][]msgID
type msgKeyList []msgKey

func (l msgKeyList) Len() int { return len(l) }
func (l msgKeyList) Less(i, j int) bool {
if l[i].msgctxt != l[j].msgctxt {
return l[i].msgctxt < l[j].msgctxt
}
return l[i].msgtext < l[j].msgtext
}
func (l msgKeyList) Swap(i, j int) { l[i], l[j] = l[j], l[i] }

var msgIDs map[msgKey][]msgData

func formatComment(com string) string {
out := ""
Expand Down Expand Up @@ -132,28 +147,28 @@ func findCommentsForTranslation(fset *token.FileSet, f *ast.File, posCall token.
}

func constructValue(val interface{}) (string, error) {
switch val.(type) {
switch val := val.(type) {
case *ast.BasicLit:
return val.(*ast.BasicLit).Value, nil
return val.Value, nil
// this happens for constructs like:
// gettext.Gettext("foo" + "bar")
case *ast.BinaryExpr:
// we only support string concat
if val.(*ast.BinaryExpr).Op != token.ADD {
if val.Op != token.ADD {
return "", nil
}
left, err := constructValue(val.(*ast.BinaryExpr).X)
left, err := constructValue(val.X)
if err != nil {
return "", err
}
// strip right " (or `)
left = left[0 : len(left)-1]
right, err := constructValue(val.(*ast.BinaryExpr).Y)
right, err := constructValue(val.Y)
if err != nil {
return "", err
}
// strip left " (or `)
right = right[1:len(right)]
right = right[1:]
return left + right, nil
default:
return "", fmt.Errorf("unknown type: %v", val)
Expand All @@ -179,39 +194,73 @@ func parseFunExpr(path string, expr ast.Expr) string {
func inspectNodeForTranslations(k keywords, fset *token.FileSet, f *ast.File, n ast.Node) bool {
switch x := n.(type) {
case *ast.CallExpr:
var i18nStr, i18nStrPlural, i18nCtxt string
var i18nStr, i18nStrPlural, i18nCtxt, formatHint string
var err error
name := parseFunExpr("", x.Fun)
if name == "" {
break
}
if keyword, ok := k[name]; ok {
idx := keyword.SkipArgs
switch keyword.Type {
case kTypeSingular:
i18nStr, err = constructValue(x.Args[idx])
case kTypePlural:
i18nStr, err = constructValue(x.Args[idx])
if err != nil {
break
}
i18nStrPlural, err = constructValue(x.Args[idx+1])
case kTypeContextual:
i18nCtxt, err = constructValue(x.Args[idx])
if err != nil {
formatHint = keyword.FormatHint
for i := 0; i < keyword.FormatHintArgs; i++ {
if idx >= len(x.Args) {
err = fmt.Errorf("not enough arguments")
break
}
i18nStr, err = constructValue(x.Args[idx+1])
case kTypePluralContextual:
i18nCtxt, err = constructValue(x.Args[idx])
var argVal string
argVal, err = constructValue(x.Args[idx])
if err != nil {
break
}
i18nStr, err = constructValue(x.Args[idx+1])
if err != nil {
break
// strip leading and trailing " (or `)
argVal = argVal[1 : len(argVal)-1]
formatHint = fmt.Sprintf("%s,%s", formatHint, argVal)
idx++
}
if idx >= len(x.Args) {
err = fmt.Errorf("not enough arguments")
} else {
switch keyword.Type {
case kTypeSingular:
i18nCtxt = fmt.Sprintf("%q", keyword.ForceContext)
i18nStr, err = constructValue(x.Args[idx])
case kTypePlural:
i18nCtxt = fmt.Sprintf("%q", keyword.ForceContext)
if idx+1 >= len(x.Args) {
err = fmt.Errorf("not enough arguments")
break
}
i18nStr, err = constructValue(x.Args[idx])
if err != nil {
break
}
i18nStrPlural, err = constructValue(x.Args[idx+1])
case kTypeContextual:
if idx+1 >= len(x.Args) {
err = fmt.Errorf("not enough arguments")
break
}
i18nCtxt, err = constructValue(x.Args[idx])
if err != nil {
break
}
i18nStr, err = constructValue(x.Args[idx+1])
case kTypePluralContextual:
if idx+2 >= len(x.Args) {
err = fmt.Errorf("not enough arguments")
break
}
i18nCtxt, err = constructValue(x.Args[idx])
if err != nil {
break
}
i18nStr, err = constructValue(x.Args[idx+1])
if err != nil {
break
}
i18nStrPlural, err = constructValue(x.Args[idx+2])
}
i18nStrPlural, err = constructValue(x.Args[idx+2])
}
}
if err != nil {
Expand All @@ -223,19 +272,15 @@ func inspectNodeForTranslations(k keywords, fset *token.FileSet, f *ast.File, n
break
}

// FIXME: too simplistic(?), no %% is considered
formatHint := ""
if strings.Contains(i18nStr, "%") || strings.Contains(i18nStrPlural, "%") {
// well, not quite correct but close enough
formatHint = "c-format"
}
i18nCtxt = formatI18nStr(i18nCtxt)
i18nStr = formatI18nStr(i18nStr)
i18nStrPlural = formatI18nStr(i18nStrPlural)

msgidStr := formatI18nStr(i18nStr)
posCall := fset.Position(n.Pos())
msgIDs[msgidStr] = append(msgIDs[msgidStr], msgID{
k := msgKey{i18nCtxt, i18nStr}
msgIDs[k] = append(msgIDs[k], msgData{
formatHint: formatHint,
msgidPlural: formatI18nStr(i18nStrPlural),
msgctxt: formatI18nStr(i18nCtxt),
msgidPlural: i18nStrPlural,
fname: posCall.Filename,
line: posCall.Line,
comment: findCommentsForTranslation(fset, f, posCall),
Expand Down Expand Up @@ -263,7 +308,7 @@ func formatI18nStr(s string) string {

func processFiles(args []string) error {
// go over the input files
msgIDs = make(map[string][]msgID)
msgIDs = make(map[msgKey][]msgData)

fset := token.NewFileSet()
for _, fname := range args {
Expand All @@ -278,7 +323,7 @@ func processFiles(args []string) error {
func parseKeywords() (keywords, error) {
k := make(keywords)
if *keywordCfg != "" {
data, err := ioutil.ReadFile(*keywordCfg)
data, err := os.ReadFile(*keywordCfg)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -310,7 +355,7 @@ func parseKeywords() (keywords, error) {
}

func processSingleGoSource(fset *token.FileSet, fname string) error {
fnameContent, err := ioutil.ReadFile(fname)
fnameContent, err := os.ReadFile(fname)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -349,42 +394,38 @@ 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 <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=CHARSET\n"
"Content-Type: text/plain; charset=utf-8\n"
"Content-Transfer-Encoding: 8bit\n"

`, *packageName, *msgIDBugsAddress, formatTime())
fmt.Fprintf(out, "%s", header)

// yes, this is the way to do it in go
sortedKeys := []string{}
var sortedKeys msgKeyList
for k := range msgIDs {
sortedKeys = append(sortedKeys, k)
}
if *sortOutput {
sort.Strings(sortedKeys)
sort.Sort(sortedKeys)
}

// FIXME: use template here?
for _, k := range sortedKeys {
msgidList := msgIDs[k]
for _, msgid := range msgidList {
for _, key := range sortedKeys {
msgList := msgIDs[key]
for _, msgid := range msgList {
if *addComments || *addCommentsTag != "" {
fmt.Fprintf(out, "%s", msgid.comment)
}
}
if !*noLocation {
fmt.Fprintf(out, "#:")
for _, msgid := range msgidList {
for _, msgid := range msgList {
fmt.Fprintf(out, " %s:%d", msgid.fname, msgid.line)
}
fmt.Fprintf(out, "\n")
}
msgid := msgidList[0]
msgid := msgList[0]
if msgid.formatHint != "" {
fmt.Fprintf(out, "#, %s\n", msgid.formatHint)
}
Expand All @@ -395,10 +436,10 @@ msgstr "Project-Id-Version: %s\n"
// cleanup too aggressive splitting (empty "" lines)
return strings.TrimSuffix(out, "\"\n \"")
}
if msgid.msgctxt != "" {
fmt.Fprintf(out, "msgctxt \"%v\"\n", formatOutput(msgid.msgctxt))
if key.msgctxt != "" {
fmt.Fprintf(out, "msgctxt \"%v\"\n", formatOutput(key.msgctxt))
}
fmt.Fprintf(out, "msgid \"%v\"\n", formatOutput(k))
fmt.Fprintf(out, "msgid \"%v\"\n", formatOutput(key.msgtext))
if msgid.msgidPlural != "" {
fmt.Fprintf(out, "msgid_plural \"%v\"\n", formatOutput(msgid.msgidPlural))
fmt.Fprintf(out, "msgstr[0] \"\"\n")
Expand All @@ -408,7 +449,6 @@ msgstr "Project-Id-Version: %s\n"
}
fmt.Fprintf(out, "\n")
}

}

func main() {
Expand All @@ -420,6 +460,12 @@ func main() {
flag.PrintDefaults()
os.Exit(0)
}
if *deterministic {
*sortOutput = true
formatTime = func() string {
return "1970-01-01 00:00+0000"
}
}

if err := processFiles(args); err != nil {
log.Fatalf("processFiles failed with: %s", err)
Expand Down
Loading