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