Split user language (UI translation) from locale (formatted values) (#4595)
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,7 @@
|
||||
parameters:
|
||||
locale: en
|
||||
# the original list of all locales is the next line, it can be used to regenerate the locale list in case new locales will be added
|
||||
#app_locales: ar|cs|da|de|el|en|eo|es|eu|fa|fi|fo|fr|he|hr|hu|it|ja|ko|nb_NO|nl|pl|pt|ro|ru|sk|sv|tr|uk|vi|zh_CN|zh_Hant
|
||||
app_locales: ar|cs|da|de|el|en|eo|es|eu|fa|fi|fo|fr|he|hr|hu|it|ja|ko|nb_NO|nl|pl|pt|pt_BR|ro|ru|sk|sv|tr|uk|vi|zh_CN|zh_Hant|cs_CZ|da_DK|da_GL|de_AT|de_BE|de_CH|de_DE|de_IT|de_LI|de_LU|el_CY|el_GR|en_AE|en_AG|en_AI|en_AS|en_AT|en_AU|en_BB|en_BE|en_BI|en_BM|en_BS|en_BW|en_BZ|en_CA|en_CC|en_CH|en_CK|en_CM|en_CX|en_CY|en_DE|en_DK|en_DM|en_ER|en_FI|en_FJ|en_FK|en_FM|en_GB|en_GD|en_GG|en_GH|en_GI|en_GM|en_GU|en_GY|en_HK|en_IE|en_IL|en_IM|en_IN|en_IO|en_JE|en_JM|en_KE|en_KI|en_KN|en_KY|en_LC|en_LR|en_LS|en_MG|en_MH|en_MO|en_MP|en_MS|en_MT|en_MU|en_MV|en_MW|en_MY|en_NA|en_NF|en_NG|en_NL|en_NR|en_NU|en_NZ|en_PG|en_PH|en_PK|en_PN|en_PR|en_PW|en_RW|en_SB|en_SC|en_SD|en_SE|en_SG|en_SH|en_SI|en_SL|en_SS|en_SX|en_SZ|en_TC|en_TK|en_TO|en_TT|en_TV|en_TZ|en_UG|en_UM|en_US|en_VC|en_VG|en_VI|en_VU|en_WS|en_ZA|en_ZM|en_ZW|es_AR|es_BO|es_BR|es_BZ|es_CL|es_CO|es_CR|es_CU|es_DO|es_EC|es_ES|es_GQ|es_GT|es_HN|es_MX|es_NI|es_PA|es_PE|es_PH|es_PR|es_PY|es_SV|es_US|es_UY|es_VE|eu_ES|fa_AF|fa_IR|fi_FI|fo_DK|fo_FO|fr_BE|fr_BF|fr_BI|fr_BJ|fr_BL|fr_CA|fr_CD|fr_CF|fr_CG|fr_CH|fr_CI|fr_CM|fr_DJ|fr_DZ|fr_FR|fr_GA|fr_GF|fr_GN|fr_GP|fr_GQ|fr_HT|fr_KM|fr_LU|fr_MA|fr_MC|fr_MF|fr_MG|fr_ML|fr_MQ|fr_MR|fr_MU|fr_NC|fr_NE|fr_PF|fr_PM|fr_RE|fr_RW|fr_SC|fr_SN|fr_SY|fr_TD|fr_TG|fr_TN|fr_VU|fr_WF|fr_YT|he_IL|hr_BA|hr_HR|hu_HU|it_CH|it_IT|it_SM|it_VA|ja_JP|ko_KP|ko_KR|nl_AW|nl_BE|nl_BQ|nl_CW|nl_NL|nl_SR|nl_SX|pl_PL|pt_AO|pt_CH|pt_CV|pt_GQ|pt_GW|pt_LU|pt_MO|pt_MZ|pt_PT|pt_ST|pt_TL|ro_MD|ro_RO|ru_BY|ru_KG|ru_KZ|ru_MD|ru_RU|ru_UA|sk_SK|sv_AX|sv_FI|sv_SE|tr_CY|tr_TR|vi_VN
|
||||
# can be regenerated with "bin/console kimai:reset:locales"
|
||||
app_locales: ar|cs|da|de|de_CH|el|en|eo|es|eu|fa|fi|fo|fr|he|hr|hu|id|it|ja|ko|nb_NO|nl|pa|pl|pt|pt_BR|ro|ru|sk|sl|sv|tr|uk|vi|zh_CN|zh_Hant|cs_CZ|da_DK|da_GL|de_AT|de_BE|de_CH|de_DE|de_IT|de_LI|de_LU|el_CY|el_GR|en_AE|en_AG|en_AI|en_AS|en_AT|en_AU|en_BB|en_BE|en_BI|en_BM|en_BS|en_BW|en_BZ|en_CA|en_CC|en_CH|en_CK|en_CM|en_CX|en_CY|en_DE|en_DG|en_DK|en_DM|en_ER|en_FI|en_FJ|en_FK|en_FM|en_GB|en_GD|en_GG|en_GH|en_GI|en_GM|en_GU|en_GY|en_HK|en_ID|en_IE|en_IL|en_IM|en_IN|en_IO|en_JE|en_JM|en_KE|en_KI|en_KN|en_KY|en_LC|en_LR|en_LS|en_MG|en_MH|en_MO|en_MP|en_MS|en_MT|en_MU|en_MV|en_MW|en_MY|en_NA|en_NF|en_NG|en_NH|en_NL|en_NR|en_NU|en_NZ|en_PG|en_PH|en_PK|en_PN|en_PR|en_PW|en_RH|en_RW|en_SB|en_SC|en_SD|en_SE|en_SG|en_SH|en_SI|en_SL|en_SS|en_SX|en_SZ|en_TC|en_TK|en_TO|en_TT|en_TV|en_TZ|en_UG|en_UM|en_US|en_VC|en_VG|en_VI|en_VU|en_WS|en_ZA|en_ZM|en_ZW|es_AR|es_BO|es_BR|es_BZ|es_CL|es_CO|es_CR|es_CU|es_DO|es_EA|es_EC|es_ES|es_GQ|es_GT|es_HN|es_IC|es_MX|es_NI|es_PA|es_PE|es_PH|es_PR|es_PY|es_SV|es_US|es_UY|es_VE|eu_ES|fa_AF|fa_IR|fi_FI|fo_DK|fo_FO|fr_BE|fr_BF|fr_BI|fr_BJ|fr_BL|fr_CA|fr_CD|fr_CF|fr_CG|fr_CH|fr_CI|fr_CM|fr_DJ|fr_DZ|fr_FR|fr_GA|fr_GF|fr_GN|fr_GP|fr_GQ|fr_HT|fr_KM|fr_LU|fr_MA|fr_MC|fr_MF|fr_MG|fr_ML|fr_MQ|fr_MR|fr_MU|fr_NC|fr_NE|fr_PF|fr_PM|fr_RE|fr_RW|fr_SC|fr_SN|fr_SY|fr_TD|fr_TG|fr_TN|fr_VU|fr_WF|fr_YT|he_IL|hr_BA|hr_HR|hu_HU|it_CH|it_IT|it_SM|it_VA|ja_JP|ko_CN|ko_KP|ko_KR|nl_AW|nl_BE|nl_BQ|nl_CW|nl_NL|nl_SR|nl_SX|pl_PL|pt_AO|pt_BR|pt_CH|pt_CV|pt_GQ|pt_GW|pt_LU|pt_MO|pt_MZ|pt_PT|pt_ST|pt_TL|ro_MD|ro_RO|ru_BY|ru_KG|ru_KZ|ru_MD|ru_RU|ru_UA|sk_SK|sv_AX|sv_FI|sv_SE|tr_CY|tr_TR|uk_UA|vi_VN
|
||||
|
||||
services:
|
||||
# default configuration for services in *this* file
|
||||
|
||||
35
phpstan.neon
35
phpstan.neon
@@ -492,11 +492,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Command/RegenerateLocalesCommand.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Command\\\\RegenerateLocalesCommand\\:\\:\\$rtlLocales type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Command/RegenerateLocalesCommand.php
|
||||
|
||||
-
|
||||
message: "#^Cannot call method find\\(\\) on Symfony\\\\Component\\\\Console\\\\Application\\|null\\.$#"
|
||||
count: 4
|
||||
@@ -652,26 +647,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Configuration/LdapConfiguration.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Configuration\\\\LocaleService\\:\\:__construct\\(\\) has parameter \\$languageSettings with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/LocaleService.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Configuration\\\\LocaleService\\:\\:getDateFormat\\(\\) should return string but returns bool\\|string\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/LocaleService.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Configuration\\\\LocaleService\\:\\:getTimeFormat\\(\\) should return string but returns bool\\|string\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/LocaleService.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Configuration\\\\LocaleService\\:\\:isRightToLeft\\(\\) should return bool but returns bool\\|string\\.$#"
|
||||
count: 1
|
||||
path: src/Configuration/LocaleService.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Configuration\\\\SamlConfiguration\\:\\:getAttributeMapping\\(\\) return type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
@@ -1607,11 +1582,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Entity/User.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\User\\:\\:getLocale\\(\\) should return string but returns bool\\|float\\|int\\|string\\|null\\.$#"
|
||||
count: 1
|
||||
path: src/Entity/User.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Entity\\\\User\\:\\:getMemberships\\(\\) return type with generic interface Doctrine\\\\Common\\\\Collections\\\\Collection does not specify its types\\: TKey, T$#"
|
||||
count: 1
|
||||
@@ -5407,11 +5377,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Utils/FileHelper.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Utils\\\\FormFormatConverter\\:\\:\\$formatConvertRules type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: src/Utils/FormFormatConverter.php
|
||||
|
||||
-
|
||||
message: "#^Property App\\\\Utils\\\\JavascriptFormatConverter\\:\\:\\$formatConvertRules type has no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -83,7 +83,7 @@ final class ValidationFailedExceptionErrorHandler implements SubscribingHandlerI
|
||||
$user = $this->security->getUser();
|
||||
|
||||
if ($user !== null) {
|
||||
$locale = $user->getLocale();
|
||||
$locale = $user->getLanguage();
|
||||
}
|
||||
|
||||
if (null !== $error->getPlural()) {
|
||||
|
||||
@@ -29,15 +29,22 @@ use Symfony\Component\Intl\Locales;
|
||||
#[AsCommand(name: 'kimai:reset:locales')]
|
||||
final class RegenerateLocalesCommand extends Command
|
||||
{
|
||||
private string $defaultDate = 'dd.MM.y';
|
||||
private string $defaultTime = 'HH:mm';
|
||||
private array $rtlLocales = [
|
||||
'ar' => true,
|
||||
'fa' => true,
|
||||
'he' => true,
|
||||
];
|
||||
/**
|
||||
* @var string[]
|
||||
*/
|
||||
private array $rtlLocales = ['ar', 'fa', 'he'];
|
||||
/**
|
||||
* new locales were added here, to shrink the list a little bit
|
||||
* this can be removed in the future, if there will ever be the need for it
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
private array $noRegionCode = ['ar', 'id', 'pa', 'sl'];
|
||||
|
||||
public function __construct(private LocaleService $localeService, private string $projectDirectory, private string $kernelEnvironment)
|
||||
public function __construct(
|
||||
private readonly string $projectDirectory,
|
||||
private readonly string $kernelEnvironment
|
||||
)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
@@ -55,32 +62,44 @@ final class RegenerateLocalesCommand extends Command
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$locales = $this->localeService->getAllLocales();
|
||||
|
||||
// detect all registered locales and allow to choose them as well, so people get to
|
||||
// choose the language for translation with the correct format of their location
|
||||
/*
|
||||
// find all available locales from the translation filenames
|
||||
$translationFilenames = glob($this->projectDirectory . DIRECTORY_SEPARATOR . 'translations/*.xlf');
|
||||
if ($translationFilenames === false) {
|
||||
$io->error('Failed reading translation files');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
$firstLevelLocales = [];
|
||||
foreach ($translationFilenames as $file) {
|
||||
$firstLevelLocales[] = explode('.', basename($file))[1];
|
||||
}
|
||||
$firstLevelLocales = array_unique($firstLevelLocales);
|
||||
$io->title('Locales found from translation files');
|
||||
$io->writeln(implode('|', $firstLevelLocales));
|
||||
|
||||
$secondLevel = [];
|
||||
foreach (Locales::getLocales() as $locale) {
|
||||
if (substr_count($locale, '_') === 1) {
|
||||
$baseLocale = substr($locale, 0, strpos($locale, '_'));
|
||||
if (in_array($baseLocale, $locales)) {
|
||||
$subLocale = substr($locale, strpos($locale, '_') + 1);
|
||||
if (!is_numeric($subLocale)) {
|
||||
$secondLevel[] = $locale;
|
||||
foreach (Locales::getLocales() as $localeCode) {
|
||||
$locale = explode('_', $localeCode);
|
||||
if (\count($locale) === 2 && !\in_array($locale[0], $this->noRegionCode, true)) {
|
||||
$baseLocale = $locale[0];
|
||||
if (\in_array($baseLocale, $firstLevelLocales)) {
|
||||
$regionCode = $locale[1];
|
||||
if (!is_numeric($regionCode)) {
|
||||
$secondLevel[] = $localeCode;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$locales = array_merge($locales, $secondLevel);
|
||||
*/
|
||||
|
||||
sort($firstLevelLocales);
|
||||
sort($secondLevel);
|
||||
|
||||
// keep the locales that have translation filesat the begin
|
||||
// the config is than easier to read and the locales will be sorted in the UI anyway
|
||||
$locales = array_merge($firstLevelLocales, $secondLevel);
|
||||
|
||||
$appLocales = [];
|
||||
$defaults = [
|
||||
'date' => $this->defaultDate,
|
||||
'time' => $this->defaultTime,
|
||||
'rtl' => false,
|
||||
];
|
||||
|
||||
// make sure all allowed locales are registered
|
||||
foreach ($locales as $locale) {
|
||||
@@ -88,11 +107,13 @@ final class RegenerateLocalesCommand extends Command
|
||||
continue;
|
||||
}
|
||||
|
||||
$appLocales[$locale] = $defaults;
|
||||
$appLocales[$locale] = LocaleService::DEFAULT_SETTINGS;
|
||||
}
|
||||
|
||||
// make sure all keys are registered for every locale
|
||||
foreach ($appLocales as $locale => $settings) {
|
||||
$settings['translation'] = \in_array($locale, $firstLevelLocales, true);
|
||||
|
||||
// these are completely new since v2
|
||||
// calculate everything with IntlFormatter
|
||||
$shortDate = new \IntlDateFormatter($locale, \IntlDateFormatter::SHORT, \IntlDateFormatter::NONE);
|
||||
@@ -115,14 +136,50 @@ final class RegenerateLocalesCommand extends Command
|
||||
$rtlLocale = substr($rtlLocale, 0, strpos($rtlLocale, '_'));
|
||||
}
|
||||
|
||||
if (\array_key_exists($rtlLocale, $this->rtlLocales)) {
|
||||
$settings['rtl'] = $this->rtlLocales[$rtlLocale];
|
||||
}
|
||||
$settings['rtl'] = \in_array($rtlLocale, $this->rtlLocales, true);
|
||||
|
||||
// pre-fill all formats with the default locale settings
|
||||
$appLocales[$locale] = $settings;
|
||||
}
|
||||
|
||||
$removableDuplicates = [];
|
||||
foreach ($appLocales as $locale => $setting) {
|
||||
$localeParts = explode('_', $locale);
|
||||
if (\count($localeParts) === 1) {
|
||||
continue;
|
||||
}
|
||||
// e.g. norwegian just exists with region code
|
||||
if (!\array_key_exists($localeParts[0], $appLocales)) {
|
||||
continue;
|
||||
}
|
||||
$baseLocaleSettings = $appLocales[$localeParts[0]];
|
||||
if ($baseLocaleSettings['time'] !== $setting['time']) {
|
||||
continue;
|
||||
}
|
||||
if ($baseLocaleSettings['date'] !== $setting['date']) {
|
||||
continue;
|
||||
}
|
||||
if ($setting['translation'] === true) {
|
||||
continue;
|
||||
}
|
||||
if ($baseLocaleSettings['rtl'] !== $setting['rtl']) {
|
||||
continue;
|
||||
}
|
||||
$removableDuplicates[] = $locale;
|
||||
}
|
||||
|
||||
$io->title('Redundant locales that will be skipped');
|
||||
$io->writeln(implode('|', $removableDuplicates));
|
||||
|
||||
foreach ($removableDuplicates as $duplicate) {
|
||||
unset($appLocales[$duplicate]);
|
||||
}
|
||||
|
||||
// in the future this list should be reduced to the list of available translations, but for a long time users
|
||||
// could choose from the entire list of all locales, so we likely have to keep that forever ...
|
||||
$io->title('List of app_locales for services.yaml');
|
||||
$io->writeln(implode('|', $locales));
|
||||
|
||||
ksort($appLocales);
|
||||
|
||||
$filename = 'config/locales.php';
|
||||
|
||||
@@ -70,7 +70,7 @@ final class UserLoginLinkCommand extends Command
|
||||
}
|
||||
|
||||
$request = new Request();
|
||||
$request->setLocale($user->getLocale());
|
||||
$request->setLocale($user->getLanguage());
|
||||
$this->requestStack->push($request);
|
||||
|
||||
$loginLinkDetails = $this->loginLink->createLoginLink($user, $request);
|
||||
|
||||
@@ -9,9 +9,21 @@
|
||||
|
||||
namespace App\Configuration;
|
||||
|
||||
use App\Entity\User;
|
||||
|
||||
final class LocaleService
|
||||
{
|
||||
public function __construct(private array $languageSettings)
|
||||
public const DEFAULT_SETTINGS = [
|
||||
'date' => 'dd.MM.y',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
* @param array<string, array{'date': string, 'time': string, 'translation': bool}> $languageSettings
|
||||
*/
|
||||
public function __construct(private readonly array $languageSettings)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -25,6 +37,18 @@ final class LocaleService
|
||||
return array_keys($this->languageSettings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array with all language codes that have translations.
|
||||
*
|
||||
* @return string[]
|
||||
*/
|
||||
public function getTranslatedLocales(): array
|
||||
{
|
||||
return array_keys(array_filter($this->languageSettings, function (array $setting) {
|
||||
return $setting['translation'];
|
||||
}));
|
||||
}
|
||||
|
||||
public function isKnownLocale(string $language): bool
|
||||
{
|
||||
return \in_array($language, $this->getAllLocales());
|
||||
@@ -38,7 +62,7 @@ final class LocaleService
|
||||
*/
|
||||
public function getDateFormat(string $locale): string
|
||||
{
|
||||
return $this->getConfig('date', $locale);
|
||||
return (string) $this->getConfig('date', $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -49,7 +73,7 @@ final class LocaleService
|
||||
*/
|
||||
public function getTimeFormat(string $locale): string
|
||||
{
|
||||
return $this->getConfig('time', $locale);
|
||||
return (string) $this->getConfig('time', $locale);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -76,7 +100,34 @@ final class LocaleService
|
||||
|
||||
public function isRightToLeft(string $locale): bool
|
||||
{
|
||||
return $this->getConfig('rtl', $locale);
|
||||
return (bool) $this->getConfig('rtl', $locale);
|
||||
}
|
||||
|
||||
public function isTranslated(string $locale): bool
|
||||
{
|
||||
return (bool) $this->getConfig('translation', $locale);
|
||||
}
|
||||
|
||||
public function getNearestTranslationLocale(string $locale): string
|
||||
{
|
||||
if (!$this->isKnownLocale($locale)) {
|
||||
$parts = explode('_', $locale);
|
||||
if (\count($parts) !== 2 || \strlen($parts[0]) !== 2 || !$this->isKnownLocale($parts[0])) {
|
||||
return User::DEFAULT_LANGUAGE;
|
||||
}
|
||||
$locale = $parts[0];
|
||||
}
|
||||
|
||||
if (!$this->isTranslated($locale)) {
|
||||
$base = explode('_', $locale)[0];
|
||||
if (!$this->isTranslated($base)) {
|
||||
return User::DEFAULT_LANGUAGE;
|
||||
}
|
||||
|
||||
return $base;
|
||||
}
|
||||
|
||||
return $locale;
|
||||
}
|
||||
|
||||
public function is24Hour(string $locale): bool
|
||||
|
||||
@@ -22,13 +22,13 @@ use App\Form\Type\CustomerTypePatternType;
|
||||
use App\Form\Type\DatePickerType;
|
||||
use App\Form\Type\DateTimeTextType;
|
||||
use App\Form\Type\DayTimeType;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\MinuteIncrementType;
|
||||
use App\Form\Type\ProjectTypePatternType;
|
||||
use App\Form\Type\RoundingModeType;
|
||||
use App\Form\Type\SkinType;
|
||||
use App\Form\Type\TimezoneType;
|
||||
use App\Form\Type\TrackingModeType;
|
||||
use App\Form\Type\UserLanguageType;
|
||||
use App\Form\Type\WeekDaysType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use App\Timesheet\LockdownService;
|
||||
@@ -504,7 +504,7 @@ final class SystemConfigurationController extends AbstractController
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration('defaults.user.language'))
|
||||
->setLabel('language')
|
||||
->setType(LanguageType::class)
|
||||
->setType(UserLanguageType::class)
|
||||
->setOptions(['help' => 'default_value_new']),
|
||||
(new Configuration('defaults.user.theme'))
|
||||
->setLabel('skin')
|
||||
|
||||
@@ -11,9 +11,10 @@ namespace App\Controller;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Form\Type\LanguageType;
|
||||
use App\Form\Type\SkinType;
|
||||
use App\Form\Type\TimezoneType;
|
||||
use App\Form\Type\UserLanguageType;
|
||||
use App\Form\Type\UserLocaleType;
|
||||
use App\Form\UserPasswordType;
|
||||
use App\User\UserService;
|
||||
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||
@@ -44,14 +45,16 @@ final class WizardController extends AbstractController
|
||||
|
||||
if ($wizard === 'profile') {
|
||||
$data = [
|
||||
UserPreference::LOCALE => $request->getLocale(),
|
||||
UserPreference::LANGUAGE => $user->getPreferenceValue(UserPreference::LANGUAGE, $request->getLocale(), false),
|
||||
UserPreference::LOCALE => $user->getPreferenceValue(UserPreference::LOCALE, $request->getLocale(), false),
|
||||
UserPreference::TIMEZONE => $user->getTimezone(),
|
||||
UserPreference::SKIN => $user->getSkin(),
|
||||
'reload' => '0',
|
||||
];
|
||||
|
||||
$form = $this->createFormBuilder($data)
|
||||
->add(UserPreference::LOCALE, LanguageType::class)
|
||||
->add(UserPreference::LANGUAGE, UserLanguageType::class)
|
||||
->add(UserPreference::LOCALE, UserLocaleType::class, ['help' => null])
|
||||
->add(UserPreference::TIMEZONE, TimezoneType::class)
|
||||
->add(UserPreference::SKIN, SkinType::class)
|
||||
->add('reload', HiddenType::class)
|
||||
@@ -69,7 +72,8 @@ final class WizardController extends AbstractController
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
/** @var array<string, string> $data */
|
||||
$data = $form->getData();
|
||||
$user->setLanguage($data[UserPreference::LOCALE]);
|
||||
$user->setLanguage($data[UserPreference::LANGUAGE]);
|
||||
$user->setLocale($data[UserPreference::LOCALE]);
|
||||
$user->setTimezone($data[UserPreference::TIMEZONE]);
|
||||
$user->setPreferenceValue(UserPreference::SKIN, $data[UserPreference::SKIN]);
|
||||
$user->setWizardAsSeen('profile');
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
namespace App\DependencyInjection;
|
||||
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Kernel;
|
||||
use Symfony\Component\Config\Definition\Exception\InvalidConfigurationException;
|
||||
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||
@@ -112,12 +113,6 @@ final class AppExtension extends Extension
|
||||
$settings = include $directory . DIRECTORY_SEPARATOR . 'config/locales.php';
|
||||
|
||||
$appLocales = [];
|
||||
$defaults = [
|
||||
'date' => 'dd.MM.y',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
];
|
||||
|
||||
// make sure all allowed locales are registered
|
||||
foreach ($locales as $locale) {
|
||||
// unlikely that a locale disappears, but in case that a new symfony update comes with changed locales
|
||||
@@ -125,7 +120,7 @@ final class AppExtension extends Extension
|
||||
continue;
|
||||
}
|
||||
|
||||
$appLocales[$locale] = $defaults;
|
||||
$appLocales[$locale] = LocaleService::DEFAULT_SETTINGS;
|
||||
|
||||
if (\array_key_exists($locale, $settings)) {
|
||||
$appLocales[$locale] = array_merge($appLocales[$locale], $settings[$locale]);
|
||||
|
||||
@@ -318,7 +318,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
}
|
||||
|
||||
/**
|
||||
* Read-only list of of all visible user preferences.
|
||||
* Read-only list of all visible user preferences.
|
||||
*
|
||||
* @internal only for API usage
|
||||
* @return UserPreference[]
|
||||
@@ -334,6 +334,7 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
$skip = [
|
||||
UserPreference::TIMEZONE,
|
||||
UserPreference::LOCALE,
|
||||
UserPreference::LANGUAGE,
|
||||
UserPreference::SKIN,
|
||||
'calendar_initial_view',
|
||||
'login_initial_view',
|
||||
@@ -406,13 +407,22 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The locale used for formatting number, money, dates and times
|
||||
*/
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('language')]
|
||||
#[Serializer\SerializedName('locale')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(type: 'string')]
|
||||
public function getLocale(): string
|
||||
{
|
||||
return $this->getPreferenceValue(UserPreference::LOCALE, User::DEFAULT_LANGUAGE, false);
|
||||
// uses language as fallback, because the language was here before
|
||||
return (string) $this->getPreferenceValue(UserPreference::LOCALE, $this->getLanguage(), false);
|
||||
}
|
||||
|
||||
public function setLocale(?string $locale): void
|
||||
{
|
||||
$this->setPreferenceValue(UserPreference::LOCALE, $locale ?? User::DEFAULT_LANGUAGE);
|
||||
}
|
||||
|
||||
#[Serializer\VirtualProperty]
|
||||
@@ -424,17 +434,21 @@ class User implements UserInterface, EquatableInterface, ThemeUserInterface, Pas
|
||||
return $this->getPreferenceValue(UserPreference::TIMEZONE, date_default_timezone_get(), false);
|
||||
}
|
||||
|
||||
/**
|
||||
* The locale used for translations
|
||||
*/
|
||||
#[Serializer\VirtualProperty]
|
||||
#[Serializer\SerializedName('language')]
|
||||
#[Serializer\Groups(['User_Entity'])]
|
||||
#[OA\Property(type: 'string')]
|
||||
public function getLanguage(): string
|
||||
{
|
||||
return $this->getLocale();
|
||||
return (string) $this->getPreferenceValue(UserPreference::LANGUAGE, User::DEFAULT_LANGUAGE, false);
|
||||
}
|
||||
|
||||
public function setLanguage(?string $language): void
|
||||
{
|
||||
if ($language === null) {
|
||||
$language = User::DEFAULT_LANGUAGE;
|
||||
}
|
||||
$this->setPreferenceValue(UserPreference::LOCALE, $language);
|
||||
$this->setPreferenceValue(UserPreference::LANGUAGE, $language ?? User::DEFAULT_LANGUAGE);
|
||||
}
|
||||
|
||||
public function isFirstDayOfWeekSunday(): bool
|
||||
|
||||
@@ -28,7 +28,8 @@ class UserPreference
|
||||
public const HOURLY_RATE = 'hourly_rate';
|
||||
public const INTERNAL_RATE = 'internal_rate';
|
||||
public const SKIN = 'skin';
|
||||
public const LOCALE = 'language';
|
||||
public const LANGUAGE = 'language';
|
||||
public const LOCALE = 'locale';
|
||||
public const TIMEZONE = 'timezone';
|
||||
public const FIRST_WEEKDAY = 'first_weekday';
|
||||
public const WORK_HOURS_MONDAY = 'work_monday';
|
||||
|
||||
@@ -24,7 +24,11 @@ use Symfony\Component\Security\Core\Authentication\Token\Storage\TokenStorageInt
|
||||
*/
|
||||
final class ThemeOptionsSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private TokenStorageInterface $storage, private ContextHelper $helper, private LocaleService $localeService)
|
||||
public function __construct(
|
||||
private readonly TokenStorageInterface $storage,
|
||||
private readonly ContextHelper $helper,
|
||||
private readonly LocaleService $localeService
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -44,7 +48,7 @@ final class ThemeOptionsSubscriber implements EventSubscriberInterface
|
||||
|
||||
$this->helper->setAssetVersion((string) Constants::VERSION_ID);
|
||||
|
||||
if ($this->localeService->isRightToLeft(\Locale::getDefault())) {
|
||||
if ($this->localeService->isRightToLeft($event->getRequest()->getLocale())) {
|
||||
$this->helper->setIsRightToLeft(true);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Entity\User;
|
||||
use App\Twig\LocaleFormatExtensions;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
use Symfony\Component\HttpKernel\Event\RequestEvent;
|
||||
use Symfony\Component\HttpKernel\KernelEvents;
|
||||
@@ -18,7 +19,11 @@ use Symfony\Component\Security\Core\Authorization\AuthorizationCheckerInterface;
|
||||
|
||||
final class UserEnvironmentSubscriber implements EventSubscriberInterface
|
||||
{
|
||||
public function __construct(private TokenStorageInterface $tokenStorage, private AuthorizationCheckerInterface $auth)
|
||||
public function __construct(
|
||||
private readonly TokenStorageInterface $tokenStorage,
|
||||
private readonly AuthorizationCheckerInterface $auth,
|
||||
private readonly LocaleFormatExtensions $localeFormatExtensions
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -36,19 +41,21 @@ final class UserEnvironmentSubscriber implements EventSubscriberInterface
|
||||
return;
|
||||
}
|
||||
|
||||
// the locale depends on the request, not on the user configuration
|
||||
\Locale::setDefault($event->getRequest()->getLocale());
|
||||
$locale = $event->getRequest()->getLocale();
|
||||
|
||||
// ignore events like the toolbar where we do not have a token
|
||||
if (null === ($token = $this->tokenStorage->getToken())) {
|
||||
return;
|
||||
// events like the toolbar might not have a token
|
||||
if (null !== ($token = $this->tokenStorage->getToken())) {
|
||||
$user = $token->getUser();
|
||||
|
||||
if ($user instanceof User) {
|
||||
$locale = $user->getLocale();
|
||||
date_default_timezone_set($user->getTimezone());
|
||||
$user->initCanSeeAllData($this->auth->isGranted('view_all_data'));
|
||||
}
|
||||
}
|
||||
|
||||
$user = $token->getUser();
|
||||
|
||||
if ($user instanceof User) {
|
||||
date_default_timezone_set($user->getTimezone());
|
||||
$user->initCanSeeAllData($this->auth->isGranted('view_all_data'));
|
||||
}
|
||||
// the locale is primarily used for formatting values, so we depend on the user locale if available
|
||||
\Locale::setDefault($locale);
|
||||
$this->localeFormatExtensions->setLocale($locale);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ use App\Form\Type\InitialViewType;
|
||||
use App\Form\Type\SkinType;
|
||||
use App\Form\Type\TimezoneType;
|
||||
use App\Form\Type\UserLanguageType;
|
||||
use App\Form\Type\UserLocaleType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Psr\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
@@ -84,11 +85,16 @@ final class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
->setSection('locale')
|
||||
->setType(TimezoneType::class),
|
||||
|
||||
(new UserPreference(UserPreference::LOCALE, $this->systemConfiguration->getUserDefaultLanguage()))
|
||||
(new UserPreference(UserPreference::LANGUAGE, $this->systemConfiguration->getUserDefaultLanguage()))
|
||||
->setOrder(250)
|
||||
->setSection('locale')
|
||||
->setType(UserLanguageType::class),
|
||||
|
||||
(new UserPreference(UserPreference::LOCALE, $this->systemConfiguration->getUserDefaultLanguage()))
|
||||
->setOrder(250)
|
||||
->setSection('locale')
|
||||
->setType(UserLocaleType::class),
|
||||
|
||||
(new UserPreference(UserPreference::FIRST_WEEKDAY, User::DEFAULT_FIRST_WEEKDAY))
|
||||
->setOrder(300)
|
||||
->setSection('locale')
|
||||
|
||||
@@ -12,10 +12,9 @@ namespace App\Form\Helper;
|
||||
use App\Configuration\LocaleService;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Project;
|
||||
use Symfony\Contracts\Translation\LocaleAwareInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
final class ProjectHelper implements LocaleAwareInterface
|
||||
final class ProjectHelper
|
||||
{
|
||||
public const PATTERN_NAME = '{name}';
|
||||
public const PATTERN_COMMENT = '{comment}';
|
||||
@@ -33,7 +32,11 @@ final class ProjectHelper implements LocaleAwareInterface
|
||||
private bool $showEnd = false;
|
||||
private ?string $locale = null;
|
||||
|
||||
public function __construct(private SystemConfiguration $configuration, private LocaleService $localeService, private TranslatorInterface $translator)
|
||||
public function __construct(
|
||||
private readonly SystemConfiguration $configuration,
|
||||
private readonly LocaleService $localeService,
|
||||
private readonly TranslatorInterface $translator
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
@@ -42,7 +45,7 @@ final class ProjectHelper implements LocaleAwareInterface
|
||||
return $this->locale ?? \Locale::getDefault();
|
||||
}
|
||||
|
||||
public function setLocale(string $locale): void
|
||||
public function setLocale(?string $locale): void
|
||||
{
|
||||
$this->locale = $locale;
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use App\Configuration\LocaleService;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Intl\Locales;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
/**
|
||||
@@ -20,21 +21,32 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
*/
|
||||
final class LanguageType extends AbstractType
|
||||
{
|
||||
public function __construct(private LocaleService $localeService)
|
||||
public function __construct(private readonly LocaleService $localeService)
|
||||
{
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$choices = [];
|
||||
foreach ($this->localeService->getAllLocales() as $key) {
|
||||
$name = ucfirst(Locales::getName($key, $key));
|
||||
$choices[$name] = $key;
|
||||
}
|
||||
$resolver->setDefault('choices', function (Options $options) {
|
||||
$choices = [];
|
||||
|
||||
if ($options['translated_only'] === true) {
|
||||
$locales = $this->localeService->getTranslatedLocales();
|
||||
} else {
|
||||
$locales = $this->localeService->getAllLocales();
|
||||
}
|
||||
|
||||
foreach ($locales as $key) {
|
||||
$name = ucfirst(Locales::getName($key, $key));
|
||||
$choices[$name] = $key;
|
||||
}
|
||||
|
||||
return $choices;
|
||||
});
|
||||
|
||||
$resolver->setDefaults([
|
||||
'choices' => $choices,
|
||||
'label' => 'language',
|
||||
'translated_only' => false,
|
||||
'choice_translation_domain' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ use Symfony\Component\Form\CallbackTransformer;
|
||||
use Symfony\Component\Form\Exception\TransformationFailedException;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
final class TimePickerType extends AbstractType
|
||||
@@ -40,6 +42,11 @@ final class TimePickerType extends AbstractType
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildView(FormView $view, FormInterface $form, array $options): void
|
||||
{
|
||||
$view->vars['format'] = $options['format'];
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addModelTransformer(
|
||||
|
||||
@@ -9,30 +9,43 @@
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Configuration\LocaleService;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Custom form field type to select the user language.
|
||||
* Custom form field type to select the user language, which is used to translate the UI.
|
||||
* @extends AbstractType<string>
|
||||
*/
|
||||
final class UserLanguageType extends AbstractType
|
||||
{
|
||||
public function __construct(private UrlGeneratorInterface $router, private TranslatorInterface $translator)
|
||||
public function __construct(private readonly LocaleService $localeService)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->addModelTransformer(new CallbackTransformer(
|
||||
function ($value) {
|
||||
if ($value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->localeService->getNearestTranslationLocale($value);
|
||||
},
|
||||
function ($value) {
|
||||
return $value;
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$route = $this->router->generate('help_locales');
|
||||
$message = $this->translator->trans('user.language.help');
|
||||
$moreLink = $this->translator->trans('help_locales');
|
||||
|
||||
$resolver->setDefaults([
|
||||
'help_html' => true,
|
||||
'help' => sprintf('%2$s <a href="%1$s" target="help_locales">%3$s</a>', $route, $message, $moreLink)
|
||||
'label' => 'language',
|
||||
'translated_only' => true,
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
47
src/Form/Type/UserLocaleType.php
Normal file
47
src/Form/Type/UserLocaleType.php
Normal file
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
use Symfony\Contracts\Translation\TranslatorInterface;
|
||||
|
||||
/**
|
||||
* Custom form field type to select the user locale, which is used to format date/time/money/number values.
|
||||
*
|
||||
* @extends AbstractType<string>
|
||||
*/
|
||||
final class UserLocaleType extends AbstractType
|
||||
{
|
||||
public function __construct(
|
||||
private readonly UrlGeneratorInterface $router,
|
||||
private readonly TranslatorInterface $translator
|
||||
)
|
||||
{
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$route = $this->router->generate('help_locales');
|
||||
$moreLink = $this->translator->trans('help_locales');
|
||||
|
||||
$resolver->setDefaults([
|
||||
'label' => 'locale',
|
||||
'help_html' => true,
|
||||
'help' => sprintf('<a href="%1$s" target="help_locales">%2$s</a>', $route, $moreLink)
|
||||
]);
|
||||
}
|
||||
|
||||
public function getParent(): string
|
||||
{
|
||||
return LanguageType::class;
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use App\Form\Type\AvatarType;
|
||||
use App\Form\Type\MailType;
|
||||
use App\Form\Type\TimezoneType;
|
||||
use App\Form\Type\UserLanguageType;
|
||||
use App\Form\Type\UserLocaleType;
|
||||
use App\Form\Type\UserType;
|
||||
use App\Form\Type\YesNoType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
@@ -80,6 +81,10 @@ class UserEditType extends AbstractType
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$builder->add('locale', UserLocaleType::class, [
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$builder->add('timezone', TimezoneType::class, [
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
@@ -24,11 +24,11 @@ final class FormFormatConverter
|
||||
* This defines the mapping between ICU date format and PHP Date format.
|
||||
*
|
||||
* @see https://www.php.net/manual/en/datetime.format.php
|
||||
* @var array
|
||||
* @var array<string, string>
|
||||
*/
|
||||
private static array $formatConvertRules = [
|
||||
// Litepicker interprets a year like 22 as 1922 instead of 2022
|
||||
// so we have to make sure that it is always a4-digit year
|
||||
// so we have to make sure that it is always a 4-digit year
|
||||
"'h'" => "\h", // special format for fr_CA which includes 'h' as character
|
||||
'yy' => 'yyyy',
|
||||
'y' => 'yyyy',
|
||||
@@ -47,10 +47,6 @@ final class FormFormatConverter
|
||||
|
||||
/**
|
||||
* This works with ICU and DateTime format.
|
||||
*
|
||||
* @param string $format
|
||||
* @param bool $html
|
||||
* @return string
|
||||
*/
|
||||
public function convertToPattern(string $format, bool $html = true): string
|
||||
{
|
||||
@@ -83,8 +79,8 @@ final class FormFormatConverter
|
||||
$pattern = str_replace('g', self::PATTERN_HOUR_SINGLE, $pattern);
|
||||
$pattern = str_replace('i', self::PATTERN_MINUTES, $pattern);
|
||||
$pattern = str_replace('mm', self::PATTERN_MINUTES, $pattern);
|
||||
$pattern = str_replace('A', '(AM|PM){1}', $pattern);
|
||||
$pattern = str_replace('a', '(AM|PM){1}', $pattern);
|
||||
$pattern = str_replace('A', '(AM|PM|am|pm){1}', $pattern);
|
||||
$pattern = str_replace('a', '(AM|PM|am|pm){1}', $pattern);
|
||||
$pattern = str_replace('*****', 'h', $pattern);
|
||||
|
||||
if (!$html) {
|
||||
|
||||
@@ -139,6 +139,7 @@
|
||||
{%- endblock date_widget %}
|
||||
|
||||
{% block time_widget -%}
|
||||
{%- set user_format = format -%}
|
||||
{%- set format = locale_format('time') -%}
|
||||
{%- set jsFormat = format|js_format -%}
|
||||
{%- set attr = attr|merge({'pattern': format|pattern, 'autocomplete': 'off', 'data-timepicker': 'on', 'data-format': jsFormat, 'placeholder': jsFormat}) -%}
|
||||
@@ -155,7 +156,7 @@
|
||||
{% for index in [0, 1, 2, 3] %}
|
||||
<div class="dropdown-menu-column" style="min-width: 4rem">
|
||||
{% for value in time_presets %}
|
||||
{% set value = value|time %}
|
||||
{% set value = value|date_format(user_format) %}
|
||||
{% if loop.index0 % 4 == index %}
|
||||
<a class="dropdown-item justify-content-center" href="#" data-form-widget="copy-data" data-target="#{{ form.vars.id }}" data-value="{{ value }}" data-event="change">{{ value }}</a>
|
||||
{% endif %}
|
||||
|
||||
@@ -364,6 +364,7 @@ abstract class APIControllerBaseTest extends ControllerBaseTest
|
||||
'roles' => ['result' => 'array', 'type' => 'string'],
|
||||
'initials' => 'string',
|
||||
'language' => 'string',
|
||||
'locale' => 'string',
|
||||
'timezone' => 'string',
|
||||
'accountNumber' => '@string',
|
||||
'memberships' => ['result' => 'array', 'type' => 'TeamMembership'],
|
||||
|
||||
@@ -17,65 +17,102 @@ use PHPUnit\Framework\TestCase;
|
||||
*/
|
||||
class LocaleServiceTest extends TestCase
|
||||
{
|
||||
protected function getSut(array $settings)
|
||||
protected function getSut(array $settings): LocaleService
|
||||
{
|
||||
return new LocaleService($settings);
|
||||
}
|
||||
|
||||
protected function getDefaultSettings()
|
||||
/**
|
||||
* @return array<string, array{'date': string, 'time': string, 'rtl': bool, 'translation': bool}>
|
||||
*/
|
||||
protected function getDefaultSettings(): array
|
||||
{
|
||||
return [
|
||||
'de' => [
|
||||
'date' => 'd.m.Y',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'H:i',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'en' => [
|
||||
'date' => 'Y-m-d',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'H:i:s',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'en_AU' => [
|
||||
'date' => 'Y-m-d',
|
||||
'time' => 'H:i:s',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
'pt_BR' => [
|
||||
'date' => 'd-m-Y',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'it' => [
|
||||
'date' => 'd.m.Y',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'fr' => [
|
||||
'date' => 'd/m/Y',
|
||||
'duration' => '%h h %m',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'fr_BE' => [
|
||||
'date' => 'd/MM/yy',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => false,
|
||||
],
|
||||
'fr_CA' => [
|
||||
'date' => 'y-MM-dd',
|
||||
'time' => 'HH \'h\' mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'es' => [
|
||||
'date' => 'd.m.Y',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'ru' => [
|
||||
'date' => 'd.m.Y',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'ar' => [
|
||||
'date' => 'Y-m-d',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => true,
|
||||
'translation' => true,
|
||||
],
|
||||
'hu' => [
|
||||
'date' => 'Y.m.d.',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function testGetAllLocales()
|
||||
public function testGetAllLocales(): void
|
||||
{
|
||||
$sut = $this->getSut([]);
|
||||
$this->assertEquals([], $sut->getAllLocales());
|
||||
|
||||
$sut = $this->getSut($this->getDefaultSettings());
|
||||
$this->assertEquals(['de', 'en', 'pt_BR', 'it', 'fr', 'es', 'ru', 'ar', 'hu'], $sut->getAllLocales());
|
||||
$this->assertEquals(['de', 'en', 'en_AU', 'pt_BR', 'it', 'fr', 'fr_BE', 'fr_CA', 'es', 'ru', 'ar', 'hu'], $sut->getAllLocales());
|
||||
}
|
||||
|
||||
public function testInvalidLocaleWithGivenLocale()
|
||||
public function testInvalidLocaleWithGivenLocale(): void
|
||||
{
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Unknown locale given: xx');
|
||||
@@ -84,28 +121,54 @@ class LocaleServiceTest extends TestCase
|
||||
$sut->getDateFormat('xx');
|
||||
}
|
||||
|
||||
public function testGetDurationFormat()
|
||||
public function testGetDurationFormat(): void
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings());
|
||||
$this->assertEquals('%h:%m', $sut->getDurationFormat('de'));
|
||||
}
|
||||
|
||||
public function testGetDateFormat()
|
||||
public function testGetDateFormat(): void
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings());
|
||||
$this->assertEquals('d.m.Y', $sut->getDateFormat('de'));
|
||||
}
|
||||
|
||||
public function testGetDateTimeFormat()
|
||||
public function testGetDateTimeFormat(): void
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings());
|
||||
$this->assertEquals('d.m.Y H:i', $sut->getDateTimeFormat('de'));
|
||||
}
|
||||
|
||||
public function testGetTimeFormat()
|
||||
public function testGetTimeFormat(): void
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings());
|
||||
$this->assertEquals('H:i', $sut->getTimeFormat('de'));
|
||||
$this->assertEquals('H:i:s', $sut->getTimeFormat('en'));
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getNearestTranslationLocaleData
|
||||
*/
|
||||
public function testGetNearestTranslationLocale(string $locale, string $expected): void
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings());
|
||||
$actual = $sut->getNearestTranslationLocale($locale);
|
||||
$this->assertEquals($expected, $actual);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, array<int, string>>
|
||||
*/
|
||||
public function getNearestTranslationLocaleData(): array
|
||||
{
|
||||
return [
|
||||
['de', 'de'], // registered and translated: use it
|
||||
['pt_BR', 'pt_BR'], // registered and translated: use it
|
||||
['fr_CA', 'fr_CA'], // registered and translated: use it
|
||||
['fr_BE', 'fr'], // not translated, fallback to base locale
|
||||
['fr_DZ', 'fr'], // not registered region locale, but fallback to base locale
|
||||
['en_AU', 'en'], // not translated, fallback to base locale
|
||||
['uk_UA', 'en'], // not registered locales fallback to en
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,7 +422,8 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
|
||||
$data = [
|
||||
UserPreference::TIMEZONE => ['value' => 'America/Creston'],
|
||||
UserPreference::LOCALE => ['value' => 'ar'],
|
||||
UserPreference::LANGUAGE => ['value' => 'ar'],
|
||||
UserPreference::LOCALE => ['value' => 'ru'],
|
||||
UserPreference::FIRST_WEEKDAY => ['value' => 'sunday'],
|
||||
UserPreference::SKIN => ['value' => 'dark'],
|
||||
];
|
||||
@@ -453,9 +454,10 @@ class ProfileControllerTest extends ControllerBaseTest
|
||||
$this->assertEquals($expectedInternalRate, $user->getPreferenceValue(UserPreference::INTERNAL_RATE));
|
||||
$this->assertEquals('America/Creston', $user->getPreferenceValue(UserPreference::TIMEZONE));
|
||||
$this->assertEquals('America/Creston', $user->getTimezone());
|
||||
$this->assertEquals('ar', $user->getPreferenceValue(UserPreference::LOCALE));
|
||||
$this->assertEquals('ar', $user->getPreferenceValue(UserPreference::LANGUAGE));
|
||||
$this->assertEquals('ru', $user->getPreferenceValue(UserPreference::LOCALE));
|
||||
$this->assertEquals('ru', $user->getLocale());
|
||||
$this->assertEquals('ar', $user->getLanguage());
|
||||
$this->assertEquals('ar', $user->getLocale());
|
||||
$this->assertEquals('dark', $user->getPreferenceValue(UserPreference::SKIN));
|
||||
$this->assertEquals('sunday', $user->getPreferenceValue(UserPreference::FIRST_WEEKDAY));
|
||||
$this->assertEquals('sunday', $user->getFirstDayOfWeek());
|
||||
|
||||
@@ -71,26 +71,31 @@ class AppExtensionTest extends TestCase
|
||||
'date' => 'M/d/yy',
|
||||
'time' => 'h:mm a',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'de' => [
|
||||
'date' => 'dd.MM.yy',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'he' => [
|
||||
'date' => 'd.M.y',
|
||||
'time' => 'H:mm',
|
||||
'rtl' => true,
|
||||
'translation' => true,
|
||||
],
|
||||
'tr' => [
|
||||
'date' => 'd.MM.y',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
'zh_CN' => [
|
||||
'date' => 'y/M/d',
|
||||
'time' => 'HH:mm',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
],
|
||||
],
|
||||
'kimai.invoice.documents' => [
|
||||
|
||||
@@ -43,6 +43,7 @@ class UserTest extends TestCase
|
||||
self::assertNull($user->getPasswordRequestedAt());
|
||||
self::assertFalse($user->hasTotpSecret());
|
||||
self::assertNull($user->getTotpSecret());
|
||||
self::assertEquals(User::DEFAULT_LANGUAGE, $user->getLanguage());
|
||||
self::assertEquals(User::DEFAULT_LANGUAGE, $user->getLocale());
|
||||
self::assertFalse($user->hasTeamAssignment());
|
||||
self::assertFalse($user->canSeeAllData());
|
||||
@@ -288,13 +289,20 @@ class UserTest extends TestCase
|
||||
|
||||
public function testGetLocale(): void
|
||||
{
|
||||
$sut = new User();
|
||||
self::assertEquals(User::DEFAULT_LANGUAGE, $sut->getLocale());
|
||||
$user = new User();
|
||||
self::assertEquals(User::DEFAULT_LANGUAGE, $user->getLocale());
|
||||
|
||||
$language = new UserPreference(UserPreference::LOCALE, 'fr');
|
||||
$sut->addPreference($language);
|
||||
|
||||
self::assertEquals('fr', $sut->getLocale());
|
||||
self::assertEquals('en', $user->getLanguage());
|
||||
self::assertEquals('en', $user->getLocale());
|
||||
$user->setLanguage('it');
|
||||
self::assertEquals('it', $user->getLanguage());
|
||||
self::assertEquals('it', $user->getLocale());
|
||||
$user->setLocale('de');
|
||||
self::assertEquals('it', $user->getLanguage());
|
||||
self::assertEquals('de', $user->getLocale());
|
||||
$user->setPreferenceValue(UserPreference::LOCALE, 'hu');
|
||||
self::assertEquals('it', $user->getLanguage());
|
||||
self::assertEquals('hu', $user->getLocale());
|
||||
}
|
||||
|
||||
public function testTeams(): void
|
||||
|
||||
@@ -22,10 +22,10 @@ use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
*/
|
||||
class RedirectToLocaleSubscriberTest extends TestCase
|
||||
{
|
||||
public function testConstruct()
|
||||
public function testConstruct(): void
|
||||
{
|
||||
$urlGenerator = $this->createMock(UrlGeneratorInterface::class);
|
||||
$sut = new RedirectToLocaleSubscriber($urlGenerator, new LocaleService(['de', 'en']));
|
||||
$sut = new RedirectToLocaleSubscriber($urlGenerator, new LocaleService(['de' => LocaleService::DEFAULT_SETTINGS, 'en' => LocaleService::DEFAULT_SETTINGS]));
|
||||
|
||||
self::assertEquals([KernelEvents::REQUEST => ['onKernelRequest']], RedirectToLocaleSubscriber::getSubscribedEvents());
|
||||
|
||||
|
||||
@@ -28,6 +28,7 @@ class UserPreferenceSubscriberTest extends TestCase
|
||||
'internal_rate',
|
||||
'timezone',
|
||||
'language',
|
||||
'locale',
|
||||
'first_weekday',
|
||||
'skin',
|
||||
'update_browser_title',
|
||||
|
||||
@@ -45,11 +45,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
protected function getAbstractRenderer(string $classname)
|
||||
{
|
||||
$languages = [
|
||||
'en' => [
|
||||
'date' => 'Y.m.d',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'H:i',
|
||||
]
|
||||
'en' => LocaleService::DEFAULT_SETTINGS
|
||||
];
|
||||
|
||||
$security = $this->createMock(Security::class);
|
||||
|
||||
@@ -44,11 +44,7 @@ abstract class AbstractRendererTest extends KernelTestCase
|
||||
protected function getAbstractRenderer(string $classname)
|
||||
{
|
||||
$languages = [
|
||||
'en' => [
|
||||
'date' => 'Y.m.d',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'H:i',
|
||||
]
|
||||
'en' => LocaleService::DEFAULT_SETTINGS
|
||||
];
|
||||
|
||||
$user = new User();
|
||||
|
||||
@@ -26,9 +26,10 @@ class ProjectHelperTest extends TestCase
|
||||
{
|
||||
$config = SystemConfigurationFactory::createStub(['project.choice_pattern' => $format]);
|
||||
|
||||
$localeService = new LocaleService(['en_US' => ['date' => 'dd.MM.y']]);
|
||||
$localeService = new LocaleService(['en_US' => LocaleService::DEFAULT_SETTINGS]);
|
||||
$translator = $this->createMock(TranslatorInterface::class);
|
||||
$translator->method('trans')->willReturn('dating');
|
||||
|
||||
$helper = new ProjectHelper($config, $localeService, $translator);
|
||||
$helper->setLocale('en_US');
|
||||
|
||||
|
||||
@@ -66,6 +66,7 @@ trait RendererTestTrait
|
||||
'date' => 'yy.MM.dd',
|
||||
'time' => 'H:i',
|
||||
'rtl' => false,
|
||||
'translation' => true,
|
||||
]
|
||||
];
|
||||
|
||||
|
||||
@@ -38,11 +38,7 @@ class ServiceInvoiceTest extends TestCase
|
||||
private function getSut(array $paths): ServiceInvoice
|
||||
{
|
||||
$languages = [
|
||||
'en' => [
|
||||
'date' => 'Y.m.d',
|
||||
'duration' => '%h:%m',
|
||||
'time' => 'H:i',
|
||||
]
|
||||
'en' => LocaleService::DEFAULT_SETTINGS
|
||||
];
|
||||
|
||||
$formattings = new LocaleService($languages);
|
||||
|
||||
@@ -48,27 +48,18 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|array $locale
|
||||
* @param array|string $dateSettings
|
||||
* @return LocaleFormatExtensions
|
||||
* @param array<string, array{'date': string, 'time': string, 'rtl': bool, 'translation': bool}> $languageSettings
|
||||
*/
|
||||
protected function getSut($locale, $dateSettings, ?User $user = null)
|
||||
private function getSut(string $locale, array $languageSettings): LocaleFormatExtensions
|
||||
{
|
||||
$language = $locale;
|
||||
if (\is_array($locale)) {
|
||||
$language = $dateSettings;
|
||||
$dateSettings = $locale;
|
||||
}
|
||||
$user = new User();
|
||||
$user->setTimezone('Europe/Vienna');
|
||||
|
||||
if ($user === null) {
|
||||
$user = new User();
|
||||
$user->setTimezone('Europe/Vienna');
|
||||
}
|
||||
$security = $this->createMock(Security::class);
|
||||
$security->expects($this->any())->method('getUser')->willReturn($user);
|
||||
|
||||
$sut = new LocaleFormatExtensions(new LocaleService($dateSettings), $security);
|
||||
$sut->setLocale($language);
|
||||
$sut = new LocaleFormatExtensions(new LocaleService($languageSettings), $security);
|
||||
$sut->setLocale($locale);
|
||||
|
||||
return $sut;
|
||||
}
|
||||
@@ -143,9 +134,9 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
public function testDateShort(string $locale, \DateTime|string|null $date, string $expected): void
|
||||
{
|
||||
$sut = $this->getSut($locale, [
|
||||
'de' => ['date' => 'dd.MM.Y'],
|
||||
'en' => ['date' => 'Y-MM-dd'],
|
||||
'ru' => ['date' => 'dd.MM.Y'],
|
||||
'de' => array_merge(LocaleService::DEFAULT_SETTINGS, ['date' => 'dd.MM.Y']),
|
||||
'en' => array_merge(LocaleService::DEFAULT_SETTINGS, ['date' => 'Y-MM-dd']),
|
||||
'ru' => array_merge(LocaleService::DEFAULT_SETTINGS, ['date' => 'dd.MM.Y']),
|
||||
]);
|
||||
$this->assertEquals($expected, $sut->dateShort($date));
|
||||
}
|
||||
@@ -175,8 +166,8 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
public function testDateTime(string $locale, \DateTime|string|null $date, string $expected): void
|
||||
{
|
||||
$sut = $this->getSut($locale, [
|
||||
'de' => ['date' => 'dd.MM.Y', 'time' => 'HH:mm:s'],
|
||||
'en' => ['date' => 'Y-MM-dd', 'time' => 'h:mm a'],
|
||||
'de' => array_merge(LocaleService::DEFAULT_SETTINGS, ['date' => 'dd.MM.Y', 'time' => 'HH:mm:s']),
|
||||
'en' => array_merge(LocaleService::DEFAULT_SETTINGS, ['date' => 'Y-MM-dd', 'time' => 'h:mm a']),
|
||||
]);
|
||||
$this->assertEquals($expected, $sut->dateTime($date));
|
||||
}
|
||||
@@ -265,7 +256,7 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
$time = new \DateTime('2016-06-23');
|
||||
$time->setTime(17, 53, 23);
|
||||
|
||||
$sut = $this->getSut('en', ['en' => ['time' => 'HH:mm']]);
|
||||
$sut = $this->getSut('en', ['en' => array_merge(LocaleService::DEFAULT_SETTINGS, ['time' => 'HH:mm'])]);
|
||||
$this->assertEquals('17:53', $sut->time($time));
|
||||
$this->assertEquals('17:53', $sut->time('2016-06-23 17:53'));
|
||||
}
|
||||
@@ -289,10 +280,10 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
|
||||
public function testMoneyWithoutCurrency(): void
|
||||
{
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
$this->assertEquals('123.75', $sut->money(123.75));
|
||||
|
||||
$sut = $this->getSut($this->localeEn, 'de');
|
||||
$sut = $this->getSut('de', $this->localeEn);
|
||||
$this->assertEquals('123.234,76', $sut->money(123234.7554, null, true));
|
||||
$this->assertEquals('123.234,76', $sut->money(123234.7554, null, false));
|
||||
$this->assertEquals('123.234,76', $sut->money(123234.7554, 'EUR', false));
|
||||
@@ -303,7 +294,7 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
*/
|
||||
public function testMoneyNoCurrency($result, $amount, $currency, $locale): void
|
||||
{
|
||||
$sut = $this->getSut($this->localeEn, $locale);
|
||||
$sut = $this->getSut($locale, $this->localeEn);
|
||||
$this->assertEquals($result, $sut->money($amount, $currency, false));
|
||||
}
|
||||
|
||||
@@ -338,7 +329,7 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
*/
|
||||
public function testMoney(string $result, float|int|null $amount, string $currency, string $locale): void
|
||||
{
|
||||
$sut = $this->getSut($this->localeEn, $locale);
|
||||
$sut = $this->getSut($locale, $this->localeEn);
|
||||
$this->assertEquals($result, $sut->money($amount, $currency));
|
||||
}
|
||||
|
||||
@@ -370,9 +361,9 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
/**
|
||||
* @dataProvider getAmountData
|
||||
*/
|
||||
public function testAmount($result, $amount, $locale): void
|
||||
public function testAmount(string $result, null|int|float|string $amount, string $locale): void
|
||||
{
|
||||
$sut = $this->getSut($this->localeEn, $locale);
|
||||
$sut = $this->getSut($locale, $this->localeEn);
|
||||
$this->assertEquals($result, $sut->amount($amount));
|
||||
}
|
||||
|
||||
@@ -391,6 +382,7 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
['2.345,009', 2345.009, 'de'],
|
||||
['13.75', 13.75, 'en'],
|
||||
['13,75', 13.75, 'de'],
|
||||
['13,75', '13.75', 'de'],
|
||||
["13\u{a0}933,49", 13933.49, 'ru'],
|
||||
['1.234.567,891', 1234567.891234567890000, 'de'],
|
||||
];
|
||||
@@ -399,11 +391,11 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
/**
|
||||
* @dataProvider getMoneyData62_1
|
||||
*/
|
||||
public function testMoney62_1($result, $amount, $currency, $locale): void
|
||||
public function testMoney62_1(string $result, null|int|float $amount, string $currency, string $locale): void
|
||||
{
|
||||
IntlTestHelper::requireFullIntl($this, '62.1');
|
||||
|
||||
$sut = $this->getSut($this->localeEn, $locale);
|
||||
$sut = $this->getSut($locale, $this->localeEn);
|
||||
$this->assertEquals($result, $sut->money($amount, $currency));
|
||||
}
|
||||
|
||||
@@ -432,11 +424,11 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
$this->assertEquals('2.62', $sut->duration($record, true));
|
||||
|
||||
// test extended format
|
||||
$sut = $this->getSut($this->localeFake, 'XX');
|
||||
$sut = $this->getSut('XX', $this->localeFake);
|
||||
$this->assertEquals('2:37', $sut->duration($record->getDuration()));
|
||||
|
||||
// test negative duration
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
$this->assertEquals('0:00', $sut->duration(0));
|
||||
$this->assertEquals('0:00', $sut->duration(-1));
|
||||
$this->assertEquals('0:00', $sut->duration(-59));
|
||||
@@ -444,10 +436,10 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
$this->assertEquals('-1:36', $sut->duration(-5786));
|
||||
|
||||
// test zero duration
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
$this->assertEquals('0:00', $sut->duration(0));
|
||||
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
|
||||
$this->assertEquals('0:00', $sut->duration(null));
|
||||
$this->assertEquals('0.00', $sut->duration(null, true));
|
||||
@@ -499,31 +491,31 @@ class LocaleFormatExtensionsTest extends TestCase
|
||||
$this->assertEquals('2.62', $sut->durationDecimal($record));
|
||||
|
||||
// test extended format
|
||||
$sut = $this->getSut($this->localeDe, 'de');
|
||||
$sut = $this->getSut('de', $this->localeDe);
|
||||
$this->assertEquals('2,62', $sut->durationDecimal($record->getDuration()));
|
||||
|
||||
// test negative duration
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
$this->assertEquals('-0.00', $sut->durationDecimal(-1));
|
||||
|
||||
// test negative duration
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
$this->assertEquals('-0.01', $sut->durationDecimal(-40));
|
||||
$this->assertEquals('-0.01', $sut->durationDecimal(-50));
|
||||
|
||||
// test negative duration - with rounding issue
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
$this->assertEquals('-0.02', $sut->durationDecimal(-60));
|
||||
|
||||
// test zero duration
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
$this->assertEquals('0.00', $sut->durationDecimal(-0));
|
||||
$this->assertEquals('0.00', $sut->durationDecimal(0));
|
||||
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
$this->assertEquals('0.00', $sut->durationDecimal(-0));
|
||||
|
||||
$sut = $this->getSut($this->localeEn, 'en');
|
||||
$sut = $this->getSut('en', $this->localeEn);
|
||||
|
||||
$this->assertEquals('0.00', $sut->durationDecimal(null));
|
||||
}
|
||||
|
||||
@@ -1567,51 +1567,11 @@ parameters:
|
||||
count: 1
|
||||
path: Configuration/LdapConfigurationTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\LocaleServiceTest\\:\\:getDefaultSettings\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/LocaleServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\LocaleServiceTest\\:\\:getSut\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/LocaleServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\LocaleServiceTest\\:\\:getSut\\(\\) has parameter \\$settings with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Configuration/LocaleServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\LocaleServiceTest\\:\\:testGetAllLocales\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/LocaleServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\LocaleServiceTest\\:\\:testGetDateFormat\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/LocaleServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\LocaleServiceTest\\:\\:testGetDateTimeFormat\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/LocaleServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\LocaleServiceTest\\:\\:testGetDurationFormat\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/LocaleServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\LocaleServiceTest\\:\\:testGetTimeFormat\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/LocaleServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\LocaleServiceTest\\:\\:testInvalidLocaleWithGivenLocale\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: Configuration/LocaleServiceTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Configuration\\\\MailConfigurationTest\\:\\:testGetFromAddress\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -4527,11 +4487,6 @@ parameters:
|
||||
count: 1
|
||||
path: EventSubscriber/ProfileSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\RedirectToLocaleSubscriberTest\\:\\:testConstruct\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
path: EventSubscriber/RedirectToLocaleSubscriberTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\EventSubscriber\\\\UserDetailsSubscriberTest\\:\\:testGetSubscribedEvents\\(\\) has no return type specified\\.$#"
|
||||
count: 1
|
||||
@@ -8157,56 +8112,11 @@ parameters:
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:getSut\\(\\) has parameter \\$dateSettings with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:getSut\\(\\) has parameter \\$locale with no value type specified in iterable type array\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:getTimesheet\\(\\) has parameter \\$seconds with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:testAmount\\(\\) has parameter \\$amount with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:testAmount\\(\\) has parameter \\$locale with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:testAmount\\(\\) has parameter \\$result with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:testMoney62_1\\(\\) has parameter \\$amount with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:testMoney62_1\\(\\) has parameter \\$currency with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:testMoney62_1\\(\\) has parameter \\$locale with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:testMoney62_1\\(\\) has parameter \\$result with no type specified\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Method App\\\\Tests\\\\Twig\\\\LocaleFormatExtensionsTest\\:\\:testMoneyNoCurrency\\(\\) has parameter \\$amount with no type specified\\.$#"
|
||||
count: 1
|
||||
@@ -8232,16 +8142,6 @@ parameters:
|
||||
count: 5
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$languageSettings of class App\\\\Configuration\\\\LocaleService constructor expects array, array\\|string given\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$locale of method App\\\\Twig\\\\LocaleFormatExtensions\\:\\:setLocale\\(\\) expects string, array\\|string given\\.$#"
|
||||
count: 1
|
||||
path: Twig/LocaleFormatExtensionsTest.php
|
||||
|
||||
-
|
||||
message: "#^Parameter \\#1 \\$timezoneId of function date_default_timezone_set expects string, string\\|null given\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -1554,11 +1554,15 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="i_XOmN4" resname="help_locales">
|
||||
<source>help_locales</source>
|
||||
<target>Unterstützte Sprachen mit Formatierungsregeln</target>
|
||||
<target>Hier finden Sie Beispiele für die Formatierungsregeln der unterstützten Sprachen</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="d9m85KE" resname="locale">
|
||||
<source>locale</source>
|
||||
<target state="translated">Format für Zeit, Datum und Geld</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="JmkKf48" resname="user.language.help">
|
||||
<source>user.language.help</source>
|
||||
<target state="translated">Diese Einstellung ist neben der Übersetzung der Oberfläche ebenso zuständig für die Zeit-, Datums-, und Währungsformate.</target>
|
||||
<target state="translated">Diese Einstellung ist für die Übersetzung der Software zuständig</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="ulgdXdi" resname="hour_24">
|
||||
<source>hour_24</source>
|
||||
|
||||
@@ -1554,11 +1554,15 @@
|
||||
</trans-unit>
|
||||
<trans-unit id="i_XOmN4" resname="help_locales">
|
||||
<source>help_locales</source>
|
||||
<target>Supported languages with formatting rules</target>
|
||||
<target>Here are examples of formatting rules for the supported languages</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="d9m85KE" resname="locale">
|
||||
<source>locale</source>
|
||||
<target state="translated">Format for time, date and currency</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="JmkKf48" resname="user.language.help">
|
||||
<source>user.language.help</source>
|
||||
<target>In addition to translating the interface, this setting is also responsible for the time, date and currency formats.</target>
|
||||
<target>This setting is responsible for the translation of the software</target>
|
||||
</trans-unit>
|
||||
<trans-unit id="ulgdXdi" resname="hour_24">
|
||||
<source>hour_24</source>
|
||||
|
||||
Reference in New Issue
Block a user