diff --git a/context.go b/context.go
index a74aeb5..19cddfb 100644
--- a/context.go
+++ b/context.go
@@ -1,10 +1,14 @@
package html
+import i18n "forge.lthn.ai/core/go-i18n"
+
// Context carries rendering state through the node tree.
type Context struct {
- Identity string
- Locale string
- Data map[string]any
+ Identity string
+ Locale string
+ Entitlements func(feature string) bool
+ Data map[string]any
+ service *i18n.Service
}
// NewContext creates a new rendering context with sensible defaults.
@@ -13,3 +17,11 @@ func NewContext() *Context {
Data: make(map[string]any),
}
}
+
+// NewContextWithService creates a rendering context backed by a specific i18n service.
+func NewContextWithService(svc *i18n.Service) *Context {
+ return &Context{
+ Data: make(map[string]any),
+ service: svc,
+ }
+}
diff --git a/go.mod b/go.mod
index e20be72..01fbda6 100644
--- a/go.mod
+++ b/go.mod
@@ -3,3 +3,7 @@ module forge.lthn.ai/core/go-html
go 1.25.5
replace forge.lthn.ai/core/go-i18n => ../go-i18n
+
+require forge.lthn.ai/core/go-i18n v0.0.0-00010101000000-000000000000
+
+require golang.org/x/text v0.33.0 // indirect
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..62a1313
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,2 @@
+golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE=
+golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8=
diff --git a/node.go b/node.go
index f4599d3..611a2d9 100644
--- a/node.go
+++ b/node.go
@@ -1,6 +1,10 @@
package html
-import "strings"
+import (
+ "strings"
+
+ i18n "forge.lthn.ai/core/go-i18n"
+)
// Node is anything renderable.
type Node interface {
@@ -96,3 +100,38 @@ func (n *elNode) Render(ctx *Context) string {
return b.String()
}
+
+// --- escapeHTML ---
+
+// escapeHTML escapes a string for safe inclusion in HTML text content.
+func escapeHTML(s string) string {
+ s = strings.ReplaceAll(s, "&", "&")
+ s = strings.ReplaceAll(s, "<", "<")
+ s = strings.ReplaceAll(s, ">", ">")
+ s = strings.ReplaceAll(s, "\"", """)
+ s = strings.ReplaceAll(s, "'", "'")
+ return s
+}
+
+// --- textNode ---
+
+type textNode struct {
+ key string
+ args []any
+}
+
+// Text creates a node that renders through the go-i18n grammar pipeline.
+// Output is HTML-escaped by default. Safe-by-default path.
+func Text(key string, args ...any) Node {
+ return &textNode{key: key, args: args}
+}
+
+func (n *textNode) Render(ctx *Context) string {
+ var text string
+ if ctx != nil && ctx.service != nil {
+ text = ctx.service.T(n.key, n.args...)
+ } else {
+ text = i18n.T(n.key, n.args...)
+ }
+ return escapeHTML(text)
+}
diff --git a/node_test.go b/node_test.go
index aab8d72..8e54e23 100644
--- a/node_test.go
+++ b/node_test.go
@@ -1,6 +1,9 @@
package html
-import "testing"
+import (
+ "strings"
+ "testing"
+)
func TestRawNode_Render(t *testing.T) {
ctx := NewContext()
@@ -50,3 +53,24 @@ func TestElNode_VoidElement(t *testing.T) {
t.Errorf("El(\"br\").Render() = %q, want %q", got, want)
}
}
+
+func TestTextNode_Render(t *testing.T) {
+ ctx := NewContext()
+ node := Text("hello")
+ got := node.Render(ctx)
+ if got != "hello" {
+ t.Errorf("Text(\"hello\").Render() = %q, want %q", got, "hello")
+ }
+}
+
+func TestTextNode_Escapes(t *testing.T) {
+ ctx := NewContext()
+ node := Text("")
+ got := node.Render(ctx)
+ if strings.Contains(got, "