chore: migrate mail themes to shared framework and locked upstream inputs
Release / Validate Templates (push) Canceled after 0s
Release / Package & Release (push) Canceled after 0s
Release / Update Latest Release Documentation (push) Canceled after 0s

This commit is contained in:
KenanZhu committed 2026-10-09 18:34:36 +08:00
1 parent 5c0589f6f6
commit fec3ace600
225 files changed
+4718 -15807

No files matched your search

+148
View File
@@ -0,0 +1,148 @@
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
}