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  }