Просмотр исходного кода

fix: Slovenian and Lithuanian plural forms

The Slovenian plural formula follows gettext and maps n%100 == 1 to
index 1, 2 to index 2, 3-4 to index 3 and everything else to index
0. The translations were stored singular first, so all plural arrays
in sl.json were rotated right by one. Two "other" forms, for the
minutes and hours expiration messages, contained the 3-4 form and
were corrected to "minut" and "ur".

The Lithuanian formula lacked parentheses around the last two digits
check, so due to operator precedence numbers like 20, 30 or 120
selected the "few" form instead of "many". Fixed in both PHP and JS,
with tests added and the SRI hash of privatebin.js updated.

fixes #1951, fixes #1956, kudos @Jose-Tejera, kudos @fresh55
El RIDO 3 недель назад
Родитель
Сommit
3b773aab3e
7 измененных файлов с 51 добавлено и 24 удалено
  1. 2 0
      CHANGELOG.md
  2. 13 13
      i18n/sl.json
  3. 1 1
      js/privatebin.js
  4. 11 0
      js/test/I18n.js
  5. 1 1
      lib/Configuration.php
  6. 1 1
      lib/I18n.php
  7. 22 8
      tst/I18nTest.php

+ 2 - 0
CHANGELOG.md

@@ -2,6 +2,8 @@
 
 
 ## 2.1.1 (not yet released)
 ## 2.1.1 (not yet released)
 * FIXED: Creating a document failed with a JavaScript error when the `discussion` or `password` option is disabled, as the top navigation accessed the omitted elements
 * FIXED: Creating a document failed with a JavaScript error when the `discussion` or `password` option is disabled, as the top navigation accessed the omitted elements
+* FIXED: Slovenian plural forms were stored in the wrong order, i.e. "1 uri" instead of "1 ura" for one hour (#1951)
+* FIXED: Lithuanian plural formula selected the "few" form for numbers ending in 0 above 10, i.e. "20 sekundės" instead of "20 sekundžių" (#1956)
 
 
 ## 2.1.0 (2026-10-18)
 ## 2.1.0 (2026-10-18)
 * ADDED: Added `shortenviachhoto` endpoint with an `chhoto` configuration section
 * ADDED: Added `shortenviachhoto` endpoint with an `chhoto` configuration section

+ 13 - 13
i18n/sl.json

@@ -7,11 +7,11 @@
     "%s requires php %s or above to work. Sorry.": "Oprosti, %s za delovanje potrebuje vsaj php %s.",
     "%s requires php %s or above to work. Sorry.": "Oprosti, %s za delovanje potrebuje vsaj php %s.",
     "%s requires configuration section [%s] to be present in configuration file.": "%s potrebuje sekcijo konfiguracij [%s] v konfiguracijski datoteki.",
     "%s requires configuration section [%s] to be present in configuration file.": "%s potrebuje sekcijo konfiguracij [%s] v konfiguracijski datoteki.",
     "Please wait %d seconds between each post.": [
     "Please wait %d seconds between each post.": [
+        "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekundo pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekundo pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekundi pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekundi pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekunde pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekunde pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo.",
-        "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo.",
         "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo."
         "Prosim počakaj vsaj %d sekund pred vsako naslednjo objavo."
     ],
     ],
     "Document is limited to %s of encrypted data.": "Velikost prilepka je omejena na %s zakodiranih podatkov.",
     "Document is limited to %s of encrypted data.": "Velikost prilepka je omejena na %s zakodiranih podatkov.",
@@ -36,101 +36,101 @@
     "Discussion": "Razprava",
     "Discussion": "Razprava",
     "Toggle navigation": "Preklopi navigacijo",
     "Toggle navigation": "Preklopi navigacijo",
     "%d seconds": [
     "%d seconds": [
+        "%d sekund",
         "%d sekunda",
         "%d sekunda",
         "%d sekundi",
         "%d sekundi",
         "%d sekunde",
         "%d sekunde",
         "%d sekund",
         "%d sekund",
-        "%d sekund",
         "%d sekund"
         "%d sekund"
     ],
     ],
     "%d minutes": [
     "%d minutes": [
+        "%d minut",
         "%d minuta",
         "%d minuta",
         "%d minuti",
         "%d minuti",
         "%d minute",
         "%d minute",
         "%d minut",
         "%d minut",
-        "%d minut",
         "%d minut"
         "%d minut"
     ],
     ],
     "%d hours": [
     "%d hours": [
+        "%d ur",
         "%d ura",
         "%d ura",
         "%d uri",
         "%d uri",
         "%d ure",
         "%d ure",
         "%d ur",
         "%d ur",
-        "%d ur",
         "%d ur"
         "%d ur"
     ],
     ],
     "%d days": [
     "%d days": [
+        "%d dni",
         "%d dan",
         "%d dan",
         "%d dneva",
         "%d dneva",
         "%d dnevi",
         "%d dnevi",
         "%d dni",
         "%d dni",
-        "%d dni",
         "%d dni"
         "%d dni"
     ],
     ],
     "%d weeks": [
     "%d weeks": [
+        "%d tednov",
         "%d teden",
         "%d teden",
         "%d tedna",
         "%d tedna",
         "%d tedni",
         "%d tedni",
         "%d tednov",
         "%d tednov",
-        "%d tednov",
         "%d tednov"
         "%d tednov"
     ],
     ],
     "%d months": [
     "%d months": [
+        "%d mesecev",
         "%d mesec",
         "%d mesec",
         "%d meseca",
         "%d meseca",
         "%d meseci",
         "%d meseci",
         "%d mesecev",
         "%d mesecev",
-        "%d mesecev",
         "%d mesecev"
         "%d mesecev"
     ],
     ],
     "%d years": [
     "%d years": [
+        "%d let",
         "%d leto",
         "%d leto",
         "%d leti",
         "%d leti",
         "%d leta",
         "%d leta",
         "%d let",
         "%d let",
-        "%d let",
         "%d let"
         "%d let"
     ],
     ],
     "Never": "Nikoli",
     "Never": "Nikoli",
     "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "Ne pozabi: To je testna storitev: Podatki so lahko kadarkoli pobrisani. Mucki bodo umrli, če boš zlorabljala to storitev.",
     "Note: This is a test service: Data may be deleted anytime. Kittens will die if you abuse this service.": "Ne pozabi: To je testna storitev: Podatki so lahko kadarkoli pobrisani. Mucki bodo umrli, če boš zlorabljala to storitev.",
     "This document will expire in %d seconds.": [
     "This document will expire in %d seconds.": [
+        "Ta dokument bo potekel čez %d sekund.",
         "Ta dokument bo potekel čez %d sekundo.",
         "Ta dokument bo potekel čez %d sekundo.",
         "Ta dokument bo potekel čez %d sekundi.",
         "Ta dokument bo potekel čez %d sekundi.",
         "Ta dokument bo potekel čez %d sekunde.",
         "Ta dokument bo potekel čez %d sekunde.",
         "Ta dokument bo potekel čez %d sekund.",
         "Ta dokument bo potekel čez %d sekund.",
-        "Ta dokument bo potekel čez %d sekund.",
         "Ta dokument bo potekel čez %d sekund."
         "Ta dokument bo potekel čez %d sekund."
     ],
     ],
     "This document will expire in %d minutes.": [
     "This document will expire in %d minutes.": [
+        "Ta dokument bo potekel čez %d minut.",
         "Ta dokument bo potekel čez %d minuto.",
         "Ta dokument bo potekel čez %d minuto.",
         "Ta dokument bo potekel čez %d minuti.",
         "Ta dokument bo potekel čez %d minuti.",
         "Ta dokument bo potekel čez %d minute.",
         "Ta dokument bo potekel čez %d minute.",
-        "Ta dokument bo potekel čez %d minute.",
         "Ta dokument bo potekel čez %d minut.",
         "Ta dokument bo potekel čez %d minut.",
         "Ta dokument bo potekel čez %d minut."
         "Ta dokument bo potekel čez %d minut."
     ],
     ],
     "This document will expire in %d hours.": [
     "This document will expire in %d hours.": [
+        "Ta dokument bo potekel čez %d ur.",
         "Ta dokument bo potekel čez %d uro.",
         "Ta dokument bo potekel čez %d uro.",
         "Ta dokument bo potekel čez %d uri.",
         "Ta dokument bo potekel čez %d uri.",
         "Ta dokument bo potekel čez %d ure.",
         "Ta dokument bo potekel čez %d ure.",
-        "Ta dokument bo potekel čez %d ure.",
         "Ta dokument bo potekel čez %d ur.",
         "Ta dokument bo potekel čez %d ur.",
         "Ta dokument bo potekel čez %d ur."
         "Ta dokument bo potekel čez %d ur."
     ],
     ],
     "This document will expire in %d days.": [
     "This document will expire in %d days.": [
+        "Ta dokument bo potekel čez %d dni.",
         "Ta dokument bo potekel čez %d dan.",
         "Ta dokument bo potekel čez %d dan.",
         "Ta dokument bo potekel čez %d dneva.",
         "Ta dokument bo potekel čez %d dneva.",
         "Ta dokument bo potekel čez %d dni.",
         "Ta dokument bo potekel čez %d dni.",
         "Ta dokument bo potekel čez %d dni.",
         "Ta dokument bo potekel čez %d dni.",
-        "Ta dokument bo potekel čez %d dni.",
         "Ta dokument bo potekel čez %d dni."
         "Ta dokument bo potekel čez %d dni."
     ],
     ],
     "This document will expire in %d months.": [
     "This document will expire in %d months.": [
+        "Ta dokument bo potekel čez %d mesecev.",
         "Ta dokument bo potekel čez %d mesec.",
         "Ta dokument bo potekel čez %d mesec.",
         "Ta dokument bo potekel čez %d meseca.",
         "Ta dokument bo potekel čez %d meseca.",
         "Ta dokument bo potekel čez %d mesece.",
         "Ta dokument bo potekel čez %d mesece.",
         "Ta dokument bo potekel čez %d mesecev.",
         "Ta dokument bo potekel čez %d mesecev.",
-        "Ta dokument bo potekel čez %d mesecev.",
         "Ta dokument bo potekel čez %d mesecev."
         "Ta dokument bo potekel čez %d mesecev."
     ],
     ],
     "Please enter the password for this document:": "Prosim vnesi geslo tega prilepka:",
     "Please enter the password for this document:": "Prosim vnesi geslo tega prilepka:",

+ 1 - 1
js/privatebin.js

@@ -878,7 +878,7 @@ window.PrivateBin = (function () {
                 case 'th':
                 case 'th':
                     return 0;
                     return 0;
                 case 'lt':
                 case 'lt':
-                    return n % 10 === 1 && n % 100 !== 11 ? 0 : ((n % 10 >= 2 && n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
+                    return n % 10 === 1 && n % 100 !== 11 ? 0 : ((n % 10 >= 2 && (n % 100 < 10 || n % 100 >= 20)) ? 1 : 2);
                 case 'pl':
                 case 'pl':
                     return n === 1 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
                     return n === 1 ? 0 : (n % 10 >= 2 && n % 10 <= 4 && (n % 100 < 10 || n % 100 >= 20) ? 1 : 2);
                 case 'ro':
                 case 'ro':

+ 11 - 0
js/test/I18n.js

@@ -183,6 +183,17 @@ describe('I18n', function () {
                 }
                 }
             ));
             ));
         });
         });
+
+        it('routes Lithuanian cardinal forms by the last digit', () => {
+            PrivateBin.I18n.reset('lt');
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(1), 0);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(2), 1);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(10), 2);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(22), 1);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(110), 2);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(111), 2);
+            assert.strictEqual(PrivateBin.I18n.getPluralForm(120), 2);
+        });
     });
     });
 
 
     // loading of JSON via AJAX needs to be tested in the browser, this just mocks it
     // loading of JSON via AJAX needs to be tested in the browser, this just mocks it

+ 1 - 1
lib/Configuration.php

@@ -124,7 +124,7 @@ class Configuration
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/kjua-0.10.0.js'      => 'sha512-BYj4xggowR7QD150VLSTRlzH62YPfhpIM+b/1EUEr7RQpdWAGKulxWnOvjFx1FUlba4m6ihpNYuQab51H6XlYg==',
             'js/legacy.js'           => 'sha512-pRofxsrf5UItjiP22Dcjh3FAcBjF/n7h8U9/W5xqJk17U0N2U1oajhXypq/omo9jhwS1iVGOhWrRepoPeFns+w==',
             'js/legacy.js'           => 'sha512-pRofxsrf5UItjiP22Dcjh3FAcBjF/n7h8U9/W5xqJk17U0N2U1oajhXypq/omo9jhwS1iVGOhWrRepoPeFns+w==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
             'js/prettify.js'         => 'sha512-puO0Ogy++IoA2Pb9IjSxV1n4+kQkKXYAEUtVzfZpQepyDPyXk8hokiYDS7ybMogYlyyEIwMLpZqVhCkARQWLMg==',
-            'js/privatebin.js'       => 'sha512-n3OsnBiq2RnihXpDKdwrRJrF55DHJLyiD7oZtA47WYhVE3o46rma/g9VZQdTH6dy44s0nPtthuPU0nu0mSaRZQ==',
+            'js/privatebin.js'       => 'sha512-EnB+zyT/Hl7nRvSdO3htInodeShz5dNCKzyRkqVqNdqzKMbNCegubjmfBHlflDT4DbhYVsqOMJ8G/Eaty9wosA==',
             'js/purify-3.4.17.js'    => 'sha512-udF2zPdEyVCAxIGeBbeSGFK1Fj5c4eHGi6NqOSsr8M75lGs39POzgUNxO3SFxOg4cHfAoZJgR2EaXdmDi6v6AQ==',
             'js/purify-3.4.17.js'    => 'sha512-udF2zPdEyVCAxIGeBbeSGFK1Fj5c4eHGi6NqOSsr8M75lGs39POzgUNxO3SFxOg4cHfAoZJgR2EaXdmDi6v6AQ==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/showdown-2.1.0.js'   => 'sha512-WYXZgkTR0u/Y9SVIA4nTTOih0kXMEd8RRV6MLFdL6YU8ymhR528NLlYQt1nlJQbYz4EW+ZsS0fx1awhiQJme1Q==',
             'js/zlib-1.3.2.js'       => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',
             'js/zlib-1.3.2.js'       => 'sha512-RAhJgxg9siMIA8ky4c10Rc2zUgnK80olHB8Tt1IOYWY4Eh1WmrviQkDn+sgBlb38ZHq3tzufGC41kP360gmosQ==',

+ 1 - 1
lib/I18n.php

@@ -411,7 +411,7 @@ class I18n
             case 'th':
             case 'th':
                 return 0;
                 return 0;
             case 'lt':
             case 'lt':
-                return $n % 10 === 1 && $n % 100 !== 11 ? 0 : (($n % 10 >= 2 && $n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
+                return $n % 10 === 1 && $n % 100 !== 11 ? 0 : (($n % 10 >= 2 && ($n % 100 < 10 || $n % 100 >= 20)) ? 1 : 2);
             case 'pl':
             case 'pl':
                 return $n === 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
                 return $n === 1 ? 0 : ($n % 10 >= 2 && $n % 10 <= 4 && ($n % 100 < 10 || $n % 100 >= 20) ? 1 : 2);
             case 'ro':
             case 'ro':

+ 22 - 8
tst/I18nTest.php

@@ -95,6 +95,20 @@ class I18nTest extends TestCase
         );
         );
     }
     }
 
 
+    public function testBrowserLanguageLtDetection()
+    {
+        $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'lt;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
+        I18n::loadTranslations();
+        $this->assertEquals('lt', I18n::getLanguage(), 'browser language lt');
+        $this->assertEquals('1 sekundę', I18n::_('%d seconds', 1), '1 second in Lithuanian');
+        $this->assertEquals('2 sekundės', I18n::_('%d seconds', 2), '2 seconds in Lithuanian');
+        $this->assertEquals('10 sekundžių', I18n::_('%d seconds', 10), '10 seconds in Lithuanian');
+        $this->assertEquals('22 sekundės', I18n::_('%d seconds', 22), '22 seconds in Lithuanian');
+        $this->assertEquals('110 sekundžių', I18n::_('%d seconds', 110), '110 seconds in Lithuanian');
+        $this->assertEquals('111 sekundžių', I18n::_('%d seconds', 111), '111 seconds in Lithuanian');
+        $this->assertEquals('120 sekundžių', I18n::_('%d seconds', 120), '120 seconds in Lithuanian');
+    }
+
     public function testBrowserLanguageNoDetection()
     public function testBrowserLanguageNoDetection()
     {
     {
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'no;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'no;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
@@ -177,14 +191,14 @@ class I18nTest extends TestCase
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'sl;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
         $_SERVER['HTTP_ACCEPT_LANGUAGE'] = 'sl;q=0.8,en-GB;q=0.6,en-US;q=0.4,en;q=0.2';
         I18n::loadTranslations();
         I18n::loadTranslations();
         $this->assertEquals('sl', I18n::getLanguage(), 'browser language sl');
         $this->assertEquals('sl', I18n::getLanguage(), 'browser language sl');
-        $this->assertEquals('0 ura',  I18n::_('%d hours', 0), '0 hours in Slovene');
-        $this->assertEquals('1 uri',  I18n::_('%d hours', 1), '1 hour in Slovene');
-        $this->assertEquals('2 ure', I18n::_('%d hours', 2), '2 hours in Slovene');
-        $this->assertEquals('3 ur',  I18n::_('%d hours', 3), '3 hours in Slovene');
-        $this->assertEquals('11 ura',  I18n::_('%d hours', 11), '11 hours in Slovene');
-        $this->assertEquals('101 uri',  I18n::_('%d hours', 101), '101 hours in Slovene');
-        $this->assertEquals('102 ure', I18n::_('%d hours', 102), '102 hours in Slovene');
-        $this->assertEquals('104 ur',  I18n::_('%d hours', 104), '104 hours in Slovene');
+        $this->assertEquals('0 ur',  I18n::_('%d hours', 0), '0 hours in Slovene');
+        $this->assertEquals('1 ura',  I18n::_('%d hours', 1), '1 hour in Slovene');
+        $this->assertEquals('2 uri', I18n::_('%d hours', 2), '2 hours in Slovene');
+        $this->assertEquals('3 ure',  I18n::_('%d hours', 3), '3 hours in Slovene');
+        $this->assertEquals('11 ur',  I18n::_('%d hours', 11), '11 hours in Slovene');
+        $this->assertEquals('101 ura',  I18n::_('%d hours', 101), '101 hours in Slovene');
+        $this->assertEquals('102 uri', I18n::_('%d hours', 102), '102 hours in Slovene');
+        $this->assertEquals('104 ure',  I18n::_('%d hours', 104), '104 hours in Slovene');
     }
     }
 
 
     public function testBrowserLanguageCsDetection()
     public function testBrowserLanguageCsDetection()