Skip to content

Commit cf71618

Browse files
committed
[BUGFIX] Ensure correct formatting in PHP 8.4
Since php 8.4 the ext-intl class `NumberFormatter` expects a strict locale string and throws a `ValueError` if the given value cannot get parsed. Together with the `uc` of TYPO3, which holds the backend language for the currently logged-in user, this could be set to `default`, which is no valid locale. New TYPO3 instances will write a `uc['lang']=''`, if the person uses `englisch`, which is the default, but older used to write `us['lang']='default'`, which is still correct. This throws the `ValueError` if the NumberFormatter should format with this setting. Introduce the `Locales` class as a constructor argument to the `UsageService` and use the static methods converting the given uc-value to a correct language string ensuring NumberFormatter doing his job. Fixes #486
1 parent d6684db commit cf71618

File tree

3 files changed

+49
-6
lines changed

3 files changed

+49
-6
lines changed

Classes/Service/UsageService.php

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,18 @@
77
use DeepL\Usage;
88
use TYPO3\CMS\Backend\Toolbar\Enumeration\InformationStatus;
99
use TYPO3\CMS\Core\Authentication\BackendUserAuthentication;
10+
use TYPO3\CMS\Core\Localization\Locales;
1011
use TYPO3\CMS\Core\Type\ContextualFeedbackSeverity;
1112
use WebVision\Deepltranslate\Core\ClientInterface;
1213
use WebVision\Deepltranslate\Core\Event\Listener\UsageToolBarEventListener;
1314
use WebVision\Deepltranslate\Core\Hooks\UsageProcessAfterFinishHook;
1415

1516
final class UsageService implements UsageServiceInterface
1617
{
17-
protected ClientInterface $client;
18-
1918
public function __construct(
20-
ClientInterface $client
19+
private readonly ClientInterface $client,
20+
private readonly Locales $locales
2121
) {
22-
$this->client = $client;
2322
}
2423

2524
public function getCurrentUsage(): ?Usage
@@ -61,14 +60,16 @@ public function isTranslateLimitExceeded(): bool
6160
*/
6261
public function formatNumber(int $number)
6362
{
64-
$language = 'en';
63+
$language = 'default';
6564
if ($this->getBackendUser() !== null) {
6665
$uc = $this->getBackendUser()->uc;
6766
if (is_array($uc) && array_key_exists('lang', $uc)) {
6867
$language = $uc['lang'];
6968
}
7069
}
71-
$numberFormatter = new \NumberFormatter($language, \NumberFormatter::DECIMAL);
70+
71+
$locale = $this->locales->createLocale($language);
72+
$numberFormatter = new \NumberFormatter($locale->getLanguageCode(), \NumberFormatter::DECIMAL);
7273
return $numberFormatter->format($number);
7374
}
7475

Tests/Functional/Services/Fixtures/Pages.csv

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,9 @@ pages,,,,,
55
tt_content,
66
,uid,pid,header,CType,bodytext
77
,3,1,"DeepL-Functional-Test Element","text",""
8+
"be_users"
9+
,"uid","pid","tstamp","username","password","admin","disable","starttime","endtime","options","crdate","workspace_perms","deleted","TSconfig","lastlogin","workspace_id","uc"
10+
# The password is "password"
11+
# The user has a broken/outdated uc lang configuration. This is required testing the behaviour of #486
12+
,1,0,1366642540,"admin","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,0,0,0,0,1366642540,1,0,,1371033743,0,"a:19:{s:10:""moduleData"";a:7:{s:28:""dashboard/current_dashboard/"";s:40:""764f4d1012c7b870a98ffd1e8dd3ff5e133cb451"";s:9:""scheduler"";a:1:{s:6:""action"";s:16:""scheduler_manage"";}s:13:""system_config"";a:1:{s:4:""tree"";s:3:""tca"";}s:10:""web_layout"";a:3:{s:8:""function"";s:1:""2"";s:8:""language"";s:1:""2"";s:10:""showHidden"";b:1;}s:10:""FormEngine"";a:2:{i:0;a:2:{s:32:""c31c3d00814edbf9b2ddab640af3f55d"";a:5:{i:0;s:116:""Bacon ipsum dolor sit strong amet capicola jerky pork chop rump shoulder shank. Shankle strip steak pig salami link."";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:14;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:34:""&edit%5Btt_content%5D%5B14%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:14;s:3:""pid"";i:10;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:99:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=10#element-tt_content-14"";}s:32:""c312013d83c1a6ad7fec8b36a37ba3c8"";a:5:{i:0;s:25:""TYPO3 Styleguide Frontend"";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:1;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:33:""&edit%5Btt_content%5D%5B1%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:1;s:3:""pid"";i:1;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:98:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=1&#element-tt_content-1"";}}i:1;s:32:""d63be24a7702dd6c8c0504bfe838a532"";}s:57:""TYPO3\CMS\Backend\Utility\BackendUtility::getUpdateSignal"";a:0:{}s:16:""opendocs::recent"";a:5:{s:32:""d63be24a7702dd6c8c0504bfe838a532"";a:5:{i:0;s:120:""Bacon ipsum dolor sit strong amet capicola jerky pork chop rump shoulder shank. Haxe Streifen Steak Schwein Salami Link."";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:44;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:34:""&edit%5Btt_content%5D%5B44%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:44;s:3:""pid"";i:8;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:99:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=8&#element-tt_content-44"";}s:32:""ffbd6ae78a9aa555f88d6295c30fb80c"";a:5:{i:0;s:116:""Bacon ipsum dolor sit strong amet capicola jerky pork chop rump shoulder shank. Shankle strip steak pig salami link."";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:10;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:34:""&edit%5Btt_content%5D%5B10%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:10;s:3:""pid"";i:8;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:98:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=8#element-tt_content-10"";}s:32:""f3a80bc04cdfd6ed305676c6deecde13"";a:5:{i:0;s:25:""TYPO3 Styleguide Frontend"";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:43;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:34:""&edit%5Btt_content%5D%5B43%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:43;s:3:""pid"";i:1;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:70:""/typo3/module/dashboard?token=dcec9d55204841dc643c2622b1dd11306044d7fa"";}s:32:""c312013d83c1a6ad7fec8b36a37ba3c8"";a:5:{i:0;s:25:""TYPO3 Styleguide Frontend"";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:1;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:33:""&edit%5Btt_content%5D%5B1%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:1;s:3:""pid"";i:1;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:97:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=1#element-tt_content-1"";}s:32:""9b967901d6c9df7fbe10e9cd1eacc0fe"";a:5:{i:0;s:24:""styleguide frontend demo"";i:1;a:5:{s:4:""edit"";a:1:{s:5:""pages"";a:1:{i:1;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";a:1:{s:5:""pages"";a:1:{s:16:""sys_language_uid"";s:1:""0"";}}s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:76:""&edit%5Bpages%5D%5B1%5D=edit&overrideVals%5Bpages%5D%5Bsys_language_uid%5D=0"";i:3;a:5:{s:5:""table"";s:5:""pages"";s:3:""uid"";i:1;s:3:""pid"";i:0;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:217:""/typo3/record/edit?token=ae076486b94686606614f90d8a155ec1f785183e&edit%5Btt_content%5D%5B14%5D=edit&returnUrl=/typo3/module/web/layout?token%3D2fb2757ce2a0f7275162273a7363c3cdf5ae31e0%26id%3D10%23element-tt_content-14"";}}}s:14:""emailMeAtLogin"";i:0;s:8:""titleLen"";s:2:""50"";s:20:""edit_docModuleUpload"";i:1;s:15:""moduleSessionID"";a:7:{s:28:""dashboard/current_dashboard/"";s:40:""e47ba98e40d443d825f5d08a6545e37bbc86bac5"";s:9:""scheduler"";s:40:""e47ba98e40d443d825f5d08a6545e37bbc86bac5"";s:13:""system_config"";s:40:""e47ba98e40d443d825f5d08a6545e37bbc86bac5"";s:10:""web_layout"";s:40:""e47ba98e40d443d825f5d08a6545e37bbc86bac5"";s:10:""FormEngine"";s:40:""ac1694175250c17ad2c05ef590e41376512745be"";s:57:""TYPO3\CMS\Backend\Utility\BackendUtility::getUpdateSignal"";s:40:""75d90be2aa2bf0625bafeee69c3fdd82f726db12"";s:16:""opendocs::recent"";s:40:""ac1694175250c17ad2c05ef590e41376512745be"";}s:8:""realName"";s:0:"""";s:5:""email"";s:0:"""";s:8:""password"";s:0:"""";s:9:""password2"";s:0:"""";s:6:""avatar"";s:0:"""";s:4:""lang"";s:7:""default"";s:11:""startModule"";s:0:"""";s:25:""showHiddenFilesAndFolders"";i:0;s:10:""copyLevels"";s:0:"""";s:18:""resetConfiguration"";s:0:"""";s:12:""mfaProviders"";s:0:"""";s:18:""backendTitleFormat"";s:10:""titleFirst"";s:11:""colorScheme"";s:4:""auto"";s:5:""theme"";s:6:""modern"";}"
13+
,2,0,1366642540,"admin","$1$tCrlLajZ$C0sikFQQ3SWaFAZ1Me0Z/1",1,0,0,0,0,1366642540,1,0,,1371033743,0,"a:19:{s:10:""moduleData"";a:7:{s:28:""dashboard/current_dashboard/"";s:40:""764f4d1012c7b870a98ffd1e8dd3ff5e133cb451"";s:9:""scheduler"";a:1:{s:6:""action"";s:16:""scheduler_manage"";}s:13:""system_config"";a:1:{s:4:""tree"";s:3:""tca"";}s:10:""web_layout"";a:3:{s:8:""function"";s:1:""2"";s:8:""language"";s:1:""2"";s:10:""showHidden"";b:1;}s:10:""FormEngine"";a:2:{i:0;a:2:{s:32:""c31c3d00814edbf9b2ddab640af3f55d"";a:5:{i:0;s:116:""Bacon ipsum dolor sit strong amet capicola jerky pork chop rump shoulder shank. Shankle strip steak pig salami link."";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:14;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:34:""&edit%5Btt_content%5D%5B14%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:14;s:3:""pid"";i:10;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:99:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=10#element-tt_content-14"";}s:32:""c312013d83c1a6ad7fec8b36a37ba3c8"";a:5:{i:0;s:25:""TYPO3 Styleguide Frontend"";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:1;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:33:""&edit%5Btt_content%5D%5B1%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:1;s:3:""pid"";i:1;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:98:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=1&#element-tt_content-1"";}}i:1;s:32:""d63be24a7702dd6c8c0504bfe838a532"";}s:57:""TYPO3\CMS\Backend\Utility\BackendUtility::getUpdateSignal"";a:0:{}s:16:""opendocs::recent"";a:5:{s:32:""d63be24a7702dd6c8c0504bfe838a532"";a:5:{i:0;s:120:""Bacon ipsum dolor sit strong amet capicola jerky pork chop rump shoulder shank. Haxe Streifen Steak Schwein Salami Link."";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:44;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:34:""&edit%5Btt_content%5D%5B44%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:44;s:3:""pid"";i:8;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:99:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=8&#element-tt_content-44"";}s:32:""ffbd6ae78a9aa555f88d6295c30fb80c"";a:5:{i:0;s:116:""Bacon ipsum dolor sit strong amet capicola jerky pork chop rump shoulder shank. Shankle strip steak pig salami link."";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:10;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:34:""&edit%5Btt_content%5D%5B10%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:10;s:3:""pid"";i:8;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:98:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=8#element-tt_content-10"";}s:32:""f3a80bc04cdfd6ed305676c6deecde13"";a:5:{i:0;s:25:""TYPO3 Styleguide Frontend"";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:43;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:34:""&edit%5Btt_content%5D%5B43%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:43;s:3:""pid"";i:1;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:70:""/typo3/module/dashboard?token=dcec9d55204841dc643c2622b1dd11306044d7fa"";}s:32:""c312013d83c1a6ad7fec8b36a37ba3c8"";a:5:{i:0;s:25:""TYPO3 Styleguide Frontend"";i:1;a:5:{s:4:""edit"";a:1:{s:10:""tt_content"";a:1:{i:1;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";N;s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:33:""&edit%5Btt_content%5D%5B1%5D=edit"";i:3;a:5:{s:5:""table"";s:10:""tt_content"";s:3:""uid"";i:1;s:3:""pid"";i:1;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:97:""/typo3/module/web/layout?token=2fb2757ce2a0f7275162273a7363c3cdf5ae31e0&id=1#element-tt_content-1"";}s:32:""9b967901d6c9df7fbe10e9cd1eacc0fe"";a:5:{i:0;s:24:""styleguide frontend demo"";i:1;a:5:{s:4:""edit"";a:1:{s:5:""pages"";a:1:{i:1;s:4:""edit"";}}s:7:""defVals"";N;s:12:""overrideVals"";a:1:{s:5:""pages"";a:1:{s:16:""sys_language_uid"";s:1:""0"";}}s:11:""columnsOnly"";N;s:6:""noView"";N;}i:2;s:76:""&edit%5Bpages%5D%5B1%5D=edit&overrideVals%5Bpages%5D%5Bsys_language_uid%5D=0"";i:3;a:5:{s:5:""table"";s:5:""pages"";s:3:""uid"";i:1;s:3:""pid"";i:0;s:3:""cmd"";s:4:""edit"";s:12:""deleteAccess"";b:1;}i:4;s:217:""/typo3/record/edit?token=ae076486b94686606614f90d8a155ec1f785183e&edit%5Btt_content%5D%5B14%5D=edit&returnUrl=/typo3/module/web/layout?token%3D2fb2757ce2a0f7275162273a7363c3cdf5ae31e0%26id%3D10%23element-tt_content-14"";}}}s:14:""emailMeAtLogin"";i:0;s:8:""titleLen"";s:2:""50"";s:20:""edit_docModuleUpload"";i:1;s:15:""moduleSessionID"";a:7:{s:28:""dashboard/current_dashboard/"";s:40:""e47ba98e40d443d825f5d08a6545e37bbc86bac5"";s:9:""scheduler"";s:40:""e47ba98e40d443d825f5d08a6545e37bbc86bac5"";s:13:""system_config"";s:40:""e47ba98e40d443d825f5d08a6545e37bbc86bac5"";s:10:""web_layout"";s:40:""e47ba98e40d443d825f5d08a6545e37bbc86bac5"";s:10:""FormEngine"";s:40:""ac1694175250c17ad2c05ef590e41376512745be"";s:57:""TYPO3\CMS\Backend\Utility\BackendUtility::getUpdateSignal"";s:40:""75d90be2aa2bf0625bafeee69c3fdd82f726db12"";s:16:""opendocs::recent"";s:40:""ac1694175250c17ad2c05ef590e41376512745be"";}s:8:""realName"";s:0:"""";s:5:""email"";s:0:"""";s:8:""password"";s:0:"""";s:9:""password2"";s:0:"""";s:6:""avatar"";s:0:"""";s:4:""lang"";s:2:""de"";s:11:""startModule"";s:0:"""";s:25:""showHiddenFilesAndFolders"";i:0;s:10:""copyLevels"";s:0:"""";s:18:""resetConfiguration"";s:0:"""";s:12:""mfaProviders"";s:0:"""";s:18:""backendTitleFormat"";s:10:""titleFirst"";s:11:""colorScheme"";s:4:""auto"";s:5:""theme"";s:6:""modern"";}"

Tests/Functional/Services/UsageServiceTest.php

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use DeepL\Usage;
88
use DeepL\UsageDetail;
9+
use PHPUnit\Framework\Attributes\DataProvider;
910
use PHPUnit\Framework\Attributes\Test;
1011
use WebVision\Deepltranslate\Core\Service\DeeplService;
1112
use WebVision\Deepltranslate\Core\Service\ProcessingInstruction;
@@ -104,4 +105,39 @@ public function checkHTMLMarkupsIsNotPartOfLimit(): void
104105
static::assertInstanceOf(UsageDetail::class, $character);
105106
static::assertEquals(strlen($translateContent), $character->count);
106107
}
108+
109+
public static function numberFormatterLocalesDataProvider(): \Generator
110+
{
111+
yield 'Default formats to english' => [
112+
'user' => 1,
113+
'number' => 20000,
114+
'expectedFormat' => '20,000',
115+
];
116+
yield 'BE uc lang "de" formats german' => [
117+
'user' => 2,
118+
'number' => 93254850,
119+
'expectedFormat' => '93.254.850',
120+
];
121+
}
122+
/**
123+
* This test ensures that in PHP >=8.4 the NumberFormatter works correctly.
124+
* With migrated TYPO3 data there is the possibility that uc['lang'] is set to 'default',
125+
* which is no correct format for a locale the number formatter accepts. THis will lead
126+
* to an error during initialisation.
127+
*/
128+
#[Test]
129+
#[DataProvider('numberFormatterLocalesDataProvider')]
130+
public function numberFormatRespectsLocalesAndDefault(
131+
int $user,
132+
int $number,
133+
string $expectedFormat
134+
): void {
135+
$this->importCSVDataSet(__DIR__ . '/Fixtures/Pages.csv');
136+
$this->setUpBackendUser($user);
137+
/** @var UsageService $usageService */
138+
$usageService = $this->get(UsageService::class);
139+
140+
$formatted = $usageService->formatNumber($number);
141+
static::assertEquals($expectedFormat, $formatted);
142+
}
107143
}

0 commit comments

Comments
 (0)