From eefb0bfd01c487f0f272d4b479799deadc231474 Mon Sep 17 00:00:00 2001 From: Thomas Voss Date: Wed, 16 Jul 2025 22:05:02 +0200 Subject: Update exttmpl for new translation system --- cmd/exttmpl/main.go | 341 +++++++++++++++++++++++++++++----------------------- 1 file changed, 188 insertions(+), 153 deletions(-) (limited to 'cmd') diff --git a/cmd/exttmpl/main.go b/cmd/exttmpl/main.go index 332d9d5..9211a88 100644 --- a/cmd/exttmpl/main.go +++ b/cmd/exttmpl/main.go @@ -1,204 +1,241 @@ package main import ( + "bytes" + "flag" "fmt" + "io" "os" "path/filepath" "slices" "strings" "text/template/parse" - - "golang.org/x/text/language" - "golang.org/x/text/message/pipeline" - "golang.org/x/tools/go/packages" ) -const ( - pkgbase = "git.thomasvoss.com/euro-cash.eu" - srclang = "en" - srcdir = "./src" - transdir = srcdir + "/rosetta" - outfile = "catalog.gen.go" - transfn = "T" +type config struct { + arg int + plural int + context int + domain int +} + +type translation struct { + msgid string + msgidPlural string + msgctx string + domain string +} + +type transinfo struct { + comment string + locs []loc +} + +type loc struct { + file string + line int +} + +func (l loc) String() string { + return fmt.Sprintf("%s:%d", l.file, l.line) +} + +var ( + outfile *os.File + currentFile []byte + currentPath string + lastComment string + translations map[translation]transinfo = make(map[translation]transinfo) + configs = map[string]config{ + "Get": {1, -1, -1, -1}, + "GetC": {1, -1, 2, -1}, + "GetD": {2, -1, -1, 1}, + "GetDC": {2, -1, 3, 1}, + "GetN": {1, 2, -1, -1}, + "GetNC": {1, 2, 4, -1}, + "GetND": {2, 3, -1, 1}, + "GetNDC": {2, 3, 5, 1}, + } ) +func usage() { + fmt.Fprintf(os.Stderr, "Usage: %s [flags] template...\n", + filepath.Base(os.Args[0])) + flag.PrintDefaults() +} + func main() { - /* cd to the project root directory */ try(os.Chdir(filepath.Dir(os.Args[0]))) - pkgnames := packageList(".") + outpath := flag.String("out", "-", "output file") + flag.Usage = usage + flag.Parse() - var paths []string - pkgs := try2(packages.Load(&packages.Config{ - Mode: packages.NeedFiles | packages.NeedEmbedFiles, - }, pkgnames...)) + if flag.NArg() < 1 { + flag.Usage() + os.Exit(1) + } - for _, pkg := range pkgs { - if len(pkg.Errors) != 0 { - for _, err := range pkg.Errors { - warn(err.Msg) - } - os.Exit(1) + if *outpath == "-" { + outfile = os.Stdout + } else { + outfile = try2(os.Create(*outpath)) + } + + for _, f := range flag.Args() { + process(f) + } + + for tl, ti := range translations { + if ti.comment != "" { + fmt.Fprintln(outfile, "#.", ti.comment) } - for _, f := range pkg.EmbedFiles { - if filepath.Ext(f) == ".tmpl" { - paths = append(paths, f) + + slices.SortFunc(ti.locs, func(a, b loc) int { + if x := strings.Compare(a.file, b.file); x != 0 { + return x } + return a.line - b.line + }) + for _, x := range ti.locs { + fmt.Fprintln(outfile, "#:", x) } - } - msgs := make([]pipeline.Message, 0, 1024) - for _, path := range paths { - f := try2(os.ReadFile(path)) - trees := make(map[string]*parse.Tree) - t := parse.New("name") - t.Mode |= parse.SkipFuncCheck - try2(t.Parse(string(f), "", "", trees)) - for _, t := range trees { - process(&msgs, t.Root) + if tl.msgctx != "" { + writeField(outfile, "msgctx", tl.msgctx) } + writeField(outfile, "msgid", tl.msgid) + if tl.msgidPlural != "" { + writeField(outfile, "msgid_plural", tl.msgidPlural) + writeField(outfile, "msgstr[0]", "") + writeField(outfile, "msgstr[1]", "") + } else { + writeField(outfile, "msgstr", "") + } + fmt.Fprint(outfile, "\n") } +} - pconf := &pipeline.Config{ - Supported: languages(), - SourceLanguage: language.Make(srclang), - Packages: pkgnames, - Dir: transdir, - GenFile: outfile, - GenPackage: srcdir, +func process(path string) { + currentPath = path + currentFile = try2(os.ReadFile(path)) + trees := make(map[string]*parse.Tree) + t := parse.New("name") + t.Mode |= parse.ParseComments | parse.SkipFuncCheck + try2(t.Parse(string(currentFile), "", "", trees)) + for _, t := range trees { + processNode(t.Root) } - - state := try2(pipeline.Extract(pconf)) - state.Extracted.Messages = append(state.Extracted.Messages, msgs...) - - try(state.Import()) - try(state.Merge()) - try(state.Export()) - try(state.Generate()) } -func process(tmplMsgs *[]pipeline.Message, node parse.Node) { - switch node.Type() { - case parse.NodeList: - if ln, ok := node.(*parse.ListNode); ok { - for _, n := range ln.Nodes { - process(tmplMsgs, n) - } +func processNode(node parse.Node) { + switch n := node.(type) { + case *parse.ListNode: + for _, m := range n.Nodes { + processNode(m) } - case parse.NodeIf: - if in, ok := node.(*parse.IfNode); ok { - process(tmplMsgs, in.List) - if in.ElseList != nil { - process(tmplMsgs, in.ElseList) + case *parse.IfNode: + processBranch(n.BranchNode) + case *parse.RangeNode: + processBranch(n.BranchNode) + case *parse.WithNode: + processBranch(n.BranchNode) + case *parse.ActionNode: + for _, cmd := range n.Pipe.Cmds { + if len(cmd.Args) == 0 { + continue } - } - case parse.NodeWith: - if wn, ok := node.(*parse.WithNode); ok { - process(tmplMsgs, wn.List) - if wn.ElseList != nil { - process(tmplMsgs, wn.ElseList) + + f, ok := cmd.Args[0].(*parse.FieldNode) + if !ok || len(f.Ident) == 0 { + continue } - } - case parse.NodeRange: - if rn, ok := node.(*parse.RangeNode); ok { - process(tmplMsgs, rn.List) - if rn.ElseList != nil { - process(tmplMsgs, rn.ElseList) + + cfg, ok := configs[f.Ident[len(f.Ident)-1]] + if !ok { + continue } - } - case parse.NodeAction: - an, ok := node.(*parse.ActionNode) - if !ok { - break - } - for _, cmd := range an.Pipe.Cmds { - if !hasIdent(cmd, transfn) { + var ( + tl translation + linenr int + ) + + if sn, ok := cmd.Args[cfg.arg].(*parse.StringNode); ok { + tl.msgid = sn.Text + linenr = getlinenr(sn.Pos.Position()) + } else { continue } - for _, arg := range cmd.Args { - if arg.Type() != parse.NodeString { - continue + if cfg.plural != -1 { + if sn, ok := cmd.Args[cfg.plural].(*parse.StringNode); ok { + tl.msgidPlural = sn.Text } - if sn, ok := arg.(*parse.StringNode); ok { - txt := collapse(sn.Text) - *tmplMsgs = append(*tmplMsgs, pipeline.Message{ - ID: pipeline.IDList{txt}, - Key: txt, - Message: pipeline.Text{Msg: txt}, - }) - break + } + if cfg.context != -1 { + if sn, ok := cmd.Args[cfg.context].(*parse.StringNode); ok { + tl.msgctx = sn.Text } } - } - } -} -func hasIdent(cmd *parse.CommandNode, s string) bool { - if len(cmd.Args) == 0 { - return false - } - arg := cmd.Args[0] - var idents []string - switch arg.Type() { - case parse.NodeField: - idents = arg.(*parse.FieldNode).Ident - case parse.NodeVariable: - idents = arg.(*parse.VariableNode).Ident - } - return slices.Contains(idents, s) -} - -func packageList(path string) []string { - ents := try2(os.ReadDir(path)) - xs := make([]string, 0, len(ents)) - foundOne := false - for _, ent := range ents { - switch { - case filepath.Ext(ent.Name()) == ".go": - if !foundOne { - xs = append(xs, pkgbase+"/"+path) - foundOne = true + ti := translations[tl] + if lastComment != "" { + ti.comment = lastComment + lastComment = "" } - case !ent.IsDir(), ent.Name() == "cmd", ent.Name() == "vendor": - continue - default: - xs = append(xs, packageList(path+"/"+ent.Name())...) + /* FIXME: Add filename and line number */ + 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]) } } - return xs } -func languages() []language.Tag { - ents := try2(os.ReadDir(transdir)) - tags := make([]language.Tag, len(ents)) - for i, e := range ents { - tags[i] = language.MustParse(e.Name()) +func processBranch(n parse.BranchNode) { + processNode(n.List) + if n.ElseList != nil { + processNode(n.ElseList) } - return tags } -func collapse(s string) string { - var ( - sb strings.Builder - prev bool - ) - const spc = " \t\n" +func writeField(w io.Writer, pfx, s string) { + fmt.Fprintf(w, "%s ", pfx) + if strings.ContainsRune(s, '\n') { + fmt.Fprintln(w, "\"\"") + lines := strings.SplitAfter(s, "\n") + n := len(lines) + if n > 1 && lines[n-1] == "" { + lines = lines[:n-1] + } + for _, ss := range lines { + writeLine(w, ss) + } + fmt.Fprintln(w, "\"\"") + } else { + writeLine(w, s) + } +} - for _, ch := range strings.Trim(s, spc) { - if strings.ContainsRune(spc, ch) { - if prev { - continue - } - ch = ' ' - prev = true - } else { - prev = false +func writeLine(w io.Writer, s string) { + fmt.Fprint(w, "\"") + for _, c := range s { + switch c { + case '\\', '"': + fmt.Fprintf(w, "\\%c", c) + case '\n': + fmt.Fprint(w, "\\n") + default: + fmt.Fprintf(w, "%c", c) } - sb.WriteRune(ch) } + fmt.Fprintln(w, "\"") +} - return sb.String() +func getlinenr(p parse.Pos) int { + return bytes.Count(currentFile[:p], []byte{'\n'}) + 1 } func try(err error) { @@ -208,9 +245,7 @@ func try(err error) { } func try2[T any](val T, err error) T { - if err != nil { - die(err) - } + try(err) return val } -- cgit v1.2.3