Skip to content

Commit a57dda8

Browse files
committed
Localize time.Format
Fixes #8797
1 parent f9afba9 commit a57dda8

File tree

11 files changed

+395
-20
lines changed

11 files changed

+395
-20
lines changed

common/htime/time.go

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
// Copyright 2021 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package htime
15+
16+
import (
17+
"strings"
18+
"time"
19+
20+
"github.com/go-playground/locales"
21+
)
22+
23+
var (
24+
longDayNames = []string{
25+
"Sunday",
26+
"Monday",
27+
"Tuesday",
28+
"Wednesday",
29+
"Thursday",
30+
"Friday",
31+
"Saturday",
32+
}
33+
34+
shortDayNames = []string{
35+
"Sun",
36+
"Mon",
37+
"Tue",
38+
"Wed",
39+
"Thu",
40+
"Fri",
41+
"Sat",
42+
}
43+
44+
shortMonthNames = []string{
45+
"Jan",
46+
"Feb",
47+
"Mar",
48+
"Apr",
49+
"May",
50+
"Jun",
51+
"Jul",
52+
"Aug",
53+
"Sep",
54+
"Oct",
55+
"Nov",
56+
"Dec",
57+
}
58+
59+
longMonthNames = []string{
60+
"January",
61+
"February",
62+
"March",
63+
"April",
64+
"May",
65+
"June",
66+
"July",
67+
"August",
68+
"September",
69+
"October",
70+
"November",
71+
"December",
72+
}
73+
)
74+
75+
func NewTimeFormatter(ltr locales.Translator) TimeFormatter {
76+
if ltr == nil {
77+
panic("must provide a locales.Translator")
78+
}
79+
return TimeFormatter{
80+
ltr: ltr,
81+
}
82+
}
83+
84+
// TimeFormatter is locale aware.
85+
type TimeFormatter struct {
86+
ltr locales.Translator
87+
}
88+
89+
func (f TimeFormatter) Format(t time.Time, layout string) string {
90+
if layout == "" {
91+
return ""
92+
}
93+
94+
if layout[0] == ':' {
95+
// It may be one of Hugo's custom layouts.
96+
switch strings.ToLower(layout[1:]) {
97+
case "date_full":
98+
return f.ltr.FmtDateFull(t)
99+
case "date_long":
100+
return f.ltr.FmtDateLong(t)
101+
case "date_medium":
102+
return f.ltr.FmtDateMedium(t)
103+
case "date_short":
104+
return f.ltr.FmtDateShort(t)
105+
case "time_full":
106+
return f.ltr.FmtTimeFull(t)
107+
case "time_long":
108+
return f.ltr.FmtTimeLong(t)
109+
case "time_medium":
110+
return f.ltr.FmtTimeMedium(t)
111+
case "time_short":
112+
return f.ltr.FmtTimeShort(t)
113+
}
114+
}
115+
116+
s := t.Format(layout)
117+
118+
monthIdx := t.Month() - 1 // Month() starts at 1.
119+
dayIdx := t.Weekday()
120+
121+
s = strings.ReplaceAll(s, longMonthNames[monthIdx], f.ltr.MonthWide(t.Month()))
122+
s = strings.ReplaceAll(s, shortMonthNames[monthIdx], f.ltr.MonthAbbreviated(t.Month()))
123+
s = strings.ReplaceAll(s, longDayNames[dayIdx], f.ltr.WeekdayWide(t.Weekday()))
124+
s = strings.ReplaceAll(s, shortDayNames[dayIdx], f.ltr.WeekdayAbbreviated(t.Weekday()))
125+
126+
return s
127+
}

common/htime/time_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
// Copyright 2021 The Hugo Authors. All rights reserved.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package htime
15+
16+
import (
17+
"testing"
18+
"time"
19+
20+
translators "github.com/bep/gotranslators"
21+
qt "github.com/frankban/quicktest"
22+
)
23+
24+
func TestTimeFormatter(t *testing.T) {
25+
c := qt.New(t)
26+
27+
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
28+
june06 = june06.Add(7777 * time.Second)
29+
30+
c.Run("Norsk nynorsk", func(c *qt.C) {
31+
f := NewTimeFormatter(translators.Get("nn"))
32+
33+
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "onsdag juni 6 2018")
34+
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "on. juni 6 2018")
35+
c.Assert(f.Format(june06, "Mon Mon"), qt.Equals, "on. on.")
36+
})
37+
38+
c.Run("Custom layouts Norsk nynorsk", func(c *qt.C) {
39+
f := NewTimeFormatter(translators.Get("nn"))
40+
41+
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "onsdag 6. juni 2018")
42+
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "6. juni 2018")
43+
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "6. juni 2018")
44+
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "06.06.2018")
45+
46+
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "kl. 02:09:37 UTC")
47+
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "02:09:37 UTC")
48+
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "02:09:37")
49+
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "02:09")
50+
51+
})
52+
53+
c.Run("Custom layouts English", func(c *qt.C) {
54+
f := NewTimeFormatter(translators.Get("en"))
55+
56+
c.Assert(f.Format(june06, ":date_full"), qt.Equals, "Wednesday, June 6, 2018")
57+
c.Assert(f.Format(june06, ":date_long"), qt.Equals, "June 6, 2018")
58+
c.Assert(f.Format(june06, ":date_medium"), qt.Equals, "Jun 6, 2018")
59+
c.Assert(f.Format(june06, ":date_short"), qt.Equals, "6/6/18")
60+
61+
c.Assert(f.Format(june06, ":time_full"), qt.Equals, "2:09:37 am UTC")
62+
c.Assert(f.Format(june06, ":time_long"), qt.Equals, "2:09:37 am UTC")
63+
c.Assert(f.Format(june06, ":time_medium"), qt.Equals, "2:09:37 am")
64+
c.Assert(f.Format(june06, ":time_short"), qt.Equals, "2:09 am")
65+
66+
})
67+
68+
c.Run("English", func(c *qt.C) {
69+
f := NewTimeFormatter(translators.Get("en"))
70+
71+
c.Assert(f.Format(june06, "Monday Jan 2 2006"), qt.Equals, "Wednesday Jun 6 2018")
72+
c.Assert(f.Format(june06, "Mon January 2 2006"), qt.Equals, "Wed June 6 2018")
73+
c.Assert(f.Format(june06, "Mon Mon"), qt.Equals, "Wed Wed")
74+
})
75+
76+
}
77+
78+
func BenchmarkTimeFormatter(b *testing.B) {
79+
june06, _ := time.Parse("2006-Jan-02", "2018-Jun-06")
80+
81+
b.Run("Native", func(b *testing.B) {
82+
for i := 0; i < b.N; i++ {
83+
got := june06.Format("Monday Jan 2 2006")
84+
if got != "Wednesday Jun 6 2018" {
85+
b.Fatalf("invalid format, got %q", got)
86+
}
87+
}
88+
})
89+
90+
b.Run("Localized", func(b *testing.B) {
91+
f := NewTimeFormatter(translators.Get("nn"))
92+
b.ResetTimer()
93+
for i := 0; i < b.N; i++ {
94+
got := f.Format(june06, "Monday Jan 2 2006")
95+
if got != "onsdag juni 6 2018" {
96+
b.Fatalf("invalid format, got %q", got)
97+
}
98+
}
99+
})
100+
101+
b.Run("Localized Custom", func(b *testing.B) {
102+
f := NewTimeFormatter(translators.Get("nn"))
103+
b.ResetTimer()
104+
for i := 0; i < b.N; i++ {
105+
got := f.Format(june06, ":date_medium")
106+
if got != "6. juni 2018" {
107+
b.Fatalf("invalid format, got %q", got)
108+
}
109+
}
110+
})
111+
}
Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
2-
title: dateFormat
3-
description: Converts the textual representation of the `datetime` into the specified format.
2+
title: time.Format
3+
description: Converts a date/time to a localized string.
44
godocref: https://golang.org/pkg/time/
55
date: 2017-02-01
66
publishdate: 2017-02-01
@@ -10,23 +10,47 @@ menu:
1010
docs:
1111
parent: "functions"
1212
keywords: [dates,time,strings]
13-
signature: ["dateFormat LAYOUT INPUT"]
13+
signature: ["time.Format LAYOUT INPUT"]
1414
workson: []
1515
hugoversion:
1616
relatedfuncs: [Format,now,Unix,time]
1717
deprecated: false
1818
---
1919

20-
`dateFormat` converts a timestamp string `INPUT` into the format specified by the `LAYOUT` string.
20+
`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.
2121

22+
```go-html-template
23+
{{ time.Format "Monday, Jan 2, 2006" "2015-01-21" }} → "Wednesday, Jan 21, 2015"
2224
```
23-
{{ dateFormat "Monday, Jan 2, 2006" "2015-01-21" }} → "Wednesday, Jan 21, 2015"
24-
```
2525

26-
{{% warning %}}
27-
As of v0.19 of Hugo, the `dateFormat` function is *not* supported as part of Hugo's [multilingual feature](/content-management/multilingual/).
28-
{{% /warning %}}
26+
Note that since Hugo 0.87.0, `time.Format` will return a localized string for the currrent language. {{< new-in "0.87.0" >}}
27+
28+
The `LAYOUT` string can be either:
2929

30-
See [Go’s Layout String](/functions/format/#gos-layout-string) to learn about how the `LAYOUT` string has to be formatted. There are also some useful examples.
30+
* [Go’s Layout String](/functions/format/#gos-layout-string) to learn about how the `LAYOUT` string has to be formatted. There are also some useful examples.
31+
* A custom Hugo layout identifier (see full list below)
3132

3233
See the [`time` function](/functions/time/) to convert a timestamp string to a Go `time.Time` type value.
34+
35+
36+
## Date/time formatting layouts
37+
38+
{{< new-in "0.87.0" >}}
39+
40+
Go's date layout strings can be hard to reason about, especially with multiple languages. Since Hugo 0.87.0 you can alternatively use some predefined layout idenfifiers that will output localized dates or times:
41+
42+
```go-html-template
43+
{{ .Date | time.Format ":date_long" }}
44+
```
45+
46+
The full list of custom layouts with examples for English:
47+
48+
* `:date_full` => `Wednesday, June 6, 2018`
49+
* `:date_long` => `June 6, 2018`
50+
* `:date_medium` => `Jun 6, 2018`
51+
* `:date_short` => `6/6/18`
52+
53+
* `:time_full` => `2:09:37 am UTC`
54+
* `:time_long` => `2:09:37 am UTC`
55+
* `:time_medium` => `2:09:37 am`
56+
* `:time_short` => `2:09 am`

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ require (
1313
github.com/bep/gitmap v1.1.2
1414
github.com/bep/godartsass v0.12.0
1515
github.com/bep/golibsass v1.0.0
16+
github.com/bep/gotranslators v0.0.0-20210726170149-50377fc92c80
1617
github.com/bep/gowebp v0.1.0
1718
github.com/bep/tmc v0.5.1
1819
github.com/cli/safeexec v1.0.0
@@ -24,6 +25,7 @@ require (
2425
github.com/fsnotify/fsnotify v1.4.9
2526
github.com/getkin/kin-openapi v0.67.0
2627
github.com/ghodss/yaml v1.0.0
28+
github.com/go-playground/locales v0.13.0
2729
github.com/gobuffalo/flect v0.2.3
2830
github.com/gobwas/glob v0.2.3
2931
github.com/gohugoio/go-i18n/v2 v2.1.3-0.20210430103248-4c28c89f8013

go.sum

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,10 +134,14 @@ github.com/bep/godartsass v0.12.0 h1:VvGLA4XpXUjKvp53SI05YFLhRFJ78G+Ybnlaz6Oul7E
134134
github.com/bep/godartsass v0.12.0/go.mod h1:nXQlHHk4H1ghUk6n/JkYKG5RD43yJfcfp5aHRqT/pc4=
135135
github.com/bep/golibsass v1.0.0 h1:gNguBMSDi5yZEZzVZP70YpuFQE3qogJIGUlrVILTmOw=
136136
github.com/bep/golibsass v1.0.0/go.mod h1:DL87K8Un/+pWUS75ggYv41bliGiolxzDKWJAq3eJ1MA=
137+
github.com/bep/gotranslators v0.0.0-20210726170149-50377fc92c80 h1:FuOr7TE02FmHwf0HbOzfN0UyQfHoZd1R3PVuYduFU6U=
138+
github.com/bep/gotranslators v0.0.0-20210726170149-50377fc92c80/go.mod h1:/tUOv4Jdczp4ZggwBAQriNN97HsQdG1Gm+yV0PsIGD8=
137139
github.com/bep/gowebp v0.1.0 h1:4/iQpfnxHyXs3x/aTxMMdOpLEQQhFmF6G7EieWPTQyo=
138140
github.com/bep/gowebp v0.1.0/go.mod h1:ZhFodwdiFp8ehGJpF4LdPl6unxZm9lLFjxD3z2h2AgI=
139141
github.com/bep/tmc v0.5.1 h1:CsQnSC6MsomH64gw0cT5f+EwQDcvZz4AazKunFwTpuI=
140142
github.com/bep/tmc v0.5.1/go.mod h1:tGYHN8fS85aJPhDLgXETVKp+PR382OvFi2+q2GkGsq0=
143+
github.com/bep/workers v1.0.0 h1:U+H8YmEaBCEaFZBst7GcRVEoqeRC9dzH2dWOwGmOchg=
144+
github.com/bep/workers v1.0.0/go.mod h1:7kIESOB86HfR2379pwoMWNy8B50D7r99fRLUyPSNyCs=
141145
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
142146
github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM=
143147
github.com/census-instrumentation/opencensus-proto v0.2.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
@@ -210,6 +214,8 @@ github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUe
210214
github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg=
211215
github.com/go-openapi/swag v0.19.5 h1:lTz6Ys4CmqqCQmZPBlbQENR1/GucA2bzYTE12Pw4tFY=
212216
github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk=
217+
github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q=
218+
github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8=
213219
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
214220
github.com/gobuffalo/flect v0.2.3 h1:f/ZukRnSNA/DUpSNDadko7Qc0PhGvsew35p/2tu+CRY=
215221
github.com/gobuffalo/flect v0.2.3/go.mod h1:vmkQwuZYhN5Pc4ljYQZzP+1sq+NEkK+lh20jmEmX3jc=

0 commit comments

Comments
 (0)