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