hugo

Fork of github.com/gohugoio/hugo with reverse pagination support

git clone git://git.shimmy1996.com/hugo.git

i18n_test.go (14450B)

    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                             any
   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]any{"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]any{"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 any
  430 }
  431 
  432 type noCountField struct {
  433 	Counts int
  434 }
  435 
  436 type countMethod struct {
  437 }
  438 
  439 func (c countMethod) Count() any {
  440 	return 32.5
  441 }
  442 
  443 func TestGetPluralCount(t *testing.T) {
  444 	c := qt.New(t)
  445 
  446 	c.Assert(getPluralCount(map[string]any{"Count": 32}), qt.Equals, 32)
  447 	c.Assert(getPluralCount(map[string]any{"Count": 1}), qt.Equals, 1)
  448 	c.Assert(getPluralCount(map[string]any{"Count": 1.5}), qt.Equals, "1.5")
  449 	c.Assert(getPluralCount(map[string]any{"Count": "32"}), qt.Equals, "32")
  450 	c.Assert(getPluralCount(map[string]any{"Count": "32.5"}), qt.Equals, "32.5")
  451 	c.Assert(getPluralCount(map[string]any{"count": 32}), qt.Equals, 32)
  452 	c.Assert(getPluralCount(map[string]any{"Count": "32"}), qt.Equals, "32")
  453 	c.Assert(getPluralCount(map[string]any{"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.NewWithTestDefaults()
  504 	langs.LoadLanguageSettings(v, nil)
  505 	mod, err := modules.CreateProjectModule(v)
  506 	if err != nil {
  507 		panic(err)
  508 	}
  509 	v.Set("allModules", modules.Modules{mod})
  510 
  511 	return v
  512 }
  513 
  514 func TestI18nTranslate(t *testing.T) {
  515 	c := qt.New(t)
  516 	var actual, expected string
  517 	v := getConfig()
  518 
  519 	// Test without and with placeholders
  520 	for _, enablePlaceholders := range []bool{false, true} {
  521 		v.Set("enableMissingTranslationPlaceholders", enablePlaceholders)
  522 
  523 		for _, test := range i18nTests {
  524 			c.Run(fmt.Sprintf("%s-%t", test.name, enablePlaceholders), func(c *qt.C) {
  525 				if enablePlaceholders {
  526 					expected = test.expectedFlag
  527 				} else {
  528 					expected = test.expected
  529 				}
  530 				actual = doTestI18nTranslate(c, test, v)
  531 				c.Assert(actual, qt.Equals, expected)
  532 			})
  533 		}
  534 	}
  535 }
  536 
  537 func BenchmarkI18nTranslate(b *testing.B) {
  538 	v := getConfig()
  539 	for _, test := range i18nTests {
  540 		b.Run(test.name, func(b *testing.B) {
  541 			tp := prepareTranslationProvider(b, test, v)
  542 			b.ResetTimer()
  543 			for i := 0; i < b.N; i++ {
  544 				f := tp.t.Func(test.lang)
  545 				actual := f(test.id, test.args)
  546 				if actual != test.expected {
  547 					b.Fatalf("expected %v got %v", test.expected, actual)
  548 				}
  549 			}
  550 		})
  551 	}
  552 }