aboutsummaryrefslogtreecommitdiffhomepage
path: root/cmd
diff options
context:
space:
mode:
authorThomas Voss <mail@thomasvoss.com> 2025-11-03 15:51:56 +0100
committerThomas Voss <mail@thomasvoss.com> 2025-11-03 15:51:56 +0100
commitef4e140949408a917b45cc253dd38d229e34f46b (patch)
treef0b877c76e94b84b7c55cf2bf54a5bd280ddb358 /cmd
parent15f841ab43fd9b431e93d2e870c23ae7695929cc (diff)
Support extracting translations from SQL scripts
Diffstat (limited to 'cmd')
-rw-r--r--cmd/extpo/html.go114
-rw-r--r--cmd/extpo/main.go109
-rw-r--r--cmd/extpo/sql.go54
3 files changed, 173 insertions, 104 deletions
diff --git a/cmd/extpo/html.go b/cmd/extpo/html.go
new file mode 100644
index 0000000..d53cbfa
--- /dev/null
+++ b/cmd/extpo/html.go
@@ -0,0 +1,114 @@
+package main
+
+import (
+ "strings"
+ "text/template/parse"
+)
+
+func processHtml(path string) {
+ trees := make(map[string]*parse.Tree)
+ t := parse.New(path)
+ t.Mode |= parse.ParseComments | parse.SkipFuncCheck
+ try2(t.Parse(string(currentFile), "", "", trees))
+ for _, t := range trees {
+ processHtmlNode(t.Root)
+ }
+}
+
+func processHtmlNode(node parse.Node) {
+ switch n := node.(type) {
+ case *parse.ListNode:
+ for _, m := range n.Nodes {
+ processHtmlNode(m)
+ }
+ case *parse.PipeNode:
+ for _, m := range n.Cmds {
+ processHtmlNode(m)
+ }
+ case *parse.TemplateNode:
+ processHtmlNode(n.Pipe)
+ case *parse.IfNode:
+ processHtmlBranch(n.BranchNode)
+ case *parse.RangeNode:
+ processHtmlBranch(n.BranchNode)
+ case *parse.WithNode:
+ processHtmlBranch(n.BranchNode)
+ case *parse.BranchNode:
+ processHtmlBranch(*n)
+ case *parse.ActionNode:
+ processHtmlNode(n.Pipe)
+ case *parse.CommandNode:
+ if len(n.Args) == 0 {
+ break
+ }
+
+ var funcname string
+ f, ok := n.Args[0].(*parse.FieldNode)
+ if !ok || len(f.Ident) == 0 {
+ ff, ok := n.Args[0].(*parse.VariableNode)
+ if !ok || len(ff.Ident) == 0 {
+ fff, ok := n.Args[0].(*parse.IdentifierNode)
+ if !ok {
+ for _, pipe := range n.Args {
+ processHtmlNode(pipe)
+ }
+ break
+ }
+ funcname = fff.Ident
+ } else {
+ funcname = ff.Ident[len(ff.Ident)-1]
+ }
+ } else {
+ funcname = f.Ident[len(f.Ident)-1]
+ }
+
+ cfg, ok := configs[funcname]
+ if !ok {
+ for _, pipe := range n.Args {
+ processHtmlNode(pipe)
+ }
+ break
+ }
+
+ var (
+ tl translation
+ linenr int
+ )
+
+ if sn, ok := n.Args[cfg.arg].(*parse.StringNode); ok {
+ tl.msgid = sn.Text
+ linenr = getlinenr(sn.Pos.Position())
+ } else {
+ break
+ }
+ if cfg.plural != -1 {
+ if sn, ok := n.Args[cfg.plural].(*parse.StringNode); ok {
+ tl.msgidPlural = sn.Text
+ }
+ }
+ if cfg.context != -1 {
+ if sn, ok := n.Args[cfg.context].(*parse.StringNode); ok {
+ tl.msgctxt = sn.Text
+ }
+ }
+
+ ti := translations[tl]
+ if lastComment != "" {
+ ti.comment = lastComment
+ lastComment = ""
+ }
+ ti.locs = append(ti.locs, loc{currentPath, linenr})
+ translations[tl] = ti
+ case *parse.CommentNode:
+ if strings.HasPrefix(n.Text, "/* TRANSLATORS:") {
+ lastComment = strings.TrimSpace(n.Text[2 : len(n.Text)-2])
+ }
+ }
+}
+
+func processHtmlBranch(n parse.BranchNode) {
+ processHtmlNode(n.List)
+ if n.ElseList != nil {
+ processHtmlNode(n.ElseList)
+ }
+}
diff --git a/cmd/extpo/main.go b/cmd/extpo/main.go
index 6aa1503..5db7b7c 100644
--- a/cmd/extpo/main.go
+++ b/cmd/extpo/main.go
@@ -140,110 +140,11 @@ msgstr ""
func process(path string) {
currentPath = path
currentFile = try2(os.ReadFile(path))
- trees := make(map[string]*parse.Tree)
- t := parse.New(path)
- t.Mode |= parse.ParseComments | parse.SkipFuncCheck
- try2(t.Parse(string(currentFile), "", "", trees))
- for _, t := range trees {
- processNode(t.Root)
- }
-}
-
-func processNode(node parse.Node) {
- switch n := node.(type) {
- case *parse.ListNode:
- for _, m := range n.Nodes {
- processNode(m)
- }
- case *parse.PipeNode:
- for _, m := range n.Cmds {
- processNode(m)
- }
- case *parse.TemplateNode:
- processNode(n.Pipe)
- case *parse.IfNode:
- processBranch(n.BranchNode)
- case *parse.RangeNode:
- processBranch(n.BranchNode)
- case *parse.WithNode:
- processBranch(n.BranchNode)
- case *parse.BranchNode:
- processBranch(*n)
- case *parse.ActionNode:
- processNode(n.Pipe)
- case *parse.CommandNode:
- if len(n.Args) == 0 {
- break
- }
-
- var funcname string
- f, ok := n.Args[0].(*parse.FieldNode)
- if !ok || len(f.Ident) == 0 {
- ff, ok := n.Args[0].(*parse.VariableNode)
- if !ok || len(ff.Ident) == 0 {
- fff, ok := n.Args[0].(*parse.IdentifierNode)
- if !ok {
- for _, pipe := range n.Args {
- processNode(pipe)
- }
- break
- }
- funcname = fff.Ident
- } else {
- funcname = ff.Ident[len(ff.Ident)-1]
- }
- } else {
- funcname = f.Ident[len(f.Ident)-1]
- }
-
- cfg, ok := configs[funcname]
- if !ok {
- for _, pipe := range n.Args {
- processNode(pipe)
- }
- break
- }
-
- var (
- tl translation
- linenr int
- )
-
- if sn, ok := n.Args[cfg.arg].(*parse.StringNode); ok {
- tl.msgid = sn.Text
- linenr = getlinenr(sn.Pos.Position())
- } else {
- break
- }
- if cfg.plural != -1 {
- if sn, ok := n.Args[cfg.plural].(*parse.StringNode); ok {
- tl.msgidPlural = sn.Text
- }
- }
- if cfg.context != -1 {
- if sn, ok := n.Args[cfg.context].(*parse.StringNode); ok {
- tl.msgctxt = sn.Text
- }
- }
-
- ti := translations[tl]
- if lastComment != "" {
- ti.comment = lastComment
- lastComment = ""
- }
- ti.locs = append(ti.locs, loc{currentPath, linenr})
- translations[tl] = ti
- case *parse.CommentNode:
- if strings.HasPrefix(n.Text, "/* TRANSLATORS:") {
- lastComment = strings.TrimSpace(n.Text[2 : len(n.Text)-2])
- }
- }
-}
-
-func processBranch(n parse.BranchNode) {
- processNode(n.List)
- if n.ElseList != nil {
- processNode(n.ElseList)
+ switch {
+ case strings.HasSuffix(path, ".html.tmpl"):
+ processHtml(path)
+ case strings.HasSuffix(path, ".sql"):
+ processSql(path)
}
}
diff --git a/cmd/extpo/sql.go b/cmd/extpo/sql.go
new file mode 100644
index 0000000..43bea9c
--- /dev/null
+++ b/cmd/extpo/sql.go
@@ -0,0 +1,54 @@
+package main
+
+import (
+ "bytes"
+ "errors"
+ "io"
+
+ "github.com/rqlite/sql"
+)
+
+type sqlVisitor struct{}
+
+func processSql(path string) {
+ p := sql.NewParser(bytes.NewReader(currentFile))
+ for {
+ stmt, err := p.ParseStatement()
+ switch {
+ case errors.Is(err, io.EOF):
+ return
+ case err != nil:
+ die(err)
+ }
+ sql.Walk(sqlVisitor{}, stmt)
+ }
+}
+
+func processSqlArgs(msgidExpr, msgctxtExpr sql.Expr, pos sql.Pos) {
+ msgid, ok := msgidExpr.(*sql.StringLit)
+ if !ok {
+ return
+ }
+ msgctxt, ok := msgctxtExpr.(*sql.StringLit)
+ if !ok {
+ return
+ }
+ tl := translation{
+ msgid: msgid.Value,
+ msgctxt: msgctxt.Value,
+ }
+ ti := translations[tl]
+ ti.locs = append(ti.locs, loc{currentPath, pos.Line})
+ translations[tl] = ti
+}
+
+func (v sqlVisitor) Visit(n sql.Node) (sql.Visitor, sql.Node, error) {
+ if cn, ok := n.(*sql.Call); ok && sql.IdentName(cn.Name) == "C_" {
+ processSqlArgs(cn.Args[0], cn.Args[1], cn.Lparen)
+ }
+ return v, n, nil
+}
+
+func (v sqlVisitor) VisitEnd(n sql.Node) (sql.Node, error) {
+ return n, nil
+}