initial rework of xgotext
This commit is contained in:
parent
2b59b30398
commit
75a3d22c53
8 changed files with 525 additions and 336 deletions
|
@ -4,6 +4,8 @@ import (
|
|||
"fmt"
|
||||
|
||||
"github.com/leonelquinteros/gotext"
|
||||
alias "github.com/leonelquinteros/gotext"
|
||||
"github.com/leonelquinteros/gotext/cli/xgotext/fixtures/pkg"
|
||||
)
|
||||
|
||||
// Fake object with methods similar to gotext
|
||||
|
@ -15,12 +17,26 @@ func (f Fake) Get(id int) int {
|
|||
return 42
|
||||
}
|
||||
|
||||
// Fake object with same methods as gotext
|
||||
type Fake2 struct {
|
||||
}
|
||||
|
||||
// Get by str
|
||||
func (f Fake2) Get(s string) string {
|
||||
return s
|
||||
}
|
||||
|
||||
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"))
|
||||
// same as before
|
||||
fmt.Println(gotext.Get("My text on 'domain-name' domain"))
|
||||
|
||||
// same with alias package name
|
||||
fmt.Println(alias.Get("alias call"))
|
||||
|
||||
// Translate text from a different domain without reconfigure
|
||||
fmt.Println(gotext.GetD("domain2", "Another text on a different domain"))
|
||||
|
@ -43,6 +59,24 @@ func main() {
|
|||
l.GetDC("domain2", "string", "ctx")
|
||||
l.GetNDC("translations", "ndc", "ndcs", 7, "NDC-CTX")
|
||||
|
||||
// try fake structs
|
||||
f := Fake{}
|
||||
f.Get(3)
|
||||
|
||||
f2 := Fake2{}
|
||||
f2.Get("3")
|
||||
|
||||
// use translator of sub object
|
||||
t := pkg.Translate{}
|
||||
t.L.Get("translate package")
|
||||
t.S.L.Get("translate sub package")
|
||||
|
||||
// redefine alias with fake struct
|
||||
alias := Fake2{}
|
||||
alias.Get("3")
|
||||
}
|
||||
|
||||
// dummy function
|
||||
func dummy(locale *gotext.Locale) {
|
||||
locale.Get("inside dummy")
|
||||
}
|
||||
|
|
16
cli/xgotext/fixtures/pkg/pkg.go
Normal file
16
cli/xgotext/fixtures/pkg/pkg.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
package pkg
|
||||
|
||||
import "github.com/leonelquinteros/gotext"
|
||||
|
||||
type SubTranslate struct {
|
||||
L gotext.Locale
|
||||
}
|
||||
|
||||
type Translate struct {
|
||||
L gotext.Locale
|
||||
S SubTranslate
|
||||
}
|
||||
|
||||
func test() {
|
||||
gotext.Get("inside sub package")
|
||||
}
|
|
@ -2,24 +2,16 @@ package main
|
|||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/leonelquinteros/gotext/cli/xgotext/parser"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = flag.Bool("debug", false, "enable debug mode and print AST")
|
||||
dirName = flag.String("in", "", "input dir: /path/to/go/pkg")
|
||||
outputDir = flag.String("out", "", "output dir: /path/to/i18n/files")
|
||||
fset *token.FileSet
|
||||
domainFiles map[string]*os.File
|
||||
currentDomain = "default"
|
||||
currentFile string
|
||||
dirName = flag.String("in", "", "input dir: /path/to/go/pkg")
|
||||
outputDir = flag.String("out", "", "output dir: /path/to/i18n/files")
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
@ -28,44 +20,24 @@ func main() {
|
|||
// Init logger
|
||||
log.SetFlags(0)
|
||||
|
||||
// Init domain files
|
||||
domainFiles = make(map[string]*os.File)
|
||||
|
||||
// Check if dir name parameter is valid
|
||||
log.Println(*dirName)
|
||||
f, err := os.Stat(*dirName)
|
||||
data, err := parser.ParseDirRec(*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)
|
||||
err = os.MkdirAll(*outputDir, os.ModePerm)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
log.Fatalf("failed to create output dir: %s", err)
|
||||
}
|
||||
domainFiles[domain] = f
|
||||
writePoHeader(f)
|
||||
|
||||
return f
|
||||
}
|
||||
for name, domain := range data {
|
||||
outFile := filepath.Join(*outputDir, name+".po")
|
||||
file, err := os.Create(outFile)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to save po file for %s: %s", name, err)
|
||||
}
|
||||
|
||||
func writePoHeader(f *os.File) {
|
||||
h := `msgid ""
|
||||
file.WriteString(`msgid ""
|
||||
msgstr ""
|
||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
|
@ -74,298 +46,8 @@ msgstr ""
|
|||
"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 mode
|
||||
if *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)
|
||||
|
||||
case "SetDomain":
|
||||
parseSetDomain(n)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseGet(call *ast.CallExpr) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func parseSetDomain(call *ast.CallExpr) {
|
||||
if call.Args != nil && len(call.Args) == 1 {
|
||||
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
|
||||
if lit.Kind == token.STRING {
|
||||
cd, err := strconv.Unquote(lit.Value)
|
||||
if err == nil {
|
||||
currentDomain = cd
|
||||
}
|
||||
}
|
||||
}
|
||||
`)
|
||||
file.WriteString(domain.Dump())
|
||||
file.Close()
|
||||
}
|
||||
}
|
||||
|
|
129
cli/xgotext/parser/domain.go
Normal file
129
cli/xgotext/parser/domain.go
Normal file
|
@ -0,0 +1,129 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Translation for a text to translate
|
||||
type Translation struct {
|
||||
MsgId string
|
||||
MsgIdPlural string
|
||||
Context string
|
||||
SourceLocations []string
|
||||
}
|
||||
|
||||
// AddLocations to translation
|
||||
func (t *Translation) AddLocations(locations []string) {
|
||||
if t.SourceLocations == nil {
|
||||
t.SourceLocations = locations
|
||||
} else {
|
||||
t.SourceLocations = append(t.SourceLocations, locations...)
|
||||
}
|
||||
}
|
||||
|
||||
// Dump translation as string
|
||||
func (t *Translation) Dump() string {
|
||||
data := make([]string, 0, len(t.SourceLocations)+5)
|
||||
|
||||
for _, location := range t.SourceLocations {
|
||||
data = append(data, "#: "+location)
|
||||
}
|
||||
|
||||
if t.Context != "" {
|
||||
data = append(data, "msgctxt "+t.Context)
|
||||
}
|
||||
|
||||
data = append(data, "msgid "+t.MsgId)
|
||||
|
||||
if t.MsgIdPlural == "" {
|
||||
data = append(data, "msgstr \"\"")
|
||||
} else {
|
||||
data = append(data,
|
||||
"msgid_plural "+t.MsgIdPlural,
|
||||
"msgstr[0] \"\"",
|
||||
"msgstr[1] \"\"")
|
||||
}
|
||||
|
||||
return strings.Join(data, "\n")
|
||||
}
|
||||
|
||||
// TranslationMap contains a map of translations with the ID as key
|
||||
type TranslationMap map[string]*Translation
|
||||
|
||||
// Dump the translation map as string
|
||||
func (m TranslationMap) Dump() string {
|
||||
// sort by translation id for consistence output
|
||||
keys := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
data := make([]string, 0, len(m))
|
||||
for _, key := range keys {
|
||||
data = append(data, (m)[key].Dump())
|
||||
}
|
||||
return strings.Join(data, "\n\n")
|
||||
}
|
||||
|
||||
// Domain holds all translations of one domain
|
||||
type Domain struct {
|
||||
Translations TranslationMap
|
||||
ContextTranslations map[string]TranslationMap
|
||||
}
|
||||
|
||||
// AddTranslation to the domain
|
||||
func (d *Domain) AddTranslation(translation *Translation) {
|
||||
if d.Translations == nil {
|
||||
d.Translations = make(TranslationMap)
|
||||
d.ContextTranslations = make(map[string]TranslationMap)
|
||||
}
|
||||
|
||||
if translation.Context == "" {
|
||||
if t, ok := d.Translations[translation.MsgId]; ok {
|
||||
t.AddLocations(translation.SourceLocations)
|
||||
} else {
|
||||
d.Translations[translation.MsgId] = translation
|
||||
}
|
||||
} else {
|
||||
if _, ok := d.ContextTranslations[translation.Context]; !ok {
|
||||
d.ContextTranslations[translation.Context] = make(TranslationMap)
|
||||
}
|
||||
|
||||
if t, ok := d.ContextTranslations[translation.Context][translation.MsgId]; ok {
|
||||
t.AddLocations(translation.SourceLocations)
|
||||
} else {
|
||||
d.ContextTranslations[translation.Context][translation.MsgId] = translation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dump the domain as string
|
||||
func (d *Domain) Dump() string {
|
||||
data := make([]string, 0, len(d.ContextTranslations)+1)
|
||||
data = append(data, d.Translations.Dump())
|
||||
|
||||
// sort context translations by context for consistence output
|
||||
keys := make([]string, 0, len(d.ContextTranslations))
|
||||
for k := range d.ContextTranslations {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
sort.Strings(keys)
|
||||
|
||||
for _, key := range keys {
|
||||
data = append(data, d.ContextTranslations[key].Dump())
|
||||
}
|
||||
return strings.Join(data, "\n\n")
|
||||
}
|
||||
|
||||
// DomainMap contains multiple domains as map with name as key
|
||||
type DomainMap map[string]*Domain
|
||||
|
||||
// AddTranslation to domain map
|
||||
func (m *DomainMap) AddTranslation(domain string, translation *Translation) {
|
||||
if _, ok := (*m)[domain]; !ok {
|
||||
(*m)[domain] = new(Domain)
|
||||
}
|
||||
(*m)[domain].AddTranslation(translation)
|
||||
}
|
255
cli/xgotext/parser/golang.go
Normal file
255
cli/xgotext/parser/golang.go
Normal file
|
@ -0,0 +1,255 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
)
|
||||
|
||||
// GetterDef describes a getter
|
||||
type GetterDef struct {
|
||||
Id int
|
||||
Plural int
|
||||
Context int
|
||||
Domain int
|
||||
}
|
||||
|
||||
// maxArgIndex returns the largest argument index
|
||||
func (d *GetterDef) maxArgIndex() int {
|
||||
m := d.Id
|
||||
if d.Plural > m {
|
||||
m = d.Plural
|
||||
}
|
||||
if d.Context > m {
|
||||
m = d.Context
|
||||
}
|
||||
if d.Domain > m {
|
||||
m = d.Domain
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// list of supported getter
|
||||
var gotextGetter = map[string]GetterDef{
|
||||
"Get": {0, -1, -1, -1},
|
||||
"GetN": {0, 1, -1, -1},
|
||||
"GetD": {1, -1, -1, 0},
|
||||
"GetND": {1, 2, -1, 0},
|
||||
"GetC": {0, -1, 1, -1},
|
||||
"GetNC": {0, 1, 3, -1},
|
||||
"GetDC": {1, -1, 2, 0},
|
||||
"GetNDC": {1, 2, 4, 0},
|
||||
}
|
||||
|
||||
// register go parser
|
||||
func init() {
|
||||
AddParser(goParser)
|
||||
}
|
||||
|
||||
// parse go package
|
||||
func goParser(dirPath, basePath string, data DomainMap) error {
|
||||
fileSet := token.NewFileSet()
|
||||
|
||||
conf := packages.Config{
|
||||
Mode: packages.NeedName |
|
||||
packages.NeedFiles |
|
||||
packages.NeedSyntax |
|
||||
packages.NeedTypes |
|
||||
packages.NeedTypesInfo,
|
||||
Fset: fileSet,
|
||||
Dir: basePath,
|
||||
}
|
||||
|
||||
// load package from path
|
||||
pkgs, err := packages.Load(&packages.Config{
|
||||
Mode: conf.Mode,
|
||||
Fset: fileSet,
|
||||
Dir: dirPath,
|
||||
})
|
||||
if err != nil || len(pkgs) == 0 {
|
||||
// not a go package
|
||||
return nil
|
||||
}
|
||||
|
||||
// handle each file
|
||||
for _, node := range pkgs[0].Syntax {
|
||||
file := GoFile{
|
||||
pkgConf: &conf,
|
||||
filePath: fileSet.Position(node.Package).Filename,
|
||||
basePath: basePath,
|
||||
data: data,
|
||||
fileSet: fileSet,
|
||||
|
||||
importedPackages: map[string]*packages.Package{
|
||||
pkgs[0].Name: pkgs[0],
|
||||
},
|
||||
}
|
||||
|
||||
ast.Inspect(node, file.inspectFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GoFile handles the parsing of one go file
|
||||
type GoFile struct {
|
||||
filePath string
|
||||
basePath string
|
||||
data DomainMap
|
||||
|
||||
fileSet *token.FileSet
|
||||
pkgConf *packages.Config
|
||||
|
||||
importedPackages map[string]*packages.Package
|
||||
}
|
||||
|
||||
// getPackage loads module by name
|
||||
func (g *GoFile) getPackage(name string) (*packages.Package, error) {
|
||||
pkgs, err := packages.Load(g.pkgConf, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(pkgs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
return pkgs[0], nil
|
||||
}
|
||||
|
||||
// getType from ident object
|
||||
func (g *GoFile) getType(ident *ast.Ident) types.Object {
|
||||
for _, pkg := range g.importedPackages {
|
||||
if obj, ok := pkg.TypesInfo.Uses[ident]; ok {
|
||||
return obj
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (g *GoFile) inspectFile(n ast.Node) bool {
|
||||
switch x := n.(type) {
|
||||
// get names of imported packages
|
||||
case *ast.ImportSpec:
|
||||
packageName, _ := strconv.Unquote(x.Path.Value)
|
||||
|
||||
pkg, err := g.getPackage(packageName)
|
||||
if err != nil {
|
||||
log.Printf("failed to load package %s: %s", packageName, err)
|
||||
} else {
|
||||
if x.Name == nil {
|
||||
g.importedPackages[pkg.Name] = pkg
|
||||
} else {
|
||||
g.importedPackages[x.Name.Name] = pkg
|
||||
}
|
||||
}
|
||||
|
||||
// check each function call
|
||||
case *ast.CallExpr:
|
||||
g.inspectCallExpr(x)
|
||||
|
||||
default:
|
||||
print()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// checkType for gotext object
|
||||
func (g *GoFile) checkType(rawType types.Type) bool {
|
||||
switch t := rawType.(type) {
|
||||
case *types.Pointer:
|
||||
return g.checkType(t.Elem())
|
||||
|
||||
case *types.Named:
|
||||
if t.Obj().Pkg().Path() != "github.com/leonelquinteros/gotext" {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (g *GoFile) inspectCallExpr(n *ast.CallExpr) {
|
||||
// must be a selector expression otherwise it is a local function call
|
||||
expr, ok := n.Fun.(*ast.SelectorExpr)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
switch e := expr.X.(type) {
|
||||
// direct call
|
||||
case *ast.Ident:
|
||||
// object is a package if the Obj is not set
|
||||
if e.Obj == nil {
|
||||
pkg, ok := g.importedPackages[e.Name]
|
||||
if !ok || pkg.PkgPath != "github.com/leonelquinteros/gotext" {
|
||||
return
|
||||
}
|
||||
|
||||
} else {
|
||||
// validate type of object
|
||||
if !g.checkType(g.getType(e).Type()) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// call to attribute
|
||||
case *ast.SelectorExpr:
|
||||
// validate type of object
|
||||
if !g.checkType(g.getType(e.Sel).Type()) {
|
||||
return
|
||||
}
|
||||
|
||||
default:
|
||||
return
|
||||
}
|
||||
|
||||
// convert args
|
||||
args := make([]*ast.BasicLit, len(n.Args))
|
||||
for idx, arg := range n.Args {
|
||||
args[idx], _ = arg.(*ast.BasicLit)
|
||||
}
|
||||
|
||||
// get position
|
||||
path, _ := filepath.Rel(g.basePath, g.filePath)
|
||||
position := fmt.Sprintf("%s:%d", path, g.fileSet.Position(n.Lparen).Line)
|
||||
|
||||
// handle getters
|
||||
if def, ok := gotextGetter[expr.Sel.String()]; ok {
|
||||
g.parseGetter(def, args, position)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GoFile) parseGetter(def GetterDef, args []*ast.BasicLit, pos string) {
|
||||
// check if enough arguments are given
|
||||
if len(args) < def.maxArgIndex() {
|
||||
return
|
||||
}
|
||||
|
||||
// get domain
|
||||
var domain string
|
||||
if def.Domain == -1 {
|
||||
domain = "default" // TODO
|
||||
} else {
|
||||
domain, _ = strconv.Unquote(args[def.Domain].Value)
|
||||
}
|
||||
|
||||
trans := Translation{
|
||||
MsgId: args[def.Id].Value,
|
||||
SourceLocations: []string{pos},
|
||||
}
|
||||
if def.Plural > 0 {
|
||||
trans.MsgIdPlural = args[def.Plural].Value
|
||||
}
|
||||
if def.Context > 0 {
|
||||
trans.Context = args[def.Context].Value
|
||||
}
|
||||
|
||||
g.data.AddTranslation(domain, &trans)
|
||||
}
|
55
cli/xgotext/parser/parser.go
Normal file
55
cli/xgotext/parser/parser.go
Normal file
|
@ -0,0 +1,55 @@
|
|||
package parser
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// ParseDirFunc parses one directory
|
||||
type ParseDirFunc func(filePath, basePath string, data DomainMap) error
|
||||
|
||||
var knownParser []ParseDirFunc
|
||||
|
||||
// AddParser to the known parser list
|
||||
func AddParser(parser ParseDirFunc) {
|
||||
if knownParser == nil {
|
||||
knownParser = []ParseDirFunc{parser}
|
||||
} else {
|
||||
knownParser = append(knownParser, parser)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseDir calls all known parser for each directory
|
||||
func ParseDir(dirPath, basePath string, data DomainMap) error {
|
||||
dirPath, _ = filepath.Abs(dirPath)
|
||||
basePath, _ = filepath.Abs(basePath)
|
||||
|
||||
for _, parser := range knownParser {
|
||||
err := parser(dirPath, basePath, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ParseDirRec calls all known parser for each directory
|
||||
func ParseDirRec(dirPath string) (DomainMap, error) {
|
||||
data := make(DomainMap)
|
||||
dirPath, _ = filepath.Abs(dirPath)
|
||||
|
||||
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
err := ParseDir(path, dirPath, data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return data, err
|
||||
}
|
4
go.mod
4
go.mod
|
@ -1,3 +1,7 @@
|
|||
module github.com/leonelquinteros/gotext
|
||||
|
||||
// go: no requirements found in Gopkg.lock
|
||||
|
||||
require golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd
|
||||
|
||||
go 1.13
|
||||
|
|
14
go.sum
Normal file
14
go.sum
Normal file
|
@ -0,0 +1,14 @@
|
|||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee h1:WG0RUwxtNT4qqaXX3DPA8zHFNm/D9xaBpxzHt1WcA/E=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd h1:hHkvGJK23seRCflePJnVa9IMv8fsuavSCWKd11kDQFs=
|
||||
golang.org/x/tools v0.0.0-20200221224223-e1da425f72fd/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898 h1:/atklqdjdhuosWIl6AIbOeHJjicWYPqR9bpxqxYG2pA=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
Loading…
Reference in a new issue