github.com/anakojm/hugo-katex@v0.0.0-20231023141351-42d6f5de9c0b/langs/i18n/i18n_test.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 i18n
    15  
    16  import (
    17  	"context"
    18  	"fmt"
    19  	"path/filepath"
    20  	"testing"
    21  
    22  	"github.com/bep/logg"
    23  	"github.com/gohugoio/hugo/common/loggers"
    24  	"github.com/gohugoio/hugo/common/types"
    25  	"github.com/gohugoio/hugo/config/testconfig"
    26  
    27  	"github.com/gohugoio/hugo/tpl/tplimpl"
    28  
    29  	"github.com/gohugoio/hugo/resources/page"
    30  	"github.com/spf13/afero"
    31  
    32  	"github.com/gohugoio/hugo/deps"
    33  
    34  	qt "github.com/frankban/quicktest"
    35  	"github.com/gohugoio/hugo/config"
    36  )
    37  
    38  var logger = loggers.NewDefault()
    39  
    40  type i18nTest struct {
    41  	name                             string
    42  	data                             map[string][]byte
    43  	args                             any
    44  	lang, id, expected, expectedFlag string
    45  }
    46  
    47  var i18nTests = []i18nTest{
    48  	// All translations present
    49  	{
    50  		name: "all-present",
    51  		data: map[string][]byte{
    52  			"en.toml": []byte("[hello]\nother = \"Hello, World!\""),
    53  			"es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""),
    54  		},
    55  		args:         nil,
    56  		lang:         "es",
    57  		id:           "hello",
    58  		expected:     "¡Hola, Mundo!",
    59  		expectedFlag: "¡Hola, Mundo!",
    60  	},
    61  	// Translation missing in current language but present in default
    62  	{
    63  		name: "present-in-default",
    64  		data: map[string][]byte{
    65  			"en.toml": []byte("[hello]\nother = \"Hello, World!\""),
    66  			"es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""),
    67  		},
    68  		args:         nil,
    69  		lang:         "es",
    70  		id:           "hello",
    71  		expected:     "Hello, World!",
    72  		expectedFlag: "[i18n] hello",
    73  	},
    74  	// Translation missing in default language but present in current
    75  	{
    76  		name: "present-in-current",
    77  		data: map[string][]byte{
    78  			"en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""),
    79  			"es.toml": []byte("[hello]\nother = \"¡Hola, Mundo!\""),
    80  		},
    81  		args:         nil,
    82  		lang:         "es",
    83  		id:           "hello",
    84  		expected:     "¡Hola, Mundo!",
    85  		expectedFlag: "¡Hola, Mundo!",
    86  	},
    87  	// Translation missing in both default and current language
    88  	{
    89  		name: "missing",
    90  		data: map[string][]byte{
    91  			"en.toml": []byte("[goodbye]\nother = \"Goodbye, World!\""),
    92  			"es.toml": []byte("[goodbye]\nother = \"¡Adiós, Mundo!\""),
    93  		},
    94  		args:         nil,
    95  		lang:         "es",
    96  		id:           "hello",
    97  		expected:     "",
    98  		expectedFlag: "[i18n] hello",
    99  	},
   100  	// Default translation file missing or empty
   101  	{
   102  		name: "file-missing",
   103  		data: map[string][]byte{
   104  			"en.toml": []byte(""),
   105  		},
   106  		args:         nil,
   107  		lang:         "es",
   108  		id:           "hello",
   109  		expected:     "",
   110  		expectedFlag: "[i18n] hello",
   111  	},
   112  	// Context provided
   113  	{
   114  		name: "context-provided",
   115  		data: map[string][]byte{
   116  			"en.toml": []byte("[wordCount]\nother = \"Hello, {{.WordCount}} people!\""),
   117  			"es.toml": []byte("[wordCount]\nother = \"¡Hola, {{.WordCount}} gente!\""),
   118  		},
   119  		args: struct {
   120  			WordCount int
   121  		}{
   122  			50,
   123  		},
   124  		lang:         "es",
   125  		id:           "wordCount",
   126  		expected:     "¡Hola, 50 gente!",
   127  		expectedFlag: "¡Hola, 50 gente!",
   128  	},
   129  	// https://github.com/gohugoio/hugo/issues/7787
   130  	{
   131  		name: "readingTime-one",
   132  		data: map[string][]byte{
   133  			"en.toml": []byte(`[readingTime]
   134  one = "One minute to read"
   135  other = "{{ .Count }} minutes to read"
   136  `),
   137  		},
   138  		args:         1,
   139  		lang:         "en",
   140  		id:           "readingTime",
   141  		expected:     "One minute to read",
   142  		expectedFlag: "One minute to read",
   143  	},
   144  	{
   145  		name: "readingTime-many-dot",
   146  		data: map[string][]byte{
   147  			"en.toml": []byte(`[readingTime]
   148  one = "One minute to read"
   149  other = "{{ . }} minutes to read"
   150  `),
   151  		},
   152  		args:         21,
   153  		lang:         "en",
   154  		id:           "readingTime",
   155  		expected:     "21 minutes to read",
   156  		expectedFlag: "21 minutes to read",
   157  	},
   158  	{
   159  		name: "readingTime-many",
   160  		data: map[string][]byte{
   161  			"en.toml": []byte(`[readingTime]
   162  one = "One minute to read"
   163  other = "{{ .Count }} minutes to read"
   164  `),
   165  		},
   166  		args:         21,
   167  		lang:         "en",
   168  		id:           "readingTime",
   169  		expected:     "21 minutes to read",
   170  		expectedFlag: "21 minutes to read",
   171  	},
   172  	// Issue #8454
   173  	{
   174  		name: "readingTime-map-one",
   175  		data: map[string][]byte{
   176  			"en.toml": []byte(`[readingTime]
   177  one = "One minute to read"
   178  other = "{{ .Count }} minutes to read"
   179  `),
   180  		},
   181  		args:         map[string]any{"Count": 1},
   182  		lang:         "en",
   183  		id:           "readingTime",
   184  		expected:     "One minute to read",
   185  		expectedFlag: "One minute to read",
   186  	},
   187  	{
   188  		name: "readingTime-string-one",
   189  		data: map[string][]byte{
   190  			"en.toml": []byte(`[readingTime]
   191  one = "One minute to read"
   192  other = "{{ . }} minutes to read"
   193  `),
   194  		},
   195  		args:         "1",
   196  		lang:         "en",
   197  		id:           "readingTime",
   198  		expected:     "One minute to read",
   199  		expectedFlag: "One minute to read",
   200  	},
   201  	{
   202  		name: "readingTime-map-many",
   203  		data: map[string][]byte{
   204  			"en.toml": []byte(`[readingTime]
   205  one = "One minute to read"
   206  other = "{{ .Count }} minutes to read"
   207  `),
   208  		},
   209  		args:         map[string]any{"Count": 21},
   210  		lang:         "en",
   211  		id:           "readingTime",
   212  		expected:     "21 minutes to read",
   213  		expectedFlag: "21 minutes to read",
   214  	},
   215  	{
   216  		name: "argument-float",
   217  		data: map[string][]byte{
   218  			"en.toml": []byte(`[float]
   219  other = "Number is {{ . }}"
   220  `),
   221  		},
   222  		args:         22.5,
   223  		lang:         "en",
   224  		id:           "float",
   225  		expected:     "Number is 22.5",
   226  		expectedFlag: "Number is 22.5",
   227  	},
   228  	// Same id and translation in current language
   229  	// https://github.com/gohugoio/hugo/issues/2607
   230  	{
   231  		name: "same-id-and-translation",
   232  		data: map[string][]byte{
   233  			"es.toml": []byte("[hello]\nother = \"hello\""),
   234  			"en.toml": []byte("[hello]\nother = \"hi\""),
   235  		},
   236  		args:         nil,
   237  		lang:         "es",
   238  		id:           "hello",
   239  		expected:     "hello",
   240  		expectedFlag: "hello",
   241  	},
   242  	// Translation missing in current language, but same id and translation in default
   243  	{
   244  		name: "same-id-and-translation-default",
   245  		data: map[string][]byte{
   246  			"es.toml": []byte("[bye]\nother = \"bye\""),
   247  			"en.toml": []byte("[hello]\nother = \"hello\""),
   248  		},
   249  		args:         nil,
   250  		lang:         "es",
   251  		id:           "hello",
   252  		expected:     "hello",
   253  		expectedFlag: "[i18n] hello",
   254  	},
   255  	// Unknown language code should get its plural spec from en
   256  	{
   257  		name: "unknown-language-code",
   258  		data: map[string][]byte{
   259  			"en.toml": []byte(`[readingTime]
   260  one ="one minute read"
   261  other = "{{.Count}} minutes read"`),
   262  			"klingon.toml": []byte(`[readingTime]
   263  one =  "eitt minutt med lesing"
   264  other = "{{ .Count }} minuttar lesing"`),
   265  		},
   266  		args:         3,
   267  		lang:         "klingon",
   268  		id:           "readingTime",
   269  		expected:     "3 minuttar lesing",
   270  		expectedFlag: "3 minuttar lesing",
   271  	},
   272  	// Issue #7838
   273  	{
   274  		name: "unknown-language-codes",
   275  		data: map[string][]byte{
   276  			"en.toml": []byte(`[readingTime]
   277  one ="en one"
   278  other = "en count {{.Count}}"`),
   279  			"a1.toml": []byte(`[readingTime]
   280  one =  "a1 one"
   281  other = "a1 count {{ .Count }}"`),
   282  			"a2.toml": []byte(`[readingTime]
   283  one =  "a2 one"
   284  other = "a2 count {{ .Count }}"`),
   285  		},
   286  		args:         3,
   287  		lang:         "a2",
   288  		id:           "readingTime",
   289  		expected:     "a2 count 3",
   290  		expectedFlag: "a2 count 3",
   291  	},
   292  	// https://github.com/gohugoio/hugo/issues/7798
   293  	{
   294  		name: "known-language-missing-plural",
   295  		data: map[string][]byte{
   296  			"oc.toml": []byte(`[oc]
   297  one =  "abc"`),
   298  		},
   299  		args:         1,
   300  		lang:         "oc",
   301  		id:           "oc",
   302  		expected:     "abc",
   303  		expectedFlag: "abc",
   304  	},
   305  	// https://github.com/gohugoio/hugo/issues/7794
   306  	{
   307  		name: "dotted-bare-key",
   308  		data: map[string][]byte{
   309  			"en.toml": []byte(`"shop_nextPage.one" = "Show Me The Money"
   310  `),
   311  		},
   312  		args:         nil,
   313  		lang:         "en",
   314  		id:           "shop_nextPage.one",
   315  		expected:     "Show Me The Money",
   316  		expectedFlag: "Show Me The Money",
   317  	},
   318  	// https: //github.com/gohugoio/hugo/issues/7804
   319  	{
   320  		name: "lang-with-hyphen",
   321  		data: map[string][]byte{
   322  			"pt-br.toml": []byte(`foo.one =  "abc"`),
   323  		},
   324  		args:         1,
   325  		lang:         "pt-br",
   326  		id:           "foo",
   327  		expected:     "abc",
   328  		expectedFlag: "abc",
   329  	},
   330  }
   331  
   332  func TestPlural(t *testing.T) {
   333  	c := qt.New(t)
   334  
   335  	for _, test := range []struct {
   336  		name     string
   337  		lang     string
   338  		id       string
   339  		templ    string
   340  		variants []types.KeyValue
   341  	}{
   342  		{
   343  			name: "English",
   344  			lang: "en",
   345  			id:   "hour",
   346  			templ: `
   347  [hour]
   348  one = "{{ . }} hour"
   349  other = "{{ . }} hours"`,
   350  			variants: []types.KeyValue{
   351  				{Key: 1, Value: "1 hour"},
   352  				{Key: "1", Value: "1 hour"},
   353  				{Key: 1.5, Value: "1.5 hours"},
   354  				{Key: "1.5", Value: "1.5 hours"},
   355  				{Key: 2, Value: "2 hours"},
   356  				{Key: "2", Value: "2 hours"},
   357  			},
   358  		},
   359  		{
   360  			name: "Other only",
   361  			lang: "en",
   362  			id:   "hour",
   363  			templ: `
   364  [hour]
   365  other = "{{ with . }}{{ . }}{{ end }} hours"`,
   366  			variants: []types.KeyValue{
   367  				{Key: 1, Value: "1 hours"},
   368  				{Key: "1", Value: "1 hours"},
   369  				{Key: 2, Value: "2 hours"},
   370  				{Key: nil, Value: " hours"},
   371  			},
   372  		},
   373  		{
   374  			name: "Polish",
   375  			lang: "pl",
   376  			id:   "day",
   377  			templ: `
   378  [day]
   379  one = "{{ . }} miesiąc"
   380  few = "{{ . }} miesiące"
   381  many = "{{ . }} miesięcy"
   382  other = "{{ . }} miesiąca"
   383  `,
   384  			variants: []types.KeyValue{
   385  				{Key: 1, Value: "1 miesiąc"},
   386  				{Key: 2, Value: "2 miesiące"},
   387  				{Key: 100, Value: "100 miesięcy"},
   388  				{Key: "100.0", Value: "100.0 miesiąca"},
   389  				{Key: 100.0, Value: "100 miesiąca"},
   390  			},
   391  		},
   392  	} {
   393  
   394  		c.Run(test.name, func(c *qt.C) {
   395  			cfg := config.New()
   396  			cfg.Set("enableMissingTranslationPlaceholders", true)
   397  			cfg.Set("publishDir", "public")
   398  			afs := afero.NewMemMapFs()
   399  
   400  			err := afero.WriteFile(afs, filepath.Join("i18n", test.lang+".toml"), []byte(test.templ), 0755)
   401  			c.Assert(err, qt.IsNil)
   402  
   403  			d, tp := prepareDeps(afs, cfg)
   404  
   405  			f := tp.t.Func(test.lang)
   406  			ctx := context.Background()
   407  
   408  			for _, variant := range test.variants {
   409  				c.Assert(f(ctx, test.id, variant.Key), qt.Equals, variant.Value, qt.Commentf("input: %v", variant.Key))
   410  				c.Assert(d.Log.LoggCount(logg.LevelWarn), qt.Equals, 0)
   411  			}
   412  
   413  		})
   414  
   415  	}
   416  }
   417  
   418  func doTestI18nTranslate(t testing.TB, test i18nTest, cfg config.Provider) string {
   419  	tp := prepareTranslationProvider(t, test, cfg)
   420  	f := tp.t.Func(test.lang)
   421  	return f(context.Background(), test.id, test.args)
   422  }
   423  
   424  type countField struct {
   425  	Count any
   426  }
   427  
   428  type noCountField struct {
   429  	Counts int
   430  }
   431  
   432  type countMethod struct {
   433  }
   434  
   435  func (c countMethod) Count() any {
   436  	return 32.5
   437  }
   438  
   439  func TestGetPluralCount(t *testing.T) {
   440  	c := qt.New(t)
   441  
   442  	c.Assert(getPluralCount(map[string]any{"Count": 32}), qt.Equals, 32)
   443  	c.Assert(getPluralCount(map[string]any{"Count": 1}), qt.Equals, 1)
   444  	c.Assert(getPluralCount(map[string]any{"Count": 1.5}), qt.Equals, "1.5")
   445  	c.Assert(getPluralCount(map[string]any{"Count": "32"}), qt.Equals, "32")
   446  	c.Assert(getPluralCount(map[string]any{"Count": "32.5"}), qt.Equals, "32.5")
   447  	c.Assert(getPluralCount(map[string]any{"count": 32}), qt.Equals, 32)
   448  	c.Assert(getPluralCount(map[string]any{"Count": "32"}), qt.Equals, "32")
   449  	c.Assert(getPluralCount(map[string]any{"Counts": 32}), qt.Equals, nil)
   450  	c.Assert(getPluralCount("foo"), qt.Equals, nil)
   451  	c.Assert(getPluralCount(countField{Count: 22}), qt.Equals, 22)
   452  	c.Assert(getPluralCount(countField{Count: 1.5}), qt.Equals, "1.5")
   453  	c.Assert(getPluralCount(&countField{Count: 22}), qt.Equals, 22)
   454  	c.Assert(getPluralCount(noCountField{Counts: 23}), qt.Equals, nil)
   455  	c.Assert(getPluralCount(countMethod{}), qt.Equals, "32.5")
   456  	c.Assert(getPluralCount(&countMethod{}), qt.Equals, "32.5")
   457  
   458  	c.Assert(getPluralCount(1234), qt.Equals, 1234)
   459  	c.Assert(getPluralCount(1234.4), qt.Equals, "1234.4")
   460  	c.Assert(getPluralCount(1234.0), qt.Equals, "1234.0")
   461  	c.Assert(getPluralCount("1234"), qt.Equals, "1234")
   462  	c.Assert(getPluralCount("0.5"), qt.Equals, "0.5")
   463  	c.Assert(getPluralCount(nil), qt.Equals, nil)
   464  }
   465  
   466  func prepareTranslationProvider(t testing.TB, test i18nTest, cfg config.Provider) *TranslationProvider {
   467  	c := qt.New(t)
   468  	afs := afero.NewMemMapFs()
   469  
   470  	for file, content := range test.data {
   471  		err := afero.WriteFile(afs, filepath.Join("i18n", file), []byte(content), 0755)
   472  		c.Assert(err, qt.IsNil)
   473  	}
   474  
   475  	_, tp := prepareDeps(afs, cfg)
   476  	return tp
   477  }
   478  
   479  func prepareDeps(afs afero.Fs, cfg config.Provider) (*deps.Deps, *TranslationProvider) {
   480  	d := testconfig.GetTestDeps(afs, cfg)
   481  	translationProvider := NewTranslationProvider()
   482  	d.TemplateProvider = tplimpl.DefaultTemplateProvider
   483  	d.TranslationProvider = translationProvider
   484  	d.Site = page.NewDummyHugoSite(d.Conf)
   485  	if err := d.Compile(nil); err != nil {
   486  		panic(err)
   487  	}
   488  	return d, translationProvider
   489  }
   490  
   491  func TestI18nTranslate(t *testing.T) {
   492  	c := qt.New(t)
   493  	var actual, expected string
   494  	v := config.New()
   495  
   496  	// Test without and with placeholders
   497  	for _, enablePlaceholders := range []bool{false, true} {
   498  		v.Set("enableMissingTranslationPlaceholders", enablePlaceholders)
   499  
   500  		for _, test := range i18nTests {
   501  			c.Run(fmt.Sprintf("%s-%t", test.name, enablePlaceholders), func(c *qt.C) {
   502  				if enablePlaceholders {
   503  					expected = test.expectedFlag
   504  				} else {
   505  					expected = test.expected
   506  				}
   507  				actual = doTestI18nTranslate(c, test, v)
   508  				c.Assert(actual, qt.Equals, expected)
   509  			})
   510  		}
   511  	}
   512  }
   513  
   514  func BenchmarkI18nTranslate(b *testing.B) {
   515  	v := config.New()
   516  	for _, test := range i18nTests {
   517  		b.Run(test.name, func(b *testing.B) {
   518  			tp := prepareTranslationProvider(b, test, v)
   519  			b.ResetTimer()
   520  			for i := 0; i < b.N; i++ {
   521  				f := tp.t.Func(test.lang)
   522  				actual := f(context.Background(), test.id, test.args)
   523  				if actual != test.expected {
   524  					b.Fatalf("expected %v got %v", test.expected, actual)
   525  				}
   526  			}
   527  		})
   528  	}
   529  }