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 }