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 }