diff --git a/docs/content/en/functions/dateformat.md b/docs/content/en/functions/dateformat.md index 362efabd3e7..66183779bdc 100644 --- a/docs/content/en/functions/dateformat.md +++ b/docs/content/en/functions/dateformat.md @@ -9,17 +9,21 @@ menu: docs: parent: "functions" keywords: [dates,time,strings] -signature: ["time.Format LAYOUT INPUT"] +signature: ["time.Format LAYOUT INPUT [LANGUAGE]"] workson: [] hugoversion: relatedfuncs: [Format,now,Unix,time] deprecated: false --- -`time.Format` (alias `dateFormat`) converts either a `time.Time` object (e.g. `.Date`) or a timestamp string `INPUT` into the format specified by the `LAYOUT` string. +`time.Format` (alias `dateFormat`) converts either a `time.Time` object (e.g. `.Date`) or a timestamp string `INPUT` into the format specified by the `LAYOUT` string and an optional `LANGUAGE` code. ```go-html-template -{{ time.Format "Monday, Jan 2, 2006" "2015-01-21" }} → "Wednesday, Jan 21, 2015" +{{ time.Format "Monday, Jan 2, 2006" "2015-12-21" }} → "Monday, Dec 21, 2015" +``` + +```go-html-template +{{ time.Format "Monday, Jan 2, 2006" "2015-12-21" "pt" }} → "lunes, dic. 21, 2015" ``` Note that since Hugo 0.87.0, `time.Format` will return a localized string for the current language. {{< new-in "0.87.0" >}} diff --git a/tpl/time/init.go b/tpl/time/init.go index 4bb2ddf67bf..870705b206a 100644 --- a/tpl/time/init.go +++ b/tpl/time/init.go @@ -28,7 +28,7 @@ func init() { if d.Language == nil { panic("Language must be set") } - ctx := New(langs.GetTimeFormatter(d.Language), langs.GetLocation(d.Language)) + ctx := New(d.Language.Lang, langs.GetTimeFormatter(d.Language), langs.GetLocation(d.Language)) ns := &internal.TemplateFuncsNamespace{ Name: name, diff --git a/tpl/time/time.go b/tpl/time/time.go index cd78b83aaa4..ba7ef341d7e 100644 --- a/tpl/time/time.go +++ b/tpl/time/time.go @@ -15,6 +15,7 @@ package time import ( + "errors" "fmt" "time" _time "time" @@ -22,20 +23,25 @@ import ( "github.com/gohugoio/hugo/common/htime" "github.com/spf13/cast" + + translators "github.com/gohugoio/localescompressed" ) // New returns a new instance of the time-namespaced template functions. -func New(timeFormatter htime.TimeFormatter, location *time.Location) *Namespace { +func New(langCode string, timeFormatter htime.TimeFormatter, location *time.Location) *Namespace { + timeFormatters := make(map[string]htime.TimeFormatter) + timeFormatters[langCode] = timeFormatter + timeFormatters["default"] = timeFormatter return &Namespace{ - timeFormatter: timeFormatter, - location: location, + timeFormatters: timeFormatters, + location: location, } } // Namespace provides template functions for the "time" namespace. type Namespace struct { - timeFormatter htime.TimeFormatter - location *time.Location + timeFormatters map[string]htime.TimeFormatter + location *time.Location } // AsTime converts the textual representation of the datetime string into @@ -59,13 +65,51 @@ func (ns *Namespace) AsTime(v any, args ...any) (any, error) { // Format converts the textual representation of the datetime string in v into // time.Time if needed and formats it with the given layout. -func (ns *Namespace) Format(layout string, v any) (string, error) { +func (ns *Namespace) Format(layout string, args ...any) (string, error) { + var v any + var locale any + + if len(args) == 0 { + return "", errors.New("missing date/time argument") + } + v = args[0] + if len(args) == 2 { + locale = args[1] + } + if len(args) > 2 { + return "", errors.New("missing date/time argument") + } + t, err := htime.ToTimeInDefaultLocationE(v, ns.location) if err != nil { return "", err } - return ns.timeFormatter.Format(t, layout), nil + localeStr := "" + switch val := locale.(type) { + case string: + localeStr = val + case *string: + localeStr = *val + case nil: + localeStr = "default" + default: + return "", errors.New("locale must be a string or nil") + } + + formatter, ok := ns.timeFormatters[localeStr] + if ok { + return formatter.Format(t, layout), nil + } + + translator := translators.GetTranslator(localeStr) + if translator != nil { + formatter = htime.NewTimeFormatter(translator) + ns.timeFormatters[localeStr] = formatter + return formatter.Format(t, layout), nil + } + + return "", errors.New("no time formatter for language '" + localeStr + "'") } // Now returns the current local time or `clock` time diff --git a/tpl/time/time_test.go b/tpl/time/time_test.go index 9001f6b6b4b..d80eccf1a7d 100644 --- a/tpl/time/time_test.go +++ b/tpl/time/time_test.go @@ -28,7 +28,7 @@ func TestTimeLocation(t *testing.T) { t.Parallel() loc, _ := time.LoadLocation("America/Antigua") - ns := New(htime.NewTimeFormatter(translators.GetTranslator("en")), loc) + ns := New("en", htime.NewTimeFormatter(translators.GetTranslator("en")), loc) for i, test := range []struct { name string @@ -82,32 +82,81 @@ func TestTimeLocation(t *testing.T) { } } +func TestDuration(t *testing.T) { + t.Parallel() + + ns := New("en", htime.NewTimeFormatter(translators.GetTranslator("en")), time.UTC) + + for i, test := range []struct { + unit any + num any + expect any + }{ + {"nanosecond", 10, 10 * time.Nanosecond}, + {"ns", 10, 10 * time.Nanosecond}, + {"microsecond", 20, 20 * time.Microsecond}, + {"us", 20, 20 * time.Microsecond}, + {"µs", 20, 20 * time.Microsecond}, + {"millisecond", 20, 20 * time.Millisecond}, + {"ms", 20, 20 * time.Millisecond}, + {"second", 30, 30 * time.Second}, + {"s", 30, 30 * time.Second}, + {"minute", 20, 20 * time.Minute}, + {"m", 20, 20 * time.Minute}, + {"hour", 20, 20 * time.Hour}, + {"h", 20, 20 * time.Hour}, + {"hours", 20, false}, + {"hour", "30", 30 * time.Hour}, + } { + result, err := ns.Duration(test.unit, test.num) + if b, ok := test.expect.(bool); ok && !b { + if err == nil { + t.Errorf("[%d] Duration didn't return an expected error, got %v", i, result) + } + } else { + if err != nil { + t.Errorf("[%d] Duration failed: %s", i, err) + continue + } + if result != test.expect { + t.Errorf("[%d] Duration got %v but expected %v", i, result, test.expect) + } + } + } +} + func TestFormat(t *testing.T) { c := qt.New(t) c.Run("UTC", func(c *qt.C) { c.Parallel() - ns := New(htime.NewTimeFormatter(translators.GetTranslator("en")), time.UTC) + // dummy_cfg := config.NewFrom(maps.Params{}) + // langMap := make(map[string]*langs.Language) + // langMap["es"] = langs.NewLanguage("es", dummy_cfg) + // langMap["pt"] = langs.NewLanguage("pt", dummy_cfg) + ns := New("en", htime.NewTimeFormatter(translators.GetTranslator("en")), time.UTC) for i, test := range []struct { layout string value any + locale any expect any }{ - {"Monday, Jan 2, 2006", "2015-01-21", "Wednesday, Jan 21, 2015"}, - {"Monday, Jan 2, 2006", time.Date(2015, time.January, 21, 0, 0, 0, 0, time.UTC), "Wednesday, Jan 21, 2015"}, - {"This isn't a date layout string", "2015-01-21", "This isn't a date layout string"}, + {"Monday, Jan 2, 2006", "2015-01-21", nil, "Wednesday, Jan 21, 2015"}, + {"Monday, Jan 2, 2006", "2015-12-21", "es", "lunes, dic. 21, 2015"}, + {"Monday, Jan 2, 2006", time.Date(2015, time.January, 21, 0, 0, 0, 0, time.UTC), nil, "Wednesday, Jan 21, 2015"}, + {"This isn't a date layout string", "2015-01-21", nil, "This isn't a date layout string"}, // The following test case gives either "Tuesday, Jan 20, 2015" or "Monday, Jan 19, 2015" depending on the local time zone - {"Monday, Jan 2, 2006", 1421733600, time.Unix(1421733600, 0).Format("Monday, Jan 2, 2006")}, - {"Monday, Jan 2, 2006", 1421733600.123, false}, - {time.RFC3339, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "2016-03-03T04:05:00Z"}, - {time.RFC1123, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), "Thu, 03 Mar 2016 04:05:00 UTC"}, - {time.RFC3339, "Thu, 03 Mar 2016 04:05:00 UTC", "2016-03-03T04:05:00Z"}, - {time.RFC1123, "2016-03-03T04:05:00Z", "Thu, 03 Mar 2016 04:05:00 UTC"}, + {"Monday, Jan 2, 2006", 1421733600, nil, time.Unix(1421733600, 0).Format("Monday, Jan 2, 2006")}, + {"Monday, Jan 2, 2006", 1421733600.123, nil, false}, + {time.RFC3339, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), nil, "2016-03-03T04:05:00Z"}, + {time.RFC1123, time.Date(2016, time.March, 3, 4, 5, 0, 0, time.UTC), nil, "Thu, 03 Mar 2016 04:05:00 UTC"}, + {time.RFC3339, "Thu, 03 Mar 2016 04:05:00 UTC", nil, "2016-03-03T04:05:00Z"}, + {time.RFC1123, "2016-03-03T04:05:00Z", nil, "Thu, 03 Mar 2016 04:05:00 UTC"}, // Custom layouts, as introduced in Hugo 0.87. - {":date_medium", "2015-01-21", "Jan 21, 2015"}, + {":date_medium", "2015-12-21", "pt", "21 de dez. de 2015"}, } { - result, err := ns.Format(test.layout, test.value) + result, err := ns.Format(test.layout, test.value, test.locale) if b, ok := test.expect.(bool); ok && !b { if err == nil { c.Errorf("[%d] DateFormat didn't return an expected error, got %v", i, result) @@ -128,58 +177,24 @@ func TestFormat(t *testing.T) { c.Run("TZ America/Los_Angeles", func(c *qt.C) { c.Parallel() + // dummy_cfg := config.NewFrom(maps.Params{}) + // langMap := make(map[string]*langs.Language) + // langMap["es"] = langs.NewLanguage("es", dummy_cfg) + loc, err := time.LoadLocation("America/Los_Angeles") c.Assert(err, qt.IsNil) - ns := New(htime.NewTimeFormatter(translators.GetTranslator("en")), loc) + ns := New("en", htime.NewTimeFormatter(translators.GetTranslator("en")), loc) - d, err := ns.Format(":time_full", "2020-03-09T11:00:00") + d, err := ns.Format(":time_full", "2020-03-09T11:00:00", nil) c.Assert(err, qt.IsNil) c.Assert(d, qt.Equals, "11:00:00 am Pacific Daylight Time") - }) - -} + d, err = ns.Format(":time_full", "2020-03-09T11:00:00", "es") -func TestDuration(t *testing.T) { - t.Parallel() + c.Assert(err, qt.IsNil) + c.Assert(d, qt.Equals, "11:00:00 (hora de verano del Pacífico)") - ns := New(htime.NewTimeFormatter(translators.GetTranslator("en")), time.UTC) + }) - for i, test := range []struct { - unit any - num any - expect any - }{ - {"nanosecond", 10, 10 * time.Nanosecond}, - {"ns", 10, 10 * time.Nanosecond}, - {"microsecond", 20, 20 * time.Microsecond}, - {"us", 20, 20 * time.Microsecond}, - {"µs", 20, 20 * time.Microsecond}, - {"millisecond", 20, 20 * time.Millisecond}, - {"ms", 20, 20 * time.Millisecond}, - {"second", 30, 30 * time.Second}, - {"s", 30, 30 * time.Second}, - {"minute", 20, 20 * time.Minute}, - {"m", 20, 20 * time.Minute}, - {"hour", 20, 20 * time.Hour}, - {"h", 20, 20 * time.Hour}, - {"hours", 20, false}, - {"hour", "30", 30 * time.Hour}, - } { - result, err := ns.Duration(test.unit, test.num) - if b, ok := test.expect.(bool); ok && !b { - if err == nil { - t.Errorf("[%d] Duration didn't return an expected error, got %v", i, result) - } - } else { - if err != nil { - t.Errorf("[%d] Duration failed: %s", i, err) - continue - } - if result != test.expect { - t.Errorf("[%d] Duration got %v but expected %v", i, result, test.expect) - } - } - } }