initial rework of xgotext
This commit is contained in:
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
|
||||
}
|
||||
Reference in New Issue
Block a user