feat: release v0.26 portable i18n and presentation adapters

This commit is contained in:
2026-09-29 13:31:42 +08:00
parent 6a95cad184
commit d1a747c36f
75 changed files with 5681 additions and 52 deletions

122
i18n/i18ntool/extract.go Normal file
View File

@@ -0,0 +1,122 @@
// Package i18ntool provides conservative, syntax-only Go key extraction.
package i18ntool
import (
"go/ast"
"go/parser"
"go/token"
"strconv"
)
type Reference struct {
Namespace string `json:"namespace"`
Key string `json:"key"`
File string `json:"file"`
Line int `json:"line"`
}
type Finding struct {
File string `json:"file"`
Line int `json:"line"`
Problem string `json:"problem"`
}
type Extraction struct {
References []Reference `json:"references"`
Findings []Finding `json:"findings"`
}
// ExtractGo reads literal i18n.Ref expressions, including import aliases. It
// neither executes project code nor claims to resolve dynamic keys or wrappers.
// Render/RenderContext calls without a literal reference are reported for review.
func ExtractGo(filename string, source []byte) (Extraction, error) {
out := Extraction{References: []Reference{}, Findings: []Finding{}}
fs := token.NewFileSet()
file, err := parser.ParseFile(fs, filename, source, 0)
if err != nil {
return out, err
}
aliases := map[string]bool{}
for _, imp := range file.Imports {
path, _ := strconv.Unquote(imp.Path.Value)
if path == "git.yuntuo.hk/k2safe/go-omni/i18n" {
name := "i18n"
if imp.Name != nil {
name = imp.Name.Name
}
if name == "." {
out.Findings = append(out.Findings, Finding{filename, fs.Position(imp.Pos()).Line, "dot import requires manual extraction"})
} else {
aliases[name] = true
}
}
}
if len(aliases) == 0 {
return out, nil
}
ast.Inspect(file, func(n ast.Node) bool {
lit, ok := n.(*ast.CompositeLit)
if !ok {
return true
}
sel, ok := lit.Type.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Ref" {
return true
}
pkg, ok := sel.X.(*ast.Ident)
if !ok || !aliases[pkg.Name] {
return true
}
values := map[string]string{}
valid := true
for i, el := range lit.Elts {
key := ""
value := el
if kv, ok := el.(*ast.KeyValueExpr); ok {
id, ok := kv.Key.(*ast.Ident)
if !ok {
valid = false
continue
}
key = id.Name
value = kv.Value
} else if i == 0 {
key = "Namespace"
} else if i == 1 {
key = "Key"
} else {
valid = false
}
s, ok := value.(*ast.BasicLit)
if !ok || s.Kind != token.STRING {
valid = false
continue
}
v, e := strconv.Unquote(s.Value)
if e != nil {
valid = false
}
values[key] = v
}
line := fs.Position(lit.Pos()).Line
if valid && values["Namespace"] != "" && values["Key"] != "" {
out.References = append(out.References, Reference{values["Namespace"], values["Key"], filename, line})
} else {
out.Findings = append(out.Findings, Finding{filename, line, "dynamic or incomplete message reference"})
}
return true
})
ast.Inspect(file, func(n ast.Node) bool {
call, ok := n.(*ast.CallExpr)
if !ok {
return true
}
sel, ok := call.Fun.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Render" && sel.Sel.Name != "RenderContext" || len(call.Args) != 3 {
return true
}
if _, ok := call.Args[1].(*ast.CompositeLit); !ok {
out.Findings = append(out.Findings, Finding{filename, fs.Position(call.Pos()).Line, "nonliteral Render reference; verify manually (syntax-only extraction)"})
}
return true
})
return out, nil
}