Repository: gohugoio/hugo (language: go)

Issue:
TOML dates no longer support equality tests against `now`
I recently upgraded from an old version (in the 0.50's I think) and have noticed a problem with a template that does a filter against some data that's in TOML using dates.

I have items such as the following:

```toml
{ date = 2022-06-20, name = "End to end GraphQL (workshop)", link = "https://ndcmelbourne.com/agenda/end-to-end-graphql/daba56ef9e98", event = "NDC Melbourne 2022" },
```

And use it in a template like so:

```html
{{ if .Site.Params.speaking.enable }} {{ with .Site.Params.speaking }}
<section id="speaking" class="container">
    <div>
        <h2>{{ .title }}</h2>
        {{ $talks := where .talks "date" ">" now }} {{ if eq (len $talks) 0 }}
        <p>{{ .noTalks }}</p>
        {{ else }}
        <ul class="talk-list">
            {{ range sort $talks "date" "asc" }}
            <li>
                {{ partial "talk" . }}
            </li>
            {{ end }}
        </ul>
        {{ end }}
    </div>
    <div>
        <p>{{ .content }}</p>
        <h3>
            <a href="/talks/" title="Available Talks">My Available Talks</a>
        </h3>
    </div>
</section>
{{ end }} {{ end }}
```

Upon upgrading to the latest version, the `where` condition no longer works, I'm assuming because the two types (the TOML date and `now`) can no longer be compared.

Repo containing the full code is here: https://github.com/aaronpowell/aaronpowell.github.io

Relevant files are:
- TOML: https://github.com/aaronpowell/aaronpowell.github.io/blob/main/src/config.speaking.toml
- Template: https://github.com/aaronpowell/aaronpowell.github.io/blob/main/src/themes/aaronpowell/layouts/partials/speaking.html

### What version of Hugo are you using (`hugo version`)?

<pre>
$ hugo version
hugo v0.100.1-0afb4866e345d31cbbcbab4349e43f1d36122806+extended linux/amd64 BuildDate=2022-06-01T10:11:48Z VendorInfo=gohugoio
</pre>

### Does this issue reproduce with the latest release?

Yes.


Discussion / hints:
We have a similar issue in #9949 -- which is harder to fix (but that one has a good workaround).

This one, though, is fixable. Thanks for the report and sorry about the site breakage.

Fix patch:
diff --git a/common/hreflect/helpers.go b/common/hreflect/helpers.go
index e1c01456d4f..1b7e5acf722 100644
--- a/common/hreflect/helpers.go
+++ b/common/hreflect/helpers.go
@@ -20,7 +20,9 @@ import (
 	"context"
 	"reflect"
 	"sync"
+	"time"
 
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/types"
 )
 
@@ -168,6 +170,44 @@ func GetMethodIndexByName(tp reflect.Type, name string) int {
 	return m.Index
 }
 
+var (
+	timeType           = reflect.TypeOf((*time.Time)(nil)).Elem()
+	asTimeProviderType = reflect.TypeOf((*htime.AsTimeProvider)(nil)).Elem()
+)
+
+// IsTime returns whether tp is a time.Time type or if it can be converted into one
+// in ToTime.
+func IsTime(tp reflect.Type) bool {
+	if tp == timeType {
+		return true
+	}
+
+	if tp.Implements(asTimeProviderType) {
+		return true
+	}
+	return false
+}
+
+// AsTime returns v as a time.Time if possible.
+// The given location is only used if the value implements AsTimeProvider (e.g. go-toml local).
+// A zero Time and false is returned if this isn't possible.
+// Note that this function does not accept string dates.
+func AsTime(v reflect.Value, loc *time.Location) (time.Time, bool) {
+	if v.Kind() == reflect.Interface {
+		return AsTime(v.Elem(), loc)
+	}
+
+	if v.Type() == timeType {
+		return v.Interface().(time.Time), true
+	}
+
+	if v.Type().Implements(asTimeProviderType) {
+		return v.Interface().(htime.AsTimeProvider).AsTime(loc), true
+	}
+
+	return time.Time{}, false
+}
+
 // Based on: https://github.com/golang/go/blob/178a2c42254166cffed1b25fb1d3c7a5727cada6/src/text/template/exec.go#L931
 func indirectInterface(v reflect.Value) reflect.Value {
 	if v.Kind() != reflect.Interface {
diff --git a/common/htime/time.go b/common/htime/time.go
index 052a45ed1dd..d30ecf7e118 100644
--- a/common/htime/time.go
+++ b/common/htime/time.go
@@ -20,8 +20,6 @@ import (
 	"github.com/bep/clock"
 	"github.com/spf13/cast"
 
-	toml "github.com/pelletier/go-toml/v2"
-
 	"github.com/gohugoio/locales"
 )
 
@@ -139,13 +137,12 @@ func (f TimeFormatter) Format(t time.Time, layout string) string {
 
 func ToTimeInDefaultLocationE(i any, location *time.Location) (tim time.Time, err error) {
 	switch vv := i.(type) {
-	case toml.LocalDate:
-		return vv.AsTime(location), nil
-	case toml.LocalDateTime:
+	case AsTimeProvider:
 		return vv.AsTime(location), nil
 	// issue #8895
 	// datetimes parsed by `go-toml` have empty zone name
 	// convert back them into string and use `cast`
+	// TODO(bep) add tests, make sure we really need this.
 	case time.Time:
 		i = vv.Format(time.RFC3339)
 	}
@@ -161,3 +158,8 @@ func Now() time.Time {
 func Since(t time.Time) time.Duration {
 	return Clock.Since(t)
 }
+
+// AsTimeProvider is implemented by go-toml's LocalDate and LocalDateTime.
+type AsTimeProvider interface {
+	AsTime(zone *time.Location) time.Time
+}
diff --git a/tpl/collections/collections.go b/tpl/collections/collections.go
index 8fdc8527586..299a504f415 100644
--- a/tpl/collections/collections.go
+++ b/tpl/collections/collections.go
@@ -31,6 +31,8 @@ import (
 	"github.com/gohugoio/hugo/common/types"
 	"github.com/gohugoio/hugo/deps"
 	"github.com/gohugoio/hugo/helpers"
+	"github.com/gohugoio/hugo/langs"
+	"github.com/gohugoio/hugo/tpl/compare"
 	"github.com/spf13/cast"
 )
 
@@ -41,14 +43,24 @@ func init() {
 
 // New returns a new instance of the collections-namespaced template functions.
 func New(deps *deps.Deps) *Namespace {
+	if deps.Language == nil {
+		panic("language must be set")
+	}
+
+	loc := langs.GetLocation(deps.Language)
+
 	return &Namespace{
-		deps: deps,
+		loc:      loc,
+		sortComp: compare.New(loc, true),
+		deps:     deps,
 	}
 }
 
 // Namespace provides template functions for the "collections" namespace.
 type Namespace struct {
-	deps *deps.Deps
+	loc      *time.Location
+	sortComp *compare.Namespace
+	deps     *deps.Deps
 }
 
 // After returns all the items after the first N in a rangeable list.
diff --git a/tpl/collections/reflect_helpers.go b/tpl/collections/reflect_helpers.go
index 4178850aa35..a295441ecdb 100644
--- a/tpl/collections/reflect_helpers.go
+++ b/tpl/collections/reflect_helpers.go
@@ -16,7 +16,6 @@ package collections
 import (
 	"fmt"
 	"reflect"
-	"time"
 
 	"errors"
 
@@ -26,7 +25,6 @@ import (
 var (
 	zero      reflect.Value
 	errorType = reflect.TypeOf((*error)(nil)).Elem()
-	timeType  = reflect.TypeOf((*time.Time)(nil)).Elem()
 )
 
 func numberToFloat(v reflect.Value) (float64, error) {
diff --git a/tpl/collections/sort.go b/tpl/collections/sort.go
index a0c2f815b19..ce76a4522ae 100644
--- a/tpl/collections/sort.go
+++ b/tpl/collections/sort.go
@@ -25,8 +25,6 @@ import (
 	"github.com/spf13/cast"
 )
 
-var sortComp = compare.New(true)
-
 // Sort returns a sorted sequence.
 func (ns *Namespace) Sort(seq any, args ...any) (any, error) {
 	if seq == nil {
@@ -51,7 +49,7 @@ func (ns *Namespace) Sort(seq any, args ...any) (any, error) {
 	collator := langs.GetCollator(ns.deps.Language)
 
 	// Create a list of pairs that will be used to do the sort
-	p := pairList{Collator: collator, SortAsc: true, SliceType: sliceType}
+	p := pairList{Collator: collator, sortComp: ns.sortComp, SortAsc: true, SliceType: sliceType}
 	p.Pairs = make([]pair, seqv.Len())
 
 	var sortByField string
@@ -145,6 +143,7 @@ type pair struct {
 // A slice of pairs that implements sort.Interface to sort by Value.
 type pairList struct {
 	Collator  *langs.Collator
+	sortComp  *compare.Namespace
 	Pairs     []pair
 	SortAsc   bool
 	SliceType reflect.Type
@@ -159,16 +158,16 @@ func (p pairList) Less(i, j int) bool {
 	if iv.IsValid() {
 		if jv.IsValid() {
 			// can only call Interface() on valid reflect Values
-			return sortComp.LtCollate(p.Collator, iv.Interface(), jv.Interface())
+			return p.sortComp.LtCollate(p.Collator, iv.Interface(), jv.Interface())
 		}
 
 		// if j is invalid, test i against i's zero value
-		return sortComp.LtCollate(p.Collator, iv.Interface(), reflect.Zero(iv.Type()))
+		return p.sortComp.LtCollate(p.Collator, iv.Interface(), reflect.Zero(iv.Type()))
 	}
 
 	if jv.IsValid() {
 		// if i is invalid, test j against j's zero value
-		return sortComp.LtCollate(p.Collator, reflect.Zero(jv.Type()), jv.Interface())
+		return p.sortComp.LtCollate(p.Collator, reflect.Zero(jv.Type()), jv.Interface())
 	}
 
 	return false
diff --git a/tpl/collections/where.go b/tpl/collections/where.go
index 8a9df445d02..2cf7227a00f 100644
--- a/tpl/collections/where.go
+++ b/tpl/collections/where.go
@@ -107,11 +107,10 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
 			fmv := mv.Float()
 			fmvp = &fmv
 		case reflect.Struct:
-			switch v.Type() {
-			case timeType:
-				iv := toTimeUnix(v)
+			if hreflect.IsTime(v.Type()) {
+				iv := ns.toTimeUnix(v)
 				ivp = &iv
-				imv := toTimeUnix(mv)
+				imv := ns.toTimeUnix(mv)
 				imvp = &imv
 			}
 		case reflect.Array, reflect.Slice:
@@ -167,12 +166,11 @@ func (ns *Namespace) checkCondition(v, mv reflect.Value, op string) (bool, error
 				}
 			}
 		case reflect.Struct:
-			switch v.Type() {
-			case timeType:
-				iv := toTimeUnix(v)
+			if hreflect.IsTime(v.Type()) {
+				iv := ns.toTimeUnix(v)
 				ivp = &iv
 				for i := 0; i < mv.Len(); i++ {
-					ima = append(ima, toTimeUnix(mv.Index(i)))
+					ima = append(ima, ns.toTimeUnix(mv.Index(i)))
 				}
 			}
 		case reflect.Array, reflect.Slice:
@@ -508,12 +506,10 @@ func toString(v reflect.Value) (string, error) {
 	return "", errors.New("unable to convert value to string")
 }
 
-func toTimeUnix(v reflect.Value) int64 {
-	if v.Kind() == reflect.Interface {
-		return toTimeUnix(v.Elem())
-	}
-	if v.Type() != timeType {
+func (ns *Namespace) toTimeUnix(v reflect.Value) int64 {
+	t, ok := hreflect.AsTime(v, ns.loc)
+	if !ok {
 		panic("coding error: argument must be time.Time type reflect Value")
 	}
-	return hreflect.GetMethodByName(v, "Unix").Call([]reflect.Value{})[0].Int()
+	return t.Unix()
 }
diff --git a/tpl/compare/compare.go b/tpl/compare/compare.go
index 9905003b23c..0b2d065aba7 100644
--- a/tpl/compare/compare.go
+++ b/tpl/compare/compare.go
@@ -23,16 +23,19 @@ import (
 	"github.com/gohugoio/hugo/compare"
 	"github.com/gohugoio/hugo/langs"
 
+	"github.com/gohugoio/hugo/common/hreflect"
+	"github.com/gohugoio/hugo/common/htime"
 	"github.com/gohugoio/hugo/common/types"
 )
 
 // New returns a new instance of the compare-namespaced template functions.
-func New(caseInsensitive bool) *Namespace {
-	return &Namespace{caseInsensitive: caseInsensitive}
+func New(loc *time.Location, caseInsensitive bool) *Namespace {
+	return &Namespace{loc: loc, caseInsensitive: caseInsensitive}
 }
 
 // Namespace provides template functions for the "compare" namespace.
 type Namespace struct {
+	loc *time.Location
 	// Enable to do case insensitive string compares.
 	caseInsensitive bool
 }
@@ -101,6 +104,11 @@ func (n *Namespace) Eq(first any, others ...any) bool {
 		if types.IsNil(v) {
 			return nil
 		}
+
+		if at, ok := v.(htime.AsTimeProvider); ok {
+			return at.AsTime(n.loc)
+		}
+
 		vv := reflect.ValueOf(v)
 		switch vv.Kind() {
 		case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
@@ -269,9 +277,8 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a
 			leftStr = &str
 		}
 	case reflect.Struct:
-		switch av.Type() {
-		case timeType:
-			left = float64(toTimeUnix(av))
+		if hreflect.IsTime(av.Type()) {
+			left = float64(ns.toTimeUnix(av))
 		}
 	case reflect.Bool:
 		left = 0
@@ -297,9 +304,8 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a
 			rightStr = &str
 		}
 	case reflect.Struct:
-		switch bv.Type() {
-		case timeType:
-			right = float64(toTimeUnix(bv))
+		if hreflect.IsTime(bv.Type()) {
+			right = float64(ns.toTimeUnix(bv))
 		}
 	case reflect.Bool:
 		right = 0
@@ -337,14 +343,10 @@ func (ns *Namespace) compareGetWithCollator(collator *langs.Collator, a any, b a
 	return left, right
 }
 
-var timeType = reflect.TypeOf((*time.Time)(nil)).Elem()
-
-func toTimeUnix(v reflect.Value) int64 {
-	if v.Kind() == reflect.Interface {
-		return toTimeUnix(v.Elem())
-	}
-	if v.Type() != timeType {
+func (ns *Namespace) toTimeUnix(v reflect.Value) int64 {
+	t, ok := hreflect.AsTime(v, ns.loc)
+	if !ok {
 		panic("coding error: argument must be time.Time type reflect Value")
 	}
-	return v.MethodByName("Unix").Call([]reflect.Value{})[0].Int()
+	return t.Unix()
 }
diff --git a/tpl/compare/init.go b/tpl/compare/init.go
index 2308b235e77..98c07f41b8d 100644
--- a/tpl/compare/init.go
+++ b/tpl/compare/init.go
@@ -15,6 +15,7 @@ package compare
 
 import (
 	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/tpl/internal"
 )
 
@@ -22,7 +23,11 @@ const name = "compare"
 
 func init() {
 	f := func(d *deps.Deps) *internal.TemplateFuncsNamespace {
-		ctx := New(false)
+		if d.Language == nil {
+			panic("language must be set")
+		}
+
+		ctx := New(langs.GetLocation(d.Language), false)
 
 		ns := &internal.TemplateFuncsNamespace{
 			Name:    name,


Test patch:
diff --git a/hugolib/dates_test.go b/hugolib/dates_test.go
index 4b4dc29d209..47629fb0ae2 100644
--- a/hugolib/dates_test.go
+++ b/hugolib/dates_test.go
@@ -214,3 +214,62 @@ func TestTimeOnError(t *testing.T) {
 	b.Assert(b.BuildE(BuildCfg{}), qt.Not(qt.IsNil))
 
 }
+
+func TestTOMLDates(t *testing.T) {
+	t.Parallel()
+
+	files := `
+-- config.toml --
+timeZone = "America/Los_Angeles"
+-- content/_index.md --
+---
+date: "2020-10-20"
+---
+-- content/p1.md --
++++
+title = "TOML Date with UTC offset"
+date = 2021-08-16T06:00:00+00:00
++++
+
+
+## Foo
+-- data/mydata.toml --
+date = 2020-10-20
+talks = [
+	{ date = 2017-01-23, name = "Past talk 1" },
+	{ date = 2017-01-24, name = "Past talk 2" },
+	{ date = 2017-01-26, name = "Past talk 3" },
+	{ date = 2050-02-12, name = "Future talk 1" },
+	{ date = 2050-02-13, name = "Future talk 2" },
+]
+-- layouts/index.html --
+{{ $futureTalks := where site.Data.mydata.talks "date" ">" now }}
+{{ $pastTalks := where site.Data.mydata.talks "date" "<" now }}
+
+{{ $homeDate := site.Home.Date }}
+{{ $p1Date := (site.GetPage "p1").Date }}
+Future talks: {{ len $futureTalks }}
+Past talks: {{ len $pastTalks }}
+
+Home's Date should be greater than past: {{ gt $homeDate (index $pastTalks 0).date }}
+Home's Date should be less than future: {{ lt $homeDate (index $futureTalks 0).date }}
+Home's Date should be equal mydata date: {{ eq $homeDate site.Data.mydata.date }}
+Full time: {{ $p1Date | time.Format ":time_full" }}
+`
+
+	b := NewIntegrationTestBuilder(
+		IntegrationTestConfig{
+			T:           t,
+			TxtarString: files,
+		},
+	).Build()
+
+	b.AssertFileContent("public/index.html", `
+Future talks: 2
+Past talks: 3
+Home's Date should be greater than past: true
+Home's Date should be less than future: true
+Home's Date should be equal mydata date: true
+Full time: 6:00:00 am UTC
+`)
+}
diff --git a/tpl/collections/append_test.go b/tpl/collections/append_test.go
index 02e46039fa9..232781522ff 100644
--- a/tpl/collections/append_test.go
+++ b/tpl/collections/append_test.go
@@ -18,7 +18,9 @@ import (
 	"testing"
 
 	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/langs"
 )
 
 // Also see tests in common/collection.
@@ -26,7 +28,7 @@ func TestAppend(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		start    any
diff --git a/tpl/collections/apply_test.go b/tpl/collections/apply_test.go
index 315f2bc85c5..2c7783fd910 100644
--- a/tpl/collections/apply_test.go
+++ b/tpl/collections/apply_test.go
@@ -21,7 +21,9 @@ import (
 	"testing"
 
 	qt "github.com/frankban/quicktest"
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/output"
 	"github.com/gohugoio/hugo/tpl"
 )
@@ -67,7 +69,7 @@ func (templateFinder) GetFunc(name string) (reflect.Value, bool) {
 func TestApply(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	d := &deps.Deps{}
+	d := &deps.Deps{Language: langs.NewDefaultLanguage(config.New())}
 	d.SetTmpl(new(templateFinder))
 	ns := New(d)
 
diff --git a/tpl/collections/collections_test.go b/tpl/collections/collections_test.go
index bcc2855c76a..fd78da6d4ea 100644
--- a/tpl/collections/collections_test.go
+++ b/tpl/collections/collections_test.go
@@ -40,7 +40,7 @@ func TestAfter(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		index  any
@@ -97,7 +97,7 @@ func (g *tstGrouper2) Group(key any, items any) (any, error) {
 func TestGroup(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		key    any
@@ -187,7 +187,7 @@ func TestDelimit(t *testing.T) {
 func TestDictionary(t *testing.T) {
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		values []any
@@ -226,7 +226,7 @@ func TestDictionary(t *testing.T) {
 func TestReverse(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	s := []string{"a", "b", "c"}
 	reversed, err := ns.Reverse(s)
@@ -245,7 +245,7 @@ func TestEchoParam(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		a      any
@@ -277,7 +277,7 @@ func TestFirst(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		limit  any
@@ -315,7 +315,7 @@ func TestIn(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		l1     any
@@ -391,7 +391,7 @@ func TestIntersect(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		l1, l2 any
@@ -518,7 +518,7 @@ func TestLast(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		limit  any
@@ -557,7 +557,7 @@ func TestLast(t *testing.T) {
 func TestQuerify(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		params []any
@@ -591,7 +591,7 @@ func TestQuerify(t *testing.T) {
 }
 
 func BenchmarkQuerify(b *testing.B) {
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 	params := []any{"a", "b", "c", "d", "f", " &"}
 
 	b.ResetTimer()
@@ -604,7 +604,7 @@ func BenchmarkQuerify(b *testing.B) {
 }
 
 func BenchmarkQuerifySlice(b *testing.B) {
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 	params := []string{"a", "b", "c", "d", "f", " &"}
 
 	b.ResetTimer()
@@ -619,7 +619,7 @@ func BenchmarkQuerifySlice(b *testing.B) {
 func TestSeq(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		args   []any
@@ -663,7 +663,7 @@ func TestSeq(t *testing.T) {
 func TestShuffle(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		seq     any
@@ -703,7 +703,7 @@ func TestShuffle(t *testing.T) {
 func TestShuffleRandomising(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	// Note that this test can fail with false negative result if the shuffle
 	// of the sequence happens to be the same as the original sequence. However
@@ -734,7 +734,7 @@ func TestShuffleRandomising(t *testing.T) {
 func TestSlice(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		args     []any
@@ -758,7 +758,7 @@ func TestUnion(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		l1     any
@@ -847,7 +847,7 @@ func TestUnion(t *testing.T) {
 func TestUniq(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 	for i, test := range []struct {
 		l      any
 		expect any
@@ -979,6 +979,7 @@ func newDeps(cfg config.Provider) *deps.Deps {
 		panic(err)
 	}
 	return &deps.Deps{
+		Language:    l,
 		Cfg:         cfg,
 		Fs:          hugofs.NewMem(l),
 		ContentSpec: cs,
diff --git a/tpl/collections/complement_test.go b/tpl/collections/complement_test.go
index 92565bf28a8..6c13ab5c4f1 100644
--- a/tpl/collections/complement_test.go
+++ b/tpl/collections/complement_test.go
@@ -17,7 +17,9 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/langs"
 
 	qt "github.com/frankban/quicktest"
 )
@@ -34,7 +36,7 @@ func TestComplement(t *testing.T) {
 
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	s1 := []TstX{{A: "a"}, {A: "b"}, {A: "d"}, {A: "e"}}
 	s2 := []TstX{{A: "b"}, {A: "e"}}
diff --git a/tpl/collections/index_test.go b/tpl/collections/index_test.go
index b9d5127ee7e..662fe6f3331 100644
--- a/tpl/collections/index_test.go
+++ b/tpl/collections/index_test.go
@@ -18,6 +18,8 @@ import (
 	"testing"
 
 	"github.com/gohugoio/hugo/common/maps"
+	"github.com/gohugoio/hugo/config"
+	"github.com/gohugoio/hugo/langs"
 
 	qt "github.com/frankban/quicktest"
 	"github.com/gohugoio/hugo/deps"
@@ -26,7 +28,7 @@ import (
 func TestIndex(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	for i, test := range []struct {
 		item    any
diff --git a/tpl/collections/merge_test.go b/tpl/collections/merge_test.go
index 14cca1ddddd..e7a38312653 100644
--- a/tpl/collections/merge_test.go
+++ b/tpl/collections/merge_test.go
@@ -19,7 +19,9 @@ import (
 	"testing"
 
 	"github.com/gohugoio/hugo/common/maps"
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/langs"
 	"github.com/gohugoio/hugo/parser"
 	"github.com/gohugoio/hugo/parser/metadecoders"
 
@@ -27,7 +29,7 @@ import (
 )
 
 func TestMerge(t *testing.T) {
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	simpleMap := map[string]any{"a": 1, "b": 2}
 
@@ -161,7 +163,7 @@ func TestMerge(t *testing.T) {
 
 func TestMergeDataFormats(t *testing.T) {
 	c := qt.New(t)
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	toml1 := `
 V1 = "v1_1"
diff --git a/tpl/collections/symdiff_test.go b/tpl/collections/symdiff_test.go
index c781ca04053..e5494d5a0ba 100644
--- a/tpl/collections/symdiff_test.go
+++ b/tpl/collections/symdiff_test.go
@@ -17,7 +17,9 @@ import (
 	"reflect"
 	"testing"
 
+	"github.com/gohugoio/hugo/config"
 	"github.com/gohugoio/hugo/deps"
+	"github.com/gohugoio/hugo/langs"
 
 	qt "github.com/frankban/quicktest"
 )
@@ -27,7 +29,7 @@ func TestSymDiff(t *testing.T) {
 
 	c := qt.New(t)
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	s1 := []TstX{{A: "a"}, {A: "b"}}
 	s2 := []TstX{{A: "a"}, {A: "e"}}
diff --git a/tpl/collections/where_test.go b/tpl/collections/where_test.go
index 395a90cb101..9a65de3d5a7 100644
--- a/tpl/collections/where_test.go
+++ b/tpl/collections/where_test.go
@@ -22,6 +22,8 @@ import (
 	"time"
 
 	"github.com/gohugoio/hugo/common/maps"
+	"github.com/gohugoio/hugo/config"
+	"github.com/gohugoio/hugo/langs"
 
 	"github.com/gohugoio/hugo/deps"
 )
@@ -29,7 +31,7 @@ import (
 func TestWhere(t *testing.T) {
 	t.Parallel()
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	type Mid struct {
 		Tst TstX
@@ -683,7 +685,7 @@ func TestWhere(t *testing.T) {
 func TestCheckCondition(t *testing.T) {
 	t.Parallel()
 
-	ns := New(&deps.Deps{})
+	ns := New(&deps.Deps{Language: langs.NewDefaultLanguage(config.New())})
 
 	type expect struct {
 		result  bool
diff --git a/tpl/compare/compare_test.go b/tpl/compare/compare_test.go
index 782941b15fb..ce2016b386e 100644
--- a/tpl/compare/compare_test.go
+++ b/tpl/compare/compare_test.go
@@ -88,7 +88,7 @@ func TestDefaultFunc(t *testing.T) {
 
 	then := time.Now()
 	now := time.Now()
-	ns := New(false)
+	ns := New(time.UTC, false)
 
 	for i, test := range []struct {
 		dflt   any
@@ -147,7 +147,7 @@ func TestDefaultFunc(t *testing.T) {
 func TestCompare(t *testing.T) {
 	t.Parallel()
 
-	n := New(false)
+	n := New(time.UTC, false)
 
 	twoEq := func(a, b any) bool {
 		return n.Eq(a, b)
@@ -269,7 +269,7 @@ func TestEqualExtend(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(false)
+	ns := New(time.UTC, false)
 
 	for _, test := range []struct {
 		first  any
@@ -294,7 +294,7 @@ func TestNotEqualExtend(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(false)
+	ns := New(time.UTC, false)
 
 	for _, test := range []struct {
 		first  any
@@ -314,7 +314,7 @@ func TestGreaterEqualExtend(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(false)
+	ns := New(time.UTC, false)
 
 	for _, test := range []struct {
 		first  any
@@ -335,7 +335,7 @@ func TestGreaterThanExtend(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(false)
+	ns := New(time.UTC, false)
 
 	for _, test := range []struct {
 		first  any
@@ -355,7 +355,7 @@ func TestLessEqualExtend(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(false)
+	ns := New(time.UTC, false)
 
 	for _, test := range []struct {
 		first  any
@@ -376,7 +376,7 @@ func TestLessThanExtend(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(false)
+	ns := New(time.UTC, false)
 
 	for _, test := range []struct {
 		first  any
@@ -395,7 +395,7 @@ func TestLessThanExtend(t *testing.T) {
 
 func TestCase(t *testing.T) {
 	c := qt.New(t)
-	n := New(false)
+	n := New(time.UTC, false)
 
 	c.Assert(n.Eq("az", "az"), qt.Equals, true)
 	c.Assert(n.Eq("az", stringType("az")), qt.Equals, true)
@@ -403,7 +403,7 @@ func TestCase(t *testing.T) {
 
 func TestStringType(t *testing.T) {
 	c := qt.New(t)
-	n := New(true)
+	n := New(time.UTC, true)
 
 	c.Assert(n.Lt("az", "Za"), qt.Equals, true)
 	c.Assert(n.Gt("ab", "Ab"), qt.Equals, true)
@@ -411,11 +411,12 @@ func TestStringType(t *testing.T) {
 
 func TestTimeUnix(t *testing.T) {
 	t.Parallel()
+	n := New(time.UTC, false)
 	var sec int64 = 1234567890
 	tv := reflect.ValueOf(time.Unix(sec, 0))
 	i := 1
 
-	res := toTimeUnix(tv)
+	res := n.toTimeUnix(tv)
 	if sec != res {
 		t.Errorf("[%d] timeUnix got %v but expected %v", i, res, sec)
 	}
@@ -428,13 +429,13 @@ func TestTimeUnix(t *testing.T) {
 			}
 		}()
 		iv := reflect.ValueOf(sec)
-		toTimeUnix(iv)
+		n.toTimeUnix(iv)
 	}(t)
 }
 
 func TestConditional(t *testing.T) {
 	c := qt.New(t)
-	n := New(false)
+	n := New(time.UTC, false)
 	a, b := "a", "b"
 
 	c.Assert(n.Conditional(true, a, b), qt.Equals, a)
@@ -446,7 +447,7 @@ func TestComparisonArgCount(t *testing.T) {
 	t.Parallel()
 	c := qt.New(t)
 
-	ns := New(false)
+	ns := New(time.UTC, false)
 
 	panicMsg := "missing arguments for comparison"