lang.go (6856B)
1 // Copyright 2017 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 lang provides template functions for content internationalization.
15 package lang
16
17 import (
18 "fmt"
19 "math"
20 "strconv"
21 "strings"
22
23 "errors"
24
25 "github.com/gohugoio/locales"
26 translators "github.com/gohugoio/localescompressed"
27
28 "github.com/gohugoio/hugo/common/hreflect"
29 "github.com/gohugoio/hugo/deps"
30 "github.com/spf13/cast"
31 )
32
33 // New returns a new instance of the lang-namespaced template functions.
34 func New(deps *deps.Deps, translator locales.Translator) *Namespace {
35 return &Namespace{
36 translator: translator,
37 deps: deps,
38 }
39 }
40
41 // Namespace provides template functions for the "lang" namespace.
42 type Namespace struct {
43 translator locales.Translator
44 deps *deps.Deps
45 }
46
47 // Translate returns a translated string for id.
48 func (ns *Namespace) Translate(id any, args ...any) (string, error) {
49 var templateData any
50
51 if len(args) > 0 {
52 if len(args) > 1 {
53 return "", fmt.Errorf("wrong number of arguments, expecting at most 2, got %d", len(args)+1)
54 }
55 templateData = args[0]
56 }
57
58 sid, err := cast.ToStringE(id)
59 if err != nil {
60 return "", nil
61 }
62
63 return ns.deps.Translate(sid, templateData), nil
64 }
65
66 // FormatNumber formats number with the given precision for the current language.
67 func (ns *Namespace) FormatNumber(precision, number any) (string, error) {
68 p, n, err := ns.castPrecisionNumber(precision, number)
69 if err != nil {
70 return "", err
71 }
72 return ns.translator.FmtNumber(n, p), nil
73 }
74
75 // FormatPercent formats number with the given precision for the current language.
76 // Note that the number is assumed to be a percentage.
77 func (ns *Namespace) FormatPercent(precision, number any) (string, error) {
78 p, n, err := ns.castPrecisionNumber(precision, number)
79 if err != nil {
80 return "", err
81 }
82 return ns.translator.FmtPercent(n, p), nil
83 }
84
85 // FormatCurrency returns the currency representation of number for the given currency and precision
86 // for the current language.
87 //
88 // The return value is formatted with at least two decimal places.
89 func (ns *Namespace) FormatCurrency(precision, currency, number any) (string, error) {
90 p, n, err := ns.castPrecisionNumber(precision, number)
91 if err != nil {
92 return "", err
93 }
94 c := translators.GetCurrency(cast.ToString(currency))
95 if c < 0 {
96 return "", fmt.Errorf("unknown currency code: %q", currency)
97 }
98 return ns.translator.FmtCurrency(n, p, c), nil
99 }
100
101 // FormatAccounting returns the currency representation of number for the given currency and precision
102 // for the current language in accounting notation.
103 //
104 // The return value is formatted with at least two decimal places.
105 func (ns *Namespace) FormatAccounting(precision, currency, number any) (string, error) {
106 p, n, err := ns.castPrecisionNumber(precision, number)
107 if err != nil {
108 return "", err
109 }
110 c := translators.GetCurrency(cast.ToString(currency))
111 if c < 0 {
112 return "", fmt.Errorf("unknown currency code: %q", currency)
113 }
114 return ns.translator.FmtAccounting(n, p, c), nil
115 }
116
117 func (ns *Namespace) castPrecisionNumber(precision, number any) (uint64, float64, error) {
118 p, err := cast.ToUint64E(precision)
119 if err != nil {
120 return 0, 0, err
121 }
122
123 // Sanity check.
124 if p > 20 {
125 return 0, 0, fmt.Errorf("invalid precision: %d", precision)
126 }
127
128 n, err := cast.ToFloat64E(number)
129 if err != nil {
130 return 0, 0, err
131 }
132 return p, n, nil
133 }
134
135 // FormatNumberCustom formats a number with the given precision using the
136 // negative, decimal, and grouping options. The `options`
137 // parameter is a string consisting of `<negative> <decimal> <grouping>`. The
138 // default `options` value is `- . ,`.
139 //
140 // Note that numbers are rounded up at 5 or greater.
141 // So, with precision set to 0, 1.5 becomes `2`, and 1.4 becomes `1`.
142 //
143 // For a simpler function that adapts to the current language, see FormatNumber.
144 func (ns *Namespace) FormatNumberCustom(precision, number any, options ...any) (string, error) {
145 prec, err := cast.ToIntE(precision)
146 if err != nil {
147 return "", err
148 }
149
150 n, err := cast.ToFloat64E(number)
151 if err != nil {
152 return "", err
153 }
154
155 var neg, dec, grp string
156
157 if len(options) == 0 {
158 // defaults
159 neg, dec, grp = "-", ".", ","
160 } else {
161 delim := " "
162
163 if len(options) == 2 {
164 // custom delimiter
165 s, err := cast.ToStringE(options[1])
166 if err != nil {
167 return "", nil
168 }
169
170 delim = s
171 }
172
173 s, err := cast.ToStringE(options[0])
174 if err != nil {
175 return "", nil
176 }
177
178 rs := strings.Split(s, delim)
179 switch len(rs) {
180 case 0:
181 case 1:
182 neg = rs[0]
183 case 2:
184 neg, dec = rs[0], rs[1]
185 case 3:
186 neg, dec, grp = rs[0], rs[1], rs[2]
187 default:
188 return "", errors.New("too many fields in options parameter to NumFmt")
189 }
190 }
191
192 exp := math.Pow(10.0, float64(prec))
193 r := math.Round(n*exp) / exp
194
195 // Logic from MIT Licensed github.com/gohugoio/locales/
196 // Original Copyright (c) 2016 Go Playground
197
198 s := strconv.FormatFloat(math.Abs(r), 'f', prec, 64)
199 L := len(s) + 2 + len(s[:len(s)-1-prec])/3
200
201 var count int
202 inWhole := prec == 0
203 b := make([]byte, 0, L)
204
205 for i := len(s) - 1; i >= 0; i-- {
206 if s[i] == '.' {
207 for j := len(dec) - 1; j >= 0; j-- {
208 b = append(b, dec[j])
209 }
210 inWhole = true
211 continue
212 }
213
214 if inWhole {
215 if count == 3 {
216 for j := len(grp) - 1; j >= 0; j-- {
217 b = append(b, grp[j])
218 }
219 count = 1
220 } else {
221 count++
222 }
223 }
224
225 b = append(b, s[i])
226 }
227
228 if n < 0 {
229 for j := len(neg) - 1; j >= 0; j-- {
230 b = append(b, neg[j])
231 }
232 }
233
234 // reverse
235 for i, j := 0, len(b)-1; i < j; i, j = i+1, j-1 {
236 b[i], b[j] = b[j], b[i]
237 }
238
239 return string(b), nil
240 }
241
242 // NumFmt is deprecated, use FormatNumberCustom.
243 // We renamed this in Hugo 0.87.
244 // Deprecated: Use FormatNumberCustom
245 func (ns *Namespace) NumFmt(precision, number any, options ...any) (string, error) {
246 return ns.FormatNumberCustom(precision, number, options...)
247 }
248
249 type pagesLanguageMerger interface {
250 MergeByLanguageInterface(other any) (any, error)
251 }
252
253 // Merge creates a union of pages from two languages.
254 func (ns *Namespace) Merge(p2, p1 any) (any, error) {
255 if !hreflect.IsTruthful(p1) {
256 return p2, nil
257 }
258 if !hreflect.IsTruthful(p2) {
259 return p1, nil
260 }
261 merger, ok := p1.(pagesLanguageMerger)
262 if !ok {
263 return nil, fmt.Errorf("language merge not supported for %T", p1)
264 }
265 return merger.MergeByLanguageInterface(p2)
266 }