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 }