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 }