diff --git a/internal/locale/error_test.go b/internal/locale/error_test.go new file mode 100644 index 00000000..9981f594 --- /dev/null +++ b/internal/locale/error_test.go @@ -0,0 +1,279 @@ +// SPDX-FileCopyrightText: Copyright The Miniflux Authors. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +package locale // import "miniflux.app/v2/internal/locale" + +import ( + "errors" + "testing" +) + +func TestNewLocalizedErrorWrapper(t *testing.T) { + originalErr := errors.New("original error message") + translationKey := "error.test_key" + args := []any{"arg1", 42} + + wrapper := NewLocalizedErrorWrapper(originalErr, translationKey, args...) + + if wrapper.originalErr != originalErr { + t.Errorf("Expected original error to be %v, got %v", originalErr, wrapper.originalErr) + } + + if wrapper.translationKey != translationKey { + t.Errorf("Expected translation key to be %q, got %q", translationKey, wrapper.translationKey) + } + + if len(wrapper.translationArgs) != 2 { + t.Errorf("Expected 2 translation args, got %d", len(wrapper.translationArgs)) + } + + if wrapper.translationArgs[0] != "arg1" || wrapper.translationArgs[1] != 42 { + t.Errorf("Expected translation args [arg1, 42], got %v", wrapper.translationArgs) + } +} + +func TestLocalizedErrorWrapper_Error(t *testing.T) { + originalErr := errors.New("original error message") + wrapper := NewLocalizedErrorWrapper(originalErr, "error.test_key") + + result := wrapper.Error() + if result != originalErr { + t.Errorf("Expected Error() to return original error %v, got %v", originalErr, result) + } +} + +func TestLocalizedErrorWrapper_Translate(t *testing.T) { + // Set up test catalog + defaultCatalog = catalog{ + "en_US": translationDict{ + "error.test_key": "Error: %s (code: %d)", + }, + "fr_FR": translationDict{ + "error.test_key": "Erreur : %s (code : %d)", + }, + } + + originalErr := errors.New("original error") + wrapper := NewLocalizedErrorWrapper(originalErr, "error.test_key", "test message", 404) + + // Test English translation + result := wrapper.Translate("en_US") + expected := "Error: test message (code: 404)" + if result != expected { + t.Errorf("Expected English translation %q, got %q", expected, result) + } + + // Test French translation + result = wrapper.Translate("fr_FR") + expected = "Erreur : test message (code : 404)" + if result != expected { + t.Errorf("Expected French translation %q, got %q", expected, result) + } + + // Test with missing language (should use key as fallback with args applied) + result = wrapper.Translate("invalid_lang") + expected = "error.test_key%!(EXTRA string=test message, int=404)" + if result != expected { + t.Errorf("Expected fallback translation %q, got %q", expected, result) + } +} + +func TestLocalizedErrorWrapper_TranslateWithEmptyKey(t *testing.T) { + originalErr := errors.New("original error message") + wrapper := NewLocalizedErrorWrapper(originalErr, "") + + result := wrapper.Translate("en_US") + expected := "original error message" + if result != expected { + t.Errorf("Expected original error message %q, got %q", expected, result) + } +} + +func TestLocalizedErrorWrapper_TranslateWithNoArgs(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "error.simple": "Simple error message", + }, + } + + originalErr := errors.New("original error") + wrapper := NewLocalizedErrorWrapper(originalErr, "error.simple") + + result := wrapper.Translate("en_US") + expected := "Simple error message" + if result != expected { + t.Errorf("Expected translation %q, got %q", expected, result) + } +} + +func TestNewLocalizedError(t *testing.T) { + translationKey := "error.validation" + args := []any{"field1", "invalid"} + + localizedErr := NewLocalizedError(translationKey, args...) + + if localizedErr.translationKey != translationKey { + t.Errorf("Expected translation key to be %q, got %q", translationKey, localizedErr.translationKey) + } + + if len(localizedErr.translationArgs) != 2 { + t.Errorf("Expected 2 translation args, got %d", len(localizedErr.translationArgs)) + } + + if localizedErr.translationArgs[0] != "field1" || localizedErr.translationArgs[1] != "invalid" { + t.Errorf("Expected translation args [field1, invalid], got %v", localizedErr.translationArgs) + } +} + +func TestLocalizedError_String(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "error.validation": "Validation failed for %s: %s", + }, + } + + localizedErr := NewLocalizedError("error.validation", "username", "too short") + + result := localizedErr.String() + expected := "Validation failed for username: too short" + if result != expected { + t.Errorf("Expected String() result %q, got %q", expected, result) + } +} + +func TestLocalizedError_StringWithMissingTranslation(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{}, + } + + localizedErr := NewLocalizedError("error.missing", "arg1") + + result := localizedErr.String() + expected := "error.missing%!(EXTRA string=arg1)" + if result != expected { + t.Errorf("Expected String() result %q, got %q", expected, result) + } +} + +func TestLocalizedError_Error(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "error.database": "Database connection failed: %s", + }, + } + + localizedErr := NewLocalizedError("error.database", "timeout") + + result := localizedErr.Error() + if result == nil { + t.Error("Expected Error() to return a non-nil error") + } + + expected := "Database connection failed: timeout" + if result.Error() != expected { + t.Errorf("Expected Error() message %q, got %q", expected, result.Error()) + } +} + +func TestLocalizedError_Translate(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "error.permission": "Permission denied for %s", + }, + "es_ES": translationDict{ + "error.permission": "Permiso denegado para %s", + }, + } + + localizedErr := NewLocalizedError("error.permission", "admin panel") + + // Test English translation + result := localizedErr.Translate("en_US") + expected := "Permission denied for admin panel" + if result != expected { + t.Errorf("Expected English translation %q, got %q", expected, result) + } + + // Test Spanish translation + result = localizedErr.Translate("es_ES") + expected = "Permiso denegado para admin panel" + if result != expected { + t.Errorf("Expected Spanish translation %q, got %q", expected, result) + } + + // Test with missing language + result = localizedErr.Translate("invalid_lang") + expected = "error.permission%!(EXTRA string=admin panel)" + if result != expected { + t.Errorf("Expected fallback translation %q, got %q", expected, result) + } +} + +func TestLocalizedError_TranslateWithNoArgs(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "error.generic": "An error occurred", + }, + "de_DE": translationDict{ + "error.generic": "Ein Fehler ist aufgetreten", + }, + } + + localizedErr := NewLocalizedError("error.generic") + + // Test English + result := localizedErr.Translate("en_US") + expected := "An error occurred" + if result != expected { + t.Errorf("Expected English translation %q, got %q", expected, result) + } + + // Test German + result = localizedErr.Translate("de_DE") + expected = "Ein Fehler ist aufgetreten" + if result != expected { + t.Errorf("Expected German translation %q, got %q", expected, result) + } +} + +func TestLocalizedError_TranslateWithComplexArgs(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "error.complex": "Error %d: %s occurred at %s with severity %s", + }, + } + + localizedErr := NewLocalizedError("error.complex", 500, "Internal Server Error", "2024-01-01", "high") + + result := localizedErr.Translate("en_US") + expected := "Error 500: Internal Server Error occurred at 2024-01-01 with severity high" + if result != expected { + t.Errorf("Expected complex translation %q, got %q", expected, result) + } +} + +func TestLocalizedErrorWrapper_WithNilError(t *testing.T) { + // This tests edge case behavior - what happens with nil error + wrapper := NewLocalizedErrorWrapper(nil, "error.test") + + // Error() should return nil + result := wrapper.Error() + if result != nil { + t.Errorf("Expected Error() to return nil, got %v", result) + } +} + +func TestLocalizedError_EmptyKey(t *testing.T) { + localizedErr := NewLocalizedError("") + + result := localizedErr.String() + expected := "" + if result != expected { + t.Errorf("Expected empty string for empty key, got %q", result) + } + + result = localizedErr.Translate("en_US") + if result != expected { + t.Errorf("Expected empty string for empty key translation, got %q", result) + } +} diff --git a/internal/locale/plural_test.go b/internal/locale/plural_test.go index b9a326ee..e21726e8 100644 --- a/internal/locale/plural_test.go +++ b/internal/locale/plural_test.go @@ -7,84 +7,192 @@ import "testing" func TestPluralRules(t *testing.T) { scenarios := map[string]map[int]int{ + // Default rule (covers fr_FR, pt_BR, tr_TR, and other unlisted languages) "default": { - 1: 0, - 2: 1, - 5: 1, + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + 5: 1, // n > 1 }, + // Arabic (ar_AR) - 6 forms "ar_AR": { - 0: 0, - 1: 1, - 2: 2, - 5: 3, - 11: 4, - 200: 5, + 0: 0, // n == 0 + 1: 1, // n == 1 + 2: 2, // n == 2 + 3: 3, // n%100 >= 3 && n%100 <= 10 + 5: 3, // n%100 >= 3 && n%100 <= 10 + 10: 3, // n%100 >= 3 && n%100 <= 10 + 11: 4, // n%100 >= 11 + 15: 4, // n%100 >= 11 + 99: 4, // n%100 >= 11 + 100: 5, // default case (n%100 == 0, doesn't match any condition) + 101: 5, // default case (n%100 == 1, but n != 1) + 200: 5, // default case }, + // Czech (cs_CZ) - 3 forms "cs_CZ": { - 1: 0, - 2: 1, - 5: 2, + 1: 0, // n == 1 + 2: 1, // n >= 2 && n <= 4 + 3: 1, // n >= 2 && n <= 4 + 4: 1, // n >= 2 && n <= 4 + 5: 2, // default case }, + // French (fr_FR) - uses default rule "fr_FR": { - 1: 0, - 2: 1, - 5: 1, + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + 5: 1, // n > 1 }, + // Indonesian (id_ID) - always form 0 "id_ID": { - 1: 0, - 5: 0, + 0: 0, + 1: 0, + 5: 0, + 100: 0, }, + // Japanese (ja_JP) - always form 0 "ja_JP": { - 1: 0, - 2: 0, - 5: 0, + 0: 0, + 1: 0, + 2: 0, + 5: 0, + 100: 0, }, + // Polish (pl_PL) - 3 forms "pl_PL": { - 1: 0, - 2: 1, - 5: 2, + 1: 0, // n == 1 + 2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) + 3: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) + 4: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) + 5: 2, // default case + 10: 2, // default case (n%100 < 10, but n%10 not in 2-4) + 11: 2, // default case (n%100 >= 10 and < 20) + 12: 2, // default case (n%100 >= 10 and < 20) + 22: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 >= 20) + 24: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 >= 20) }, + // Portuguese Brazilian (pt_BR) - uses default rule "pt_BR": { - 1: 0, - 2: 1, - 5: 1, + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + 5: 1, // n > 1 }, + // Romanian (ro_RO) - 3 forms "ro_RO": { - 1: 0, - 2: 1, - 5: 1, + 0: 1, // n == 0 || (n%100 > 0 && n%100 < 20) + 1: 0, // n == 1 + 2: 1, // n == 0 || (n%100 > 0 && n%100 < 20) + 5: 1, // n == 0 || (n%100 > 0 && n%100 < 20) + 19: 1, // n == 0 || (n%100 > 0 && n%100 < 20) + 20: 2, // default case + 21: 2, // default case + 100: 2, // default case (n%100 == 0, so condition fails) + 101: 1, // n%100 == 1, so n%100 > 0 && n%100 < 20 }, + // Russian (ru_RU) - 3 forms "ru_RU": { - 1: 0, - 2: 1, - 5: 2, + 1: 0, // n%10 == 1 && n%100 != 11 + 2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) + 3: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) + 4: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) + 5: 2, // default case + 11: 2, // n%10 == 1 but n%100 == 11, so default case + 12: 2, // default case + 21: 0, // n%10 == 1 && n%100 != 11 + 22: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 >= 20) }, + // Serbian (sr_RS) - same as Russian "sr_RS": { - 1: 0, - 2: 1, - 5: 2, + 1: 0, // n%10 == 1 && n%100 != 11 + 2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) + 5: 2, // default case + 11: 2, // n%10 == 1 but n%100 == 11, so default case + 21: 0, // n%10 == 1 && n%100 != 11 }, + // Turkish (tr_TR) - uses default rule "tr_TR": { - 1: 0, - 2: 1, - 5: 1, + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + 5: 1, // n > 1 }, + // Ukrainian (uk_UA) - same as Russian "uk_UA": { - 1: 0, - 2: 1, - 5: 2, + 1: 0, // n%10 == 1 && n%100 != 11 + 2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20) + 5: 2, // default case + 11: 2, // n%10 == 1 but n%100 == 11, so default case + 21: 0, // n%10 == 1 && n%100 != 11 }, + // Chinese Simplified (zh_CN) - always form 0 "zh_CN": { - 1: 0, - 5: 0, + 0: 0, + 1: 0, + 5: 0, + 100: 0, }, + // Chinese Traditional (zh_TW) - always form 0 "zh_TW": { - 1: 0, - 5: 0, + 0: 0, + 1: 0, + 5: 0, + 100: 0, }, + // Min Nan (nan_Latn_pehoeji) - always form 0 "nan_Latn_pehoeji": { - 1: 0, - 5: 0, + 0: 0, + 1: 0, + 5: 0, + 100: 0, + }, + // Additional languages from AvailableLanguages that use default rule + "de_DE": { + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + }, + "el_EL": { + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + }, + "en_US": { + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + }, + "es_ES": { + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + }, + "fi_FI": { + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + }, + "hi_IN": { + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + }, + "it_IT": { + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + }, + "nl_NL": { + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 + }, + // Test a language not in the switch (should use default rule) + "unknown_language": { + 0: 0, // n <= 1 + 1: 0, // n <= 1 + 2: 1, // n > 1 }, } diff --git a/internal/locale/printer.go b/internal/locale/printer.go index bca1f6b5..a4d97e0d 100644 --- a/internal/locale/printer.go +++ b/internal/locale/printer.go @@ -10,6 +10,11 @@ type Printer struct { language string } +// NewPrinter creates a new Printer instance for the given language. +func NewPrinter(language string) *Printer { + return &Printer{language} +} + func (p *Printer) Print(key string) string { if dict, err := getTranslationDict(p.language); err == nil { if str, ok := dict[key]; ok { @@ -65,8 +70,3 @@ func (p *Printer) Plural(key string, n int, args ...interface{}) string { return key } - -// NewPrinter creates a new Printer. -func NewPrinter(language string) *Printer { - return &Printer{language} -} diff --git a/internal/locale/printer_test.go b/internal/locale/printer_test.go index 780da7cb..528f593b 100644 --- a/internal/locale/printer_test.go +++ b/internal/locale/printer_test.go @@ -5,7 +5,7 @@ package locale // import "miniflux.app/v2/internal/locale" import "testing" -func TestTranslateWithMissingLanguage(t *testing.T) { +func TestPrintfWithMissingLanguage(t *testing.T) { defaultCatalog = catalog{} translation := NewPrinter("invalid").Printf("missing.key") @@ -14,7 +14,7 @@ func TestTranslateWithMissingLanguage(t *testing.T) { } } -func TestTranslateWithMissingKey(t *testing.T) { +func TestPrintfWithMissingKey(t *testing.T) { defaultCatalog = catalog{ "en_US": translationDict{ "k": "v", @@ -27,7 +27,7 @@ func TestTranslateWithMissingKey(t *testing.T) { } } -func TestTranslateWithExistingKey(t *testing.T) { +func TestPrintfWithExistingKey(t *testing.T) { defaultCatalog = catalog{ "en_US": translationDict{ "auth.username": "Login", @@ -40,7 +40,7 @@ func TestTranslateWithExistingKey(t *testing.T) { } } -func TestTranslateWithExistingKeyAndPlaceholder(t *testing.T) { +func TestPrintfWithExistingKeyAndPlaceholder(t *testing.T) { defaultCatalog = catalog{ "en_US": translationDict{ "key": "Test: %s", @@ -56,7 +56,7 @@ func TestTranslateWithExistingKeyAndPlaceholder(t *testing.T) { } } -func TestTranslateWithMissingKeyAndPlaceholder(t *testing.T) { +func TestPrintfWithMissingKeyAndPlaceholder(t *testing.T) { defaultCatalog = catalog{ "en_US": translationDict{ "auth.username": "Login", @@ -72,7 +72,7 @@ func TestTranslateWithMissingKeyAndPlaceholder(t *testing.T) { } } -func TestTranslateWithInvalidValue(t *testing.T) { +func TestPrintfWithInvalidValue(t *testing.T) { defaultCatalog = catalog{ "en_US": translationDict{ "auth.username": "Login", @@ -88,7 +88,134 @@ func TestTranslateWithInvalidValue(t *testing.T) { } } -func TestTranslatePluralWithDefaultRule(t *testing.T) { +func TestPrintWithMissingLanguage(t *testing.T) { + defaultCatalog = catalog{} + translation := NewPrinter("invalid").Print("missing.key") + + if translation != "missing.key" { + t.Errorf(`Wrong translation, got %q`, translation) + } +} + +func TestPrintWithMissingKey(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "existing.key": "value", + }, + } + + translation := NewPrinter("en_US").Print("missing.key") + if translation != "missing.key" { + t.Errorf(`Wrong translation, got %q`, translation) + } +} + +func TestPrintWithExistingKey(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "auth.username": "Login", + }, + } + + translation := NewPrinter("en_US").Print("auth.username") + if translation != "Login" { + t.Errorf(`Wrong translation, got %q`, translation) + } +} + +func TestPrintWithDifferentLanguages(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "greeting": "Hello", + }, + "fr_FR": translationDict{ + "greeting": "Bonjour", + }, + "es_ES": translationDict{ + "greeting": "Hola", + }, + } + + tests := []struct { + language string + expected string + }{ + {"en_US", "Hello"}, + {"fr_FR", "Bonjour"}, + {"es_ES", "Hola"}, + } + + for _, test := range tests { + translation := NewPrinter(test.language).Print("greeting") + if translation != test.expected { + t.Errorf(`Wrong translation for %s, got %q instead of %q`, test.language, translation, test.expected) + } + } +} + +func TestPrintWithInvalidTranslationType(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "valid.key": "valid string", + "invalid.key": 12345, // not a string + }, + } + + printer := NewPrinter("en_US") + + // Valid string should work + translation := printer.Print("valid.key") + if translation != "valid string" { + t.Errorf(`Wrong translation for valid key, got %q`, translation) + } + + // Invalid type should return the key itself + translation = printer.Print("invalid.key") + if translation != "invalid.key" { + t.Errorf(`Wrong translation for invalid key, got %q`, translation) + } +} + +func TestPrintWithNilTranslation(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "nil.key": nil, + }, + } + + translation := NewPrinter("en_US").Print("nil.key") + if translation != "nil.key" { + t.Errorf(`Wrong translation for nil value, got %q`, translation) + } +} + +func TestPrintWithEmptyKey(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "": "empty key translation", + }, + } + + translation := NewPrinter("en_US").Print("") + if translation != "empty key translation" { + t.Errorf(`Wrong translation for empty key, got %q`, translation) + } +} + +func TestPrintWithEmptyTranslation(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "empty.value": "", + }, + } + + translation := NewPrinter("en_US").Print("empty.value") + if translation != "" { + t.Errorf(`Wrong translation for empty value, got %q`, translation) + } +} + +func TestPluralWithDefaultRule(t *testing.T) { defaultCatalog = catalog{ "en_US": translationDict{ "number_of_users": []string{"%d user (%s)", "%d users (%s)"}, @@ -112,7 +239,7 @@ func TestTranslatePluralWithDefaultRule(t *testing.T) { } } -func TestTranslatePluralWithRussianRule(t *testing.T) { +func TestPluralWithRussianRule(t *testing.T) { defaultCatalog = catalog{ "en_US": translationDict{ "time_elapsed.years": []string{"%d year", "%d years"}, @@ -143,7 +270,7 @@ func TestTranslatePluralWithRussianRule(t *testing.T) { } } -func TestTranslatePluralWithMissingTranslation(t *testing.T) { +func TestPluralWithMissingTranslation(t *testing.T) { defaultCatalog = catalog{ "en_US": translationDict{ "number_of_users": []string{"%d user (%s)", "%d users (%s)"}, @@ -157,7 +284,7 @@ func TestTranslatePluralWithMissingTranslation(t *testing.T) { } } -func TestTranslatePluralWithInvalidValues(t *testing.T) { +func TestPluralWithInvalidValues(t *testing.T) { defaultCatalog = catalog{ "en_US": translationDict{ "number_of_users": []string{"%d user (%s)", "%d users (%s)"}, @@ -172,3 +299,126 @@ func TestTranslatePluralWithInvalidValues(t *testing.T) { t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected) } } + +func TestPluralWithMissingLanguage(t *testing.T) { + defaultCatalog = catalog{} + translation := NewPrinter("invalid_language").Plural("test.key", 2) + expected := "test.key" + if translation != expected { + t.Errorf(`Wrong translation, got %q instead of %q`, translation, expected) + } +} + +func TestPluralWithAnySliceType(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "test.key": []any{"%d item", "%d items"}, + }, + } + + printer := NewPrinter("en_US") + + translation := printer.Plural("test.key", 1, 1) + expected := "1 item" + if translation != expected { + t.Errorf(`Wrong translation for singular, got %q instead of %q`, translation, expected) + } + + translation = printer.Plural("test.key", 2, 2) + expected = "2 items" + if translation != expected { + t.Errorf(`Wrong translation for plural, got %q instead of %q`, translation, expected) + } +} + +func TestPluralWithMixedAnySliceTypes(t *testing.T) { + defaultCatalog = catalog{ + "en_US": translationDict{ + "mixed.key": []any{"single: %s", "multiple: %s", "many: %s"}, + }, + } + + printer := NewPrinter("en_US") + + // Test first element (should convert first any element to string) + translation := printer.Plural("mixed.key", 0, "test") // n=0 uses index 0 + expected := "single: test" + if translation != expected { + t.Errorf(`Wrong translation for index 0, got %q instead of %q`, translation, expected) + } + + // Test second element (should use plural form) + translation = printer.Plural("mixed.key", 2, "items") // plural form for default language + expected = "multiple: items" + if translation != expected { + t.Errorf(`Wrong translation for index 1, got %q instead of %q`, translation, expected) + } +} + +func TestPluralWithIndexOutOfBounds(t *testing.T) { + defaultCatalog = catalog{ + "test_lang": translationDict{ + "limited.key": []string{"only one form"}, + }, + } + + // Force a scenario where getPluralForm might return an index >= len(plurals) + // We'll create a scenario with Czech language rules + defaultCatalog["cs_CZ"] = translationDict{ + "limited.key": []string{"one form only"}, // Only one form, but Czech has 3 plural forms + } + + printer := NewPrinter("cs_CZ") + // n=5 should return index 2 for Czech, but we only have 1 form (index 0) + translation := printer.Plural("limited.key", 5) + expected := "limited.key" + if translation != expected { + t.Errorf(`Wrong translation for out of bounds index, got %q instead of %q`, translation, expected) + } +} + +func TestPluralWithVariousLanguageRules(t *testing.T) { + defaultCatalog = catalog{ + "ar_AR": translationDict{ + "items": []string{"no items", "one item", "two items", "few items", "many items", "other items"}, + }, + "pl_PL": translationDict{ + "files": []string{"one file", "few files", "many files"}, + }, + "ja_JP": translationDict{ + "photos": []string{"photos"}, + }, + } + + tests := []struct { + language string + key string + n int + expected string + }{ + // Arabic tests + {"ar_AR", "items", 0, "no items"}, + {"ar_AR", "items", 1, "one item"}, + {"ar_AR", "items", 2, "two items"}, + {"ar_AR", "items", 5, "few items"}, // n%100 >= 3 && n%100 <= 10 + {"ar_AR", "items", 15, "many items"}, // n%100 >= 11 + + // Polish tests + {"pl_PL", "files", 1, "one file"}, + {"pl_PL", "files", 3, "few files"}, // n%10 >= 2 && n%10 <= 4 + {"pl_PL", "files", 5, "many files"}, // default case + + // Japanese tests (always uses same form) + {"ja_JP", "photos", 1, "photos"}, + {"ja_JP", "photos", 10, "photos"}, + } + + for _, test := range tests { + printer := NewPrinter(test.language) + translation := printer.Plural(test.key, test.n) + if translation != test.expected { + t.Errorf(`Wrong translation for %s with n=%d, got %q instead of %q`, + test.language, test.n, translation, test.expected) + } + } +}