From 2c4dd2594cc47ca0b5cd719b963304ec48aecba2 Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 10:52:08 +0000 Subject: [PATCH 1/4] fix: do not encode source JSON translation string resulting in wrong display of special characters like ' Fixes #1712 Disclosure: Coded with help of Copiot. (description wrtten by me) So this does indeed loosen the encoding a bit. However, IMHO, it was neither better before though. You could always bypass the encoding for `args{0]` when you just include `')) { - continue; - } - } elseif (is_int($args[$i])) { + for ($i = 1; $i < $argsCount; ++$i) { + if (is_int($args[$i])) { continue; } $args[$i] = self::encode($args[$i]); diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 9e196103..118fe23b 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -182,7 +182,20 @@ class I18nTest extends TestCase $result = htmlspecialchars($input, ENT_QUOTES | ENT_HTML5 | ENT_DISALLOWED, 'UTF-8', false); $this->assertEquals($result, I18n::encode($input), 'encodes HTML entities'); $this->assertEquals('some ' . $result . ' + 1', I18n::_('some %s + %d', $input, 1), 'encodes parameters in translations'); - $this->assertEquals($result . $result, I18n::_($input . '%s', $input), 'encodes message ID as well, when no link'); + // Message ID should NOT be encoded (it comes from trusted source), only the parameter should be + $this->assertEquals($input . $result, I18n::_($input . '%s', $input), 'encodes only parameters, not message ID'); + } + + public function testFrenchApostropheInMessage() + { + $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr'; + I18n::loadTranslations(); + // The French translation should not have the apostrophe encoded + // Original: "Le document n'existe pas, a expiré, ou a été supprimé." + // Should NOT become: "Le document n'existe pas, a expiré, ou a été supprimé." + $message = I18n::_('Document does not exist, has expired or has been deleted.'); + $this->assertFalse(strpos($message, ''') !== false, 'French apostrophe should not be encoded in translation message'); + $this->assertTrue(strpos($message, "n'existe") !== false, 'French apostrophe should be present as literal character'); } public function testFallbackAlwaysPresent() From 38a722d2f5516085783583dee8f9c99e2ff2695a Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 12:19:49 +0000 Subject: [PATCH 2/4] test: make sure to unset HTTP_ACCEPT_LANGUAGE at test teardown --- tst/I18nTest.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 118fe23b..0374c897 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -37,6 +37,7 @@ class I18nTest extends TestCase public function tearDown(): void { unset($_COOKIE['lang'], $_SERVER['HTTP_ACCEPT_LANGUAGE']); + unset($_SERVER['HTTP_ACCEPT_LANGUAGE']); } public function testTranslationFallback() @@ -186,7 +187,7 @@ class I18nTest extends TestCase $this->assertEquals($input . $result, I18n::_($input . '%s', $input), 'encodes only parameters, not message ID'); } - public function testFrenchApostropheInMessage() + public function testApostropheEncodngInMessage() { $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr'; I18n::loadTranslations(); From e6762646168c69f32ea4ec1de5ec96d88f221935 Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 12:28:03 +0000 Subject: [PATCH 3/4] test: make I18nTest actually reload English translations again --- tst/I18nTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 0374c897..02d8cd3f 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -37,7 +37,7 @@ class I18nTest extends TestCase public function tearDown(): void { unset($_COOKIE['lang'], $_SERVER['HTTP_ACCEPT_LANGUAGE']); - unset($_SERVER['HTTP_ACCEPT_LANGUAGE']); + I18n::loadTranslations(); } public function testTranslationFallback() From 72d4c7aa2b91b64ae1c2af923535e5a6a02f5642 Mon Sep 17 00:00:00 2001 From: rugk Date: Thu, 13 Nov 2025 12:33:31 +0000 Subject: [PATCH 4/4] style: clarify comments --- tst/I18nTest.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tst/I18nTest.php b/tst/I18nTest.php index 02d8cd3f..d707d724 100644 --- a/tst/I18nTest.php +++ b/tst/I18nTest.php @@ -191,9 +191,8 @@ class I18nTest extends TestCase { $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'fr'; I18n::loadTranslations(); - // The French translation should not have the apostrophe encoded - // Original: "Le document n'existe pas, a expiré, ou a été supprimé." - // Should NOT become: "Le document n'existe pas, a expiré, ou a été supprimé." + // For example, the French translation should not have the apostrophe encoded + // See https://github.com/PrivateBin/PrivateBin/issues/1712 $message = I18n::_('Document does not exist, has expired or has been deleted.'); $this->assertFalse(strpos($message, ''') !== false, 'French apostrophe should not be encoded in translation message'); $this->assertTrue(strpos($message, "n'existe") !== false, 'French apostrophe should be present as literal character');