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 }