149 lines
3.5 KiB
Go
149 lines
3.5 KiB
Go
package upstream
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"text/template"
|
|
"text/template/parse"
|
|
)
|
|
|
|
// ReferencedKeys walks Go's template AST, including nested pipelines and both
|
|
// TrN keys. Dynamic translation keys require an explicit adapter review.
|
|
func ReferencedKeys(s *Snapshot) ([]string, error) {
|
|
fns := template.FuncMap{}
|
|
for _, name := range []string{"AppUrl", "AppName", "AppDomain", "DotEscape", "QueryEscape", "ShortSha", "HTMLFormat", "PathEscapeSegments", "FormatByteSize", "Dict", "dict", "StringUtils", "SliceUtils", "JsonUtils", "QueryBuild", "PathEscape", "SanitizeHTML"} {
|
|
fns[name] = func(...any) string { return "" }
|
|
}
|
|
keys := map[string]bool{}
|
|
var walk func(parse.Node) error
|
|
walk = func(n parse.Node) error {
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
switch n := n.(type) {
|
|
case *parse.ListNode:
|
|
if n != nil {
|
|
for _, child := range n.Nodes {
|
|
if err := walk(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
case *parse.ActionNode:
|
|
return walk(n.Pipe)
|
|
case *parse.PipeNode:
|
|
if n == nil {
|
|
return nil
|
|
}
|
|
for _, cmd := range n.Cmds {
|
|
if err := walk(cmd); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case *parse.CommandNode:
|
|
if len(n.Args) > 0 {
|
|
var ident []string
|
|
switch f := n.Args[0].(type) {
|
|
case *parse.FieldNode:
|
|
ident = f.Ident
|
|
case *parse.VariableNode:
|
|
ident = f.Ident
|
|
}
|
|
if len(ident) >= 2 && ident[len(ident)-2] == "locale" {
|
|
var positions []int
|
|
switch ident[len(ident)-1] {
|
|
case "Tr", "TrString":
|
|
positions = []int{1}
|
|
case "TrN":
|
|
positions = []int{2, 3}
|
|
}
|
|
for _, i := range positions {
|
|
if i >= len(n.Args) {
|
|
return fmt.Errorf("invalid translation call: %s", n)
|
|
}
|
|
key, ok := n.Args[i].(*parse.StringNode)
|
|
if !ok {
|
|
return fmt.Errorf("dynamic translation key needs review: %s", n)
|
|
}
|
|
keys[key.Text] = true
|
|
}
|
|
}
|
|
}
|
|
for _, arg := range n.Args {
|
|
if err := walk(arg); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case *parse.IfNode:
|
|
for _, child := range []parse.Node{n.Pipe, n.List, n.ElseList} {
|
|
if err := walk(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case *parse.RangeNode:
|
|
for _, child := range []parse.Node{n.Pipe, n.List, n.ElseList} {
|
|
if err := walk(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case *parse.WithNode:
|
|
for _, child := range []parse.Node{n.Pipe, n.List, n.ElseList} {
|
|
if err := walk(child); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
case *parse.TemplateNode:
|
|
return walk(n.Pipe)
|
|
}
|
|
return nil
|
|
}
|
|
for p, data := range s.Templates {
|
|
t, err := template.New(p).Funcs(fns).Parse(string(data))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%s: %w", p, err)
|
|
}
|
|
for _, t := range t.Templates() {
|
|
if t.Tree != nil {
|
|
if err := walk(t.Tree.Root); err != nil {
|
|
return nil, fmt.Errorf("%s: %w", p, err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var result []string
|
|
for k := range keys {
|
|
result = append(result, k)
|
|
}
|
|
sort.Strings(result)
|
|
return result, nil
|
|
}
|
|
|
|
func ValidateKeys(s *Snapshot) error {
|
|
keys, err := ReferencedKeys(s)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, k := range keys {
|
|
if s.Locales["en-US"][k] == "" {
|
|
return fmt.Errorf("official English catalog is missing referenced key %q", k)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func MissingKeys(s *Snapshot) (map[string][]string, error) {
|
|
keys, err := ReferencedKeys(s)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
missing := map[string][]string{}
|
|
for lang, catalog := range s.Locales {
|
|
for _, k := range keys {
|
|
if catalog[k] == "" {
|
|
missing[lang] = append(missing[lang], k)
|
|
}
|
|
}
|
|
}
|
|
return missing, nil
|
|
}
|