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