mirror of
https://github.com/miniflux/v2.git
synced 2025-08-06 17:41:00 +00:00
test(locale): increase test coverage to 100%
This commit is contained in:
parent
e7b98afdbe
commit
f860daef7f
4 changed files with 699 additions and 62 deletions
279
internal/locale/error_test.go
Normal file
279
internal/locale/error_test.go
Normal file
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,84 +7,192 @@ import "testing"
|
||||||
|
|
||||||
func TestPluralRules(t *testing.T) {
|
func TestPluralRules(t *testing.T) {
|
||||||
scenarios := map[string]map[int]int{
|
scenarios := map[string]map[int]int{
|
||||||
|
// Default rule (covers fr_FR, pt_BR, tr_TR, and other unlisted languages)
|
||||||
"default": {
|
"default": {
|
||||||
1: 0,
|
0: 0, // n <= 1
|
||||||
2: 1,
|
1: 0, // n <= 1
|
||||||
5: 1,
|
2: 1, // n > 1
|
||||||
|
5: 1, // n > 1
|
||||||
},
|
},
|
||||||
|
// Arabic (ar_AR) - 6 forms
|
||||||
"ar_AR": {
|
"ar_AR": {
|
||||||
0: 0,
|
0: 0, // n == 0
|
||||||
1: 1,
|
1: 1, // n == 1
|
||||||
2: 2,
|
2: 2, // n == 2
|
||||||
5: 3,
|
3: 3, // n%100 >= 3 && n%100 <= 10
|
||||||
11: 4,
|
5: 3, // n%100 >= 3 && n%100 <= 10
|
||||||
200: 5,
|
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": {
|
"cs_CZ": {
|
||||||
1: 0,
|
1: 0, // n == 1
|
||||||
2: 1,
|
2: 1, // n >= 2 && n <= 4
|
||||||
5: 2,
|
3: 1, // n >= 2 && n <= 4
|
||||||
|
4: 1, // n >= 2 && n <= 4
|
||||||
|
5: 2, // default case
|
||||||
},
|
},
|
||||||
|
// French (fr_FR) - uses default rule
|
||||||
"fr_FR": {
|
"fr_FR": {
|
||||||
1: 0,
|
0: 0, // n <= 1
|
||||||
2: 1,
|
1: 0, // n <= 1
|
||||||
5: 1,
|
2: 1, // n > 1
|
||||||
|
5: 1, // n > 1
|
||||||
},
|
},
|
||||||
|
// Indonesian (id_ID) - always form 0
|
||||||
"id_ID": {
|
"id_ID": {
|
||||||
1: 0,
|
0: 0,
|
||||||
5: 0,
|
1: 0,
|
||||||
|
5: 0,
|
||||||
|
100: 0,
|
||||||
},
|
},
|
||||||
|
// Japanese (ja_JP) - always form 0
|
||||||
"ja_JP": {
|
"ja_JP": {
|
||||||
1: 0,
|
0: 0,
|
||||||
2: 0,
|
1: 0,
|
||||||
5: 0,
|
2: 0,
|
||||||
|
5: 0,
|
||||||
|
100: 0,
|
||||||
},
|
},
|
||||||
|
// Polish (pl_PL) - 3 forms
|
||||||
"pl_PL": {
|
"pl_PL": {
|
||||||
1: 0,
|
1: 0, // n == 1
|
||||||
2: 1,
|
2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||||
5: 2,
|
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": {
|
"pt_BR": {
|
||||||
1: 0,
|
0: 0, // n <= 1
|
||||||
2: 1,
|
1: 0, // n <= 1
|
||||||
5: 1,
|
2: 1, // n > 1
|
||||||
|
5: 1, // n > 1
|
||||||
},
|
},
|
||||||
|
// Romanian (ro_RO) - 3 forms
|
||||||
"ro_RO": {
|
"ro_RO": {
|
||||||
1: 0,
|
0: 1, // n == 0 || (n%100 > 0 && n%100 < 20)
|
||||||
2: 1,
|
1: 0, // n == 1
|
||||||
5: 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": {
|
"ru_RU": {
|
||||||
1: 0,
|
1: 0, // n%10 == 1 && n%100 != 11
|
||||||
2: 1,
|
2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||||
5: 2,
|
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": {
|
"sr_RS": {
|
||||||
1: 0,
|
1: 0, // n%10 == 1 && n%100 != 11
|
||||||
2: 1,
|
2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||||
5: 2,
|
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": {
|
"tr_TR": {
|
||||||
1: 0,
|
0: 0, // n <= 1
|
||||||
2: 1,
|
1: 0, // n <= 1
|
||||||
5: 1,
|
2: 1, // n > 1
|
||||||
|
5: 1, // n > 1
|
||||||
},
|
},
|
||||||
|
// Ukrainian (uk_UA) - same as Russian
|
||||||
"uk_UA": {
|
"uk_UA": {
|
||||||
1: 0,
|
1: 0, // n%10 == 1 && n%100 != 11
|
||||||
2: 1,
|
2: 1, // n%10 >= 2 && n%10 <= 4 && (n%100 < 10 || n%100 >= 20)
|
||||||
5: 2,
|
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": {
|
"zh_CN": {
|
||||||
1: 0,
|
0: 0,
|
||||||
5: 0,
|
1: 0,
|
||||||
|
5: 0,
|
||||||
|
100: 0,
|
||||||
},
|
},
|
||||||
|
// Chinese Traditional (zh_TW) - always form 0
|
||||||
"zh_TW": {
|
"zh_TW": {
|
||||||
1: 0,
|
0: 0,
|
||||||
5: 0,
|
1: 0,
|
||||||
|
5: 0,
|
||||||
|
100: 0,
|
||||||
},
|
},
|
||||||
|
// Min Nan (nan_Latn_pehoeji) - always form 0
|
||||||
"nan_Latn_pehoeji": {
|
"nan_Latn_pehoeji": {
|
||||||
1: 0,
|
0: 0,
|
||||||
5: 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
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -10,6 +10,11 @@ type Printer struct {
|
||||||
language string
|
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 {
|
func (p *Printer) Print(key string) string {
|
||||||
if dict, err := getTranslationDict(p.language); err == nil {
|
if dict, err := getTranslationDict(p.language); err == nil {
|
||||||
if str, ok := dict[key]; ok {
|
if str, ok := dict[key]; ok {
|
||||||
|
@ -65,8 +70,3 @@ func (p *Printer) Plural(key string, n int, args ...interface{}) string {
|
||||||
|
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewPrinter creates a new Printer.
|
|
||||||
func NewPrinter(language string) *Printer {
|
|
||||||
return &Printer{language}
|
|
||||||
}
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ package locale // import "miniflux.app/v2/internal/locale"
|
||||||
|
|
||||||
import "testing"
|
import "testing"
|
||||||
|
|
||||||
func TestTranslateWithMissingLanguage(t *testing.T) {
|
func TestPrintfWithMissingLanguage(t *testing.T) {
|
||||||
defaultCatalog = catalog{}
|
defaultCatalog = catalog{}
|
||||||
translation := NewPrinter("invalid").Printf("missing.key")
|
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{
|
defaultCatalog = catalog{
|
||||||
"en_US": translationDict{
|
"en_US": translationDict{
|
||||||
"k": "v",
|
"k": "v",
|
||||||
|
@ -27,7 +27,7 @@ func TestTranslateWithMissingKey(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTranslateWithExistingKey(t *testing.T) {
|
func TestPrintfWithExistingKey(t *testing.T) {
|
||||||
defaultCatalog = catalog{
|
defaultCatalog = catalog{
|
||||||
"en_US": translationDict{
|
"en_US": translationDict{
|
||||||
"auth.username": "Login",
|
"auth.username": "Login",
|
||||||
|
@ -40,7 +40,7 @@ func TestTranslateWithExistingKey(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTranslateWithExistingKeyAndPlaceholder(t *testing.T) {
|
func TestPrintfWithExistingKeyAndPlaceholder(t *testing.T) {
|
||||||
defaultCatalog = catalog{
|
defaultCatalog = catalog{
|
||||||
"en_US": translationDict{
|
"en_US": translationDict{
|
||||||
"key": "Test: %s",
|
"key": "Test: %s",
|
||||||
|
@ -56,7 +56,7 @@ func TestTranslateWithExistingKeyAndPlaceholder(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTranslateWithMissingKeyAndPlaceholder(t *testing.T) {
|
func TestPrintfWithMissingKeyAndPlaceholder(t *testing.T) {
|
||||||
defaultCatalog = catalog{
|
defaultCatalog = catalog{
|
||||||
"en_US": translationDict{
|
"en_US": translationDict{
|
||||||
"auth.username": "Login",
|
"auth.username": "Login",
|
||||||
|
@ -72,7 +72,7 @@ func TestTranslateWithMissingKeyAndPlaceholder(t *testing.T) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTranslateWithInvalidValue(t *testing.T) {
|
func TestPrintfWithInvalidValue(t *testing.T) {
|
||||||
defaultCatalog = catalog{
|
defaultCatalog = catalog{
|
||||||
"en_US": translationDict{
|
"en_US": translationDict{
|
||||||
"auth.username": "Login",
|
"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{
|
defaultCatalog = catalog{
|
||||||
"en_US": translationDict{
|
"en_US": translationDict{
|
||||||
"number_of_users": []string{"%d user (%s)", "%d users (%s)"},
|
"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{
|
defaultCatalog = catalog{
|
||||||
"en_US": translationDict{
|
"en_US": translationDict{
|
||||||
"time_elapsed.years": []string{"%d year", "%d years"},
|
"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{
|
defaultCatalog = catalog{
|
||||||
"en_US": translationDict{
|
"en_US": translationDict{
|
||||||
"number_of_users": []string{"%d user (%s)", "%d users (%s)"},
|
"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{
|
defaultCatalog = catalog{
|
||||||
"en_US": translationDict{
|
"en_US": translationDict{
|
||||||
"number_of_users": []string{"%d user (%s)", "%d users (%s)"},
|
"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)
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue