First implementation of CLI tool

This commit is contained in:
Leonel Quinteros
2019-02-15 15:20:42 -03:00
parent ff3209d159
commit 0e382cfe26
8 changed files with 531 additions and 1 deletions

40
cli/xgotext/README.md Normal file
View File

@@ -0,0 +1,40 @@
# xgotext
CLI tool to extract translation strings from Go packages into .PO files.
## Installation
```
go install github.com/leonelquinteros/gotext/cli/xgotext
```
## Usage
```
xgotext /path/to/go/package [/path/to/output/dir]
```
## Implementation
This is the first (naive) implementation for this tool.
It will scan the Go package provided for method calls that matches the method names from the gotext package and write the corresponding translation files to the output directory.
Isn't able to parse calls to translation functions using parameters inside variables, if the translation string is inside a variable and that variable is used to invoke the translation function, this tool won't be able to parse that string. See this example code:
```go
// This line will be added to the .po file
gotext.Get("Translate this")
tr := "Translate this string"
// The following line will NOT be added to the .po file
gotext.Get(tr)
```
The CLI tool doesn't traverse sub-directories and other packages.
## Contribute
Please

View File

@@ -0,0 +1,28 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"X-Generator: xgotext\n"
#: fixtures/main.go:23
#. gotext.Get
msgid "My text on 'domain-name' domain"
msgstr ""
#: fixtures/main.go:38
#. l.GetN
msgid "Singular"
msgid_plural "Plural"
msgstr[0] ""
msgstr[1] ""
#: fixtures/main.go:40
#. l.GetN
msgid "SingularVar"
msgid_plural "PluralVar"
msgstr[0] ""
msgstr[1] ""

View File

@@ -0,0 +1,15 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"X-Generator: xgotext\n"
#: fixtures/main.go:42
#. l.GetDC
msgctxt "ctx"
msgid "string"
msgstr ""

View File

@@ -0,0 +1,14 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"X-Generator: xgotext\n"
#: fixtures/main.go:26
#. gotext.GetD
msgid "Another text on a different domain"
msgstr ""

View File

@@ -0,0 +1,22 @@
msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"X-Generator: xgotext\n"
#: fixtures/main.go:35
#. l.GetD
msgid "Translate this"
msgstr ""
#: fixtures/main.go:43
#. l.GetNDC
msgctxt "NDC-CTX"
msgid "ndc"
msgid_plural "ndcs"
msgstr[0] ""
msgstr[1] ""

View File

@@ -0,0 +1,47 @@
package main
import (
"fmt"
"github.com/leonelquinteros/gotext"
)
// Fake object with methods similar to gotext
type Fake struct {
}
// Get by id
func (f Fake) Get(id int) int {
return 42
}
func main() {
// Configure package
gotext.Configure("/path/to/locales/root/dir", "en_UK", "domain-name")
// Translate text from default domain
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
// Translate text from a different domain without reconfigure
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
// Create Locale with library path and language code
l := gotext.NewLocale("/path/to/locales/root/dir", "es_UY")
// Load domain '/path/to/locales/root/dir/es_UY/default.po'
l.AddDomain("default")
// Translate text from domain
fmt.Println(l.GetD("translations", "Translate this"))
// Get plural translations
l.GetN("Singular", "Plural", 4)
num := 17
l.GetN("SingularVar", "PluralVar", num)
l.GetDC("domain", "string", "ctx")
l.GetNDC("translations", "ndc", "ndcs", 7, "NDC-CTX")
f := Fake{}
f.Get(3)
}

364
cli/xgotext/main.go Normal file
View File

@@ -0,0 +1,364 @@
package main
import (
"fmt"
"go/ast"
"go/parser"
"go/token"
"log"
"os"
"path"
"strconv"
)
var (
dirName string
outputDir string
fset *token.FileSet
domainFiles map[string]*os.File
currentDomain = "default"
currentFile string
)
func main() {
// Init logger
log.SetFlags(0)
// Init domain files
domainFiles = make(map[string]*os.File)
// Validate args
if len(os.Args) < 2 {
log.Println("Usage: ")
log.Fatal("$ xgotext /path/to/package [ /path/to/output/dir ]")
}
if len(os.Args) > 2 {
outputDir = os.Args[2]
}
// Check if dir name parameter is valid
dirName = os.Args[1]
f, err := os.Stat(dirName)
if err != nil {
log.Fatal(err)
}
// Process file or dir
if f.IsDir() {
parseDir(dirName)
} else {
parseFile(dirName)
}
}
func getDomainFile(domain string) *os.File {
// Return existent when available
if f, ok := domainFiles[domain]; ok {
return f
}
// If the file doesn't exist, create it.
filePath := path.Join(outputDir, domain+".po")
f, err := os.OpenFile(filePath, os.O_TRUNC|os.O_CREATE|os.O_WRONLY, 0644)
if err != nil {
log.Fatal(err)
}
domainFiles[domain] = f
writePoHeader(f)
return f
}
func writePoHeader(f *os.File) {
h := `msgid ""
msgstr ""
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Language: \n"
"X-Generator: xgotext\n"
`
f.Write([]byte(h))
}
func write(dom, msgid string) {
f := getDomainFile(dom)
f.Write([]byte("\nmsgid " + msgid))
f.Write([]byte("\nmsgstr \"\""))
f.Write([]byte("\n"))
}
func writePlural(dom, msgid, msgidPlural string) {
f := getDomainFile(dom)
f.Write([]byte("\nmsgid " + msgid))
f.Write([]byte("\nmsgid_plural " + msgidPlural))
f.Write([]byte("\nmsgstr[0] \"\""))
f.Write([]byte("\nmsgstr[1] \"\""))
f.Write([]byte("\n"))
}
func writeContext(dom, ctx string) {
f := getDomainFile(dom)
f.Write([]byte("\nmsgctxt " + ctx))
}
func writeComments(dom, file, call string) {
f := getDomainFile(dom)
f.Write([]byte("\n#: " + file))
f.Write([]byte("\n#. " + call))
}
func parseDir(dirName string) error {
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, dirName, nil, parser.AllErrors)
if err != nil {
log.Fatal(err)
}
for _, pkg := range pkgs {
for fn := range pkg.Files {
parseFile(fn)
}
}
return nil
}
func parseFile(fileName string) error {
// Remember current file to write comments on .po file
currentFile = fileName
// Parse AST
fset = token.NewFileSet()
node, err := parser.ParseFile(fset, fileName, nil, parser.AllErrors)
if err != nil {
log.Fatal(err)
return err
}
// Debug
//ast.Print(fset, node)
ast.Inspect(node, inspectFile)
return nil
}
func inspectFile(n ast.Node) bool {
switch x := n.(type) {
case *ast.CallExpr:
inspectCallExpr(x)
}
return true
}
func inspectCallExpr(n *ast.CallExpr) {
if se, ok := n.Fun.(*ast.SelectorExpr); ok {
switch se.Sel.String() {
case "Get":
parseGet(n)
case "GetN":
parseGetN(n)
case "GetD":
parseGetD(n)
case "GetND":
parseGetND(n)
case "GetC":
parseGetC(n)
case "GetNC":
parseGetNC(n)
case "GetDC":
parseGetDC(n)
case "GetNDC":
parseGetNDC(n)
}
}
}
func parseGet(call *ast.CallExpr) {
// Expect first param to be string
if call.Args != nil && len(call.Args) > 0 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit.Kind == token.STRING {
writeComments(currentDomain,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
write(currentDomain, lit.Value)
}
}
}
}
func parseGetN(call *ast.CallExpr) {
// Expect at least 3 params, first 2 strings, third int
if call.Args == nil || len(call.Args) < 3 {
return
}
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok1 := call.Args[1].(*ast.BasicLit); ok1 {
if lit.Kind == token.STRING && lit1.Kind == token.STRING {
switch x := call.Args[2].(type) {
case *ast.BasicLit:
if x.Kind != token.INT {
return
}
case *ast.Ident:
if x.Obj.Kind != ast.Var && x.Obj.Kind != ast.Con {
return
}
default:
return
}
writeComments(currentDomain,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writePlural(currentDomain, lit.Value, lit1.Value)
}
}
}
}
func parseGetD(call *ast.CallExpr) {
// Expect first 2 params to be string
if call.Args != nil && len(call.Args) > 1 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING {
dom, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatal(err)
}
writeComments(dom,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
write(dom, lit1.Value)
}
}
}
}
}
func parseGetND(call *ast.CallExpr) {
// Expect first 3 params to be string
if call.Args != nil && len(call.Args) > 2 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit2, ok := call.Args[2].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING && lit2.Kind == token.STRING {
dom, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatal(err)
}
writeComments(dom,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writePlural(dom, lit1.Value, lit2.Value)
}
}
}
}
}
}
func parseGetC(call *ast.CallExpr) {
// Expect first 2 params to be string
if call.Args != nil && len(call.Args) > 1 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING {
writeComments(currentDomain,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writeContext(currentDomain, lit1.Value)
write(currentDomain, lit.Value)
}
}
}
}
}
func parseGetNC(call *ast.CallExpr) {
// Expect at least 4 params. 1, 2, and 3 as string
if call.Args != nil && len(call.Args) > 3 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit3, ok := call.Args[3].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING && lit3.Kind == token.STRING {
writeComments(currentDomain,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writeContext(currentDomain, lit3.Value)
writePlural(currentDomain, lit.Value, lit1.Value)
}
}
}
}
}
}
func parseGetDC(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) > 2 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit2, ok := call.Args[2].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING && lit2.Kind == token.STRING {
dom, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatal(err)
}
writeComments(dom,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writeContext(dom, lit2.Value)
write(dom, lit1.Value)
}
}
}
}
}
}
func parseGetNDC(call *ast.CallExpr) {
if call.Args != nil && len(call.Args) > 4 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
if lit1, ok := call.Args[1].(*ast.BasicLit); ok {
if lit2, ok := call.Args[2].(*ast.BasicLit); ok {
if lit4, ok := call.Args[4].(*ast.BasicLit); ok {
if lit.Kind == token.STRING && lit1.Kind == token.STRING && lit2.Kind == token.STRING && lit4.Kind == token.STRING {
dom, err := strconv.Unquote(lit.Value)
if err != nil {
log.Fatal(err)
}
writeComments(dom,
fmt.Sprintf("%s:%d", fset.Position(call.Lparen).Filename, fset.Position(call.Lparen).Line),
fmt.Sprintf("%s.%s", call.Fun.(*ast.SelectorExpr).X.(*ast.Ident).Name, call.Fun.(*ast.SelectorExpr).Sel.String()),
)
writeContext(dom, lit4.Value)
writePlural(dom, lit1.Value, lit2.Value)
}
}
}
}
}
}
}

View File

@@ -152,7 +152,7 @@ func (l *Locale) AddTranslator(dom string, tr Translator) {
l.Unlock()
}
// GetDomain is the domain getter for the package configuration
// GetDomain is the domain getter for Locale configuration
func (l *Locale) GetDomain() string {
l.RLock()
dom := l.defaultDomain