diff --git a/context.go b/context.go index 3bc77ef..a220dbc 100644 --- a/context.go +++ b/context.go @@ -1,6 +1,12 @@ package html -import i18n "dappco.re/go/core/i18n" +// Translator provides Text() lookups for a rendering context. +// +// The default server build uses go-i18n. Alternate builds, including WASM, +// can provide any implementation with the same T() method. +type Translator interface { + T(key string, args ...any) string +} // Context carries rendering state through the node tree. type Context struct { @@ -8,7 +14,7 @@ type Context struct { Locale string Entitlements func(feature string) bool Data map[string]any - service *i18n.Service + service Translator } // NewContext creates a new rendering context with sensible defaults. @@ -18,8 +24,8 @@ func NewContext() *Context { } } -// NewContextWithService creates a rendering context backed by a specific i18n service. -func NewContextWithService(svc *i18n.Service) *Context { +// NewContextWithService creates a rendering context backed by a specific translator. +func NewContextWithService(svc Translator) *Context { return &Context{ Data: make(map[string]any), service: svc, diff --git a/docs/architecture.md b/docs/architecture.md index a78bad6..7ecd934 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -23,7 +23,7 @@ All concrete node types are unexported structs with exported constructor functio |-------------|-----------| | `El(tag, ...Node)` | HTML element with children. Void elements (`br`, `img`, `input`, etc.) never emit a closing tag. | | `Attr(Node, key, value)` | Sets an attribute on an `El` node. Traverses through `If`, `Unless`, and `Entitled` wrappers. Returns the node for chaining. | -| `Text(key, ...any)` | Translated text via `go-i18n`. Output is always HTML-escaped. | +| `Text(key, ...any)` | Translated text via the active context translator. Server builds fall back to global `go-i18n`; JS builds fall back to the key. Output is always HTML-escaped. | | `Raw(content)` | Unescaped trusted content. Explicit escape hatch. | | `If(cond, Node)` | Renders the child only when the condition function returns true. | | `Unless(cond, Node)` | Renders the child only when the condition function returns false. | @@ -50,16 +50,16 @@ type Context struct { Locale string // BCP 47 locale string Entitlements func(feature string) bool // feature gate callback Data map[string]any // arbitrary per-request data - service *i18n.Service // unexported; set via constructor + service Translator // unexported; set via constructor } ``` Two constructors are provided: - `NewContext()` creates a context with sensible defaults and an empty `Data` map. -- `NewContextWithService(svc)` creates a context backed by a specific `i18n.Service` instance. +- `NewContextWithService(svc)` creates a context backed by any translator implementing `T(key, ...any) string` such as `*i18n.Service`. -The `service` field is intentionally unexported. When nil, `Text` nodes fall back to the global `i18n.T()` default. This prevents callers from setting the service inconsistently after construction. +The `service` field is intentionally unexported. When nil, server builds fall back to the global `i18n.T()` default while JS builds render the key unchanged. This prevents callers from setting the service inconsistently after construction while keeping the WASM import graph lean. ## HLCRF Layout diff --git a/docs/development.md b/docs/development.md index c5c6838..55af80b 100644 --- a/docs/development.md +++ b/docs/development.md @@ -291,6 +291,6 @@ func TestGenerateClass_ValidTag(t *testing.T) { - `NewLayout("XYZ")` silently produces empty output for unrecognised slot letters. Valid letters are `H`, `L`, `C`, `R`, `F`. There is no error or warning. - `Responsive.Variant()` accepts only `*Layout`, not arbitrary `Node` values. Arbitrary subtrees must be wrapped in a single-slot layout first. -- `Context.service` is unexported. Custom i18n service injection requires `NewContextWithService()`. There is no way to swap the service after construction. +- `Context.service` is unexported. Custom translation injection requires `NewContextWithService()`. There is no way to swap the translator after construction. - The WASM module has no integration test for the JavaScript exports. `size_test.go` tests binary size only; it does not exercise `renderToString` behaviour from JavaScript. - `codegen.GenerateBundle()` iterates a `map`, so the order of class definitions in the output is non-deterministic. This does not affect correctness but may cause cosmetic diffs between runs. diff --git a/node.go b/node.go index f47ee36..d5d6be2 100644 --- a/node.go +++ b/node.go @@ -6,8 +6,6 @@ import ( "maps" "slices" "strings" - - i18n "dappco.re/go/core/i18n" ) // Node is anything renderable. @@ -152,13 +150,7 @@ func Text(key string, args ...any) Node { } 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) + return escapeHTML(translateText(ctx, n.key, n.args...)) } // --- ifNode --- diff --git a/text_translate.go b/text_translate.go new file mode 100644 index 0000000..4e3ee8f --- /dev/null +++ b/text_translate.go @@ -0,0 +1,11 @@ +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +func translateText(ctx *Context, key string, args ...any) string { + if ctx != nil && ctx.service != nil { + return ctx.service.T(key, args...) + } + + return translateDefault(key, args...) +} diff --git a/text_translate_default.go b/text_translate_default.go new file mode 100644 index 0000000..3bb280c --- /dev/null +++ b/text_translate_default.go @@ -0,0 +1,11 @@ +//go:build !js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +import i18n "dappco.re/go/core/i18n" + +func translateDefault(key string, args ...any) string { + return i18n.T(key, args...) +} diff --git a/text_translate_js.go b/text_translate_js.go new file mode 100644 index 0000000..692e4c9 --- /dev/null +++ b/text_translate_js.go @@ -0,0 +1,9 @@ +//go:build js + +// SPDX-Licence-Identifier: EUPL-1.2 + +package html + +func translateDefault(key string, _ ...any) string { + return key +}