From 68e7ffa45286b02e87ff5395ee5310596168264e Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 15 Oct 2025 12:33:05 +0200 Subject: [PATCH 01/27] Implementiere bevorzugte Sprachauswahl und Datenquellen-Synonyme MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Spracheinstellungen/System-Settings wurden um die Möglichkeit ergänzt, bevorzugte Sprachen für die Dropdown-Menüs festzulegen. Zudem wurde ein Datenquellen-Synonymsystem implementiert, um benutzerfreundlichere Bezeichnungen anzuzeigen und zu personalisieren. --- config/packages/translation.yaml | 2 +- config/packages/twig.yaml | 4 +- config/services.yaml | 7 ++ docs/configuration.md | 2 - src/Controller/ToolsController.php | 2 +- src/Form/Type/DataSourceJsonType.php | 103 ++++++++++++++++++ src/Form/Type/LocaleSelectType.php | 6 +- src/Services/Trees/ToolsTreeBuilder.php | 45 ++++++-- src/Services/Trees/TreeViewGenerator.php | 36 +++++- .../DataSourceSynonymsSettings.php | 73 +++++++++++++ .../SystemSettings/PreferredLocales.php | 37 +++++++ .../SystemSettings/SystemSettings.php | 4 + src/Twig/DataSourceNameExtension.php | 43 ++++++++ templates/admin/category_admin.html.twig | 4 +- templates/admin/footprint_admin.html.twig | 4 +- templates/admin/manufacturer_admin.html.twig | 4 +- templates/admin/project_admin.html.twig | 4 +- templates/admin/storelocation_admin.html.twig | 4 +- templates/admin/supplier_admin.html.twig | 4 +- templates/components/tree_macros.html.twig | 22 ++-- templates/form/permission_layout.html.twig | 26 ++++- translations/messages.cs.xlf | 72 ++++++++++++ translations/messages.da.xlf | 6 + translations/messages.de.xlf | 60 ++++++++++ translations/messages.el.xlf | 6 + translations/messages.en.xlf | 60 ++++++++++ translations/messages.es.xlf | 6 + translations/messages.fr.xlf | 6 + translations/messages.it.xlf | 6 + translations/messages.ja.xlf | 6 + translations/messages.nl.xlf | 6 + translations/messages.pl.xlf | 6 + translations/messages.ru.xlf | 10 +- translations/messages.zh.xlf | 6 + 34 files changed, 648 insertions(+), 44 deletions(-) create mode 100644 src/Form/Type/DataSourceJsonType.php create mode 100644 src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php create mode 100644 src/Settings/SystemSettings/PreferredLocales.php create mode 100644 src/Twig/DataSourceNameExtension.php diff --git a/config/packages/translation.yaml b/config/packages/translation.yaml index cbc1cd7e5..a3f529e31 100644 --- a/config/packages/translation.yaml +++ b/config/packages/translation.yaml @@ -1,7 +1,7 @@ framework: default_locale: 'en' # Just enable the locales we need for performance reasons. - enabled_locale: '%partdb.locale_menu%' + enabled_locale: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] translator: default_path: '%kernel.project_dir%/translations' fallbacks: diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 674aa3177..789560261 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -7,7 +7,7 @@ twig: globals: allow_email_pw_reset: '%partdb.users.email_pw_reset%' - locale_menu: '%partdb.locale_menu%' + location_settings: '@App\Settings\SystemSettings\LocalizationSettings' attachment_manager: '@App\Services\Attachments\AttachmentManager' label_profile_dropdown_helper: '@App\Services\LabelSystem\LabelProfileDropdownHelper' error_page_admin_email: '%partdb.error_pages.admin_email%' @@ -20,4 +20,4 @@ twig: when@test: twig: - strict_variables: true \ No newline at end of file + strict_variables: true diff --git a/config/services.yaml b/config/services.yaml index b48b3eff2..fce32d47f 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -188,6 +188,13 @@ services: $fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/' $tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/' + #################################################################################################################### + # Twig Extensions + #################################################################################################################### + + App\Twig\DataSourceNameExtension: + tags: [ 'twig.extension' ] + #################################################################################################################### # Part info provider system #################################################################################################################### diff --git a/docs/configuration.md b/docs/configuration.md index 4bb46d087..42cc546a1 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -272,8 +272,6 @@ command `bin/console cache:clear`. The following options are available: -* `partdb.locale_menu`: The codes of the languages, which should be shown in the language chooser menu (the one with the - user icon in the navbar). The first language in the list will be the default language. * `partdb.gdpr_compliance`: When set to true (default value), IP addresses which are saved in the database will be anonymized, by removing the last byte of the IP. This is required by the GDPR (General Data Protection Regulation) in the EU. diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index d78aff620..5d3536155 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -61,7 +61,7 @@ public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHel 'default_timezone' => $settings->system->localization->timezone, 'default_currency' => $settings->system->localization->baseCurrency, 'default_theme' => $settings->system->customization->theme, - 'enabled_locales' => $this->getParameter('partdb.locale_menu'), + 'enabled_locales' => array_column($settings->system->localization->preferredLanguages, 'value'), 'demo_mode' => $this->getParameter('partdb.demo_mode'), 'use_gravatar' => $settings->system->privacy->useGravatar, 'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'), diff --git a/src/Form/Type/DataSourceJsonType.php b/src/Form/Type/DataSourceJsonType.php new file mode 100644 index 000000000..7d067b41e --- /dev/null +++ b/src/Form/Type/DataSourceJsonType.php @@ -0,0 +1,103 @@ +settings->dataSourceSynonyms; + } + + foreach ($dataSources as $key => $label) { + $initialData = $existingData[$key] ?? $defaultValues[$key] ?? '{}'; + + $builder->add($key, TextareaType::class, [ + 'label' => $label, + 'required' => false, + 'data' => $initialData, + 'attr' => [ + 'rows' => 3, + 'style' => 'font-family: monospace;', + 'placeholder' => sprintf('%s translations in JSON format', ucfirst($key)), + ], + 'constraints' => [ + new Assert\Callback(function ($value, $context) { + if ($value && !static::isValidJson($value)) { + $context->buildViolation('The field must contain valid JSON.')->addViolation(); + } + }), + ], + ]); + } + + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) use ($defaultValues) { + $data = $event->getData(); + + if (!$data) { + $event->setData($defaultValues); + return; + } + + foreach ($defaultValues as $key => $defaultValue) { + if (empty($data[$key])) { + $data[$key] = $defaultValue; + } else { + $decodedValue = json_decode($data[$key], true); + if (json_last_error() === JSON_ERROR_NONE) { + $data[$key] = json_encode($decodedValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); + } + } + } + + $event->setData($data); + }); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefaults([ + 'data_sources' => [], + 'default_values' => [], + ]); + + $resolver->setAllowedTypes('data_sources', 'array'); + $resolver->setAllowedTypes('default_values', 'array'); + } + + /** + * Validates if a string is a valid JSON format. + * + * @param string $json + * @return bool + */ + private static function isValidJson(string $json): bool + { + json_decode($json); + return json_last_error() === JSON_ERROR_NONE; + } +} diff --git a/src/Form/Type/LocaleSelectType.php b/src/Form/Type/LocaleSelectType.php index d47fb57fd..b87932d12 100644 --- a/src/Form/Type/LocaleSelectType.php +++ b/src/Form/Type/LocaleSelectType.php @@ -23,7 +23,7 @@ namespace App\Form\Type; -use Symfony\Component\DependencyInjection\Attribute\Autowire; +use App\Settings\SystemSettings\LocalizationSettings; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -35,7 +35,7 @@ class LocaleSelectType extends AbstractType { - public function __construct(#[Autowire(param: 'partdb.locale_menu')] private readonly array $preferred_languages) + public function __construct(private LocalizationSettings $localizationSetting) { } @@ -47,7 +47,7 @@ public function getParent(): string public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'preferred_choices' => $this->preferred_languages, + 'preferred_choices' => array_column($this->localizationSetting->preferredLanguages, 'value'), ]); } } diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index 56064ba46..9a22ad4f2 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -38,6 +38,7 @@ use App\Entity\UserSystem\User; use App\Helpers\Trees\TreeViewNode; use App\Services\Cache\UserCacheKeyGenerator; +use App\Settings\BehaviorSettings\DataSourceSynonymsSettings; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Cache\ItemInterface; @@ -50,8 +51,14 @@ */ class ToolsTreeBuilder { - public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security) - { + public function __construct( + protected TranslatorInterface $translator, + protected UrlGeneratorInterface $urlGenerator, + protected TagAwareCacheInterface $cache, + protected UserCacheKeyGenerator $keyGenerator, + protected Security $security, + protected DataSourceSynonymsSettings $dataSourceSynonymsSettings, + ) { } /** @@ -139,7 +146,7 @@ protected function getToolsNode(): array $this->translator->trans('info_providers.search.title'), $this->urlGenerator->generate('info_providers_search') ))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down'); - + $nodes[] = (new TreeViewNode( $this->translator->trans('info_providers.bulk_import.manage_jobs'), $this->urlGenerator->generate('bulk_info_provider_manage') @@ -166,37 +173,37 @@ protected function getEditNodes(): array } if ($this->security->isGranted('read', new Category())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.categories'), + $this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()), $this->urlGenerator->generate('category_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-tags'); } if ($this->security->isGranted('read', new Project())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.projects'), + $this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()), $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } if ($this->security->isGranted('read', new Supplier())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.suppliers'), + $this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()), $this->urlGenerator->generate('supplier_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-truck'); } if ($this->security->isGranted('read', new Manufacturer())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.manufacturer'), + $this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()), $this->urlGenerator->generate('manufacturer_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-industry'); } if ($this->security->isGranted('read', new StorageLocation())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.storelocation'), + $this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()), $this->urlGenerator->generate('store_location_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-cube'); } if ($this->security->isGranted('read', new Footprint())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.footprint'), + $this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()), $this->urlGenerator->generate('footprint_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-microchip'); } @@ -310,4 +317,24 @@ protected function getSystemNodes(): array return $nodes; } + + protected function getTranslatedDataSourceOrSynonym(string $dataSource, string $translationKey, string $locale): string + { + $currentTranslation = $this->translator->trans($translationKey); + + $synonyms = $this->dataSourceSynonymsSettings->getSynonymsAsArray(); + + // Call alternatives from DataSourcesynonyms (if available) + if (!empty($synonyms[$dataSource][$locale])) { + $alternativeTranslation = $synonyms[$dataSource][$locale]; + + // Use alternative translation when it deviates from the standard translation + if ($alternativeTranslation !== $currentTranslation) { + return $alternativeTranslation; + } + } + + // Otherwise return the standard translation + return $currentTranslation; + } } diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 73ffa5baf..4b30cb184 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -38,6 +38,7 @@ use App\Services\Cache\ElementCacheTagGenerator; use App\Services\Cache\UserCacheKeyGenerator; use App\Services\EntityURLGenerator; +use App\Settings\BehaviorSettings\DataSourceSynonymsSettings; use App\Settings\BehaviorSettings\SidebarSettings; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; @@ -67,6 +68,7 @@ public function __construct( protected TranslatorInterface $translator, private readonly UrlGeneratorInterface $router, private readonly SidebarSettings $sidebarSettings, + protected DataSourceSynonymsSettings $dataSourceSynonymsSettings, ) { $this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled; $this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded; @@ -212,13 +214,15 @@ protected function entityClassToRootNodeHref(string $class): ?string protected function entityClassToRootNodeString(string $class): string { + $locale = $this->translator->getLocale(); + return match ($class) { - Category::class => $this->translator->trans('category.labelp'), - StorageLocation::class => $this->translator->trans('storelocation.labelp'), - Footprint::class => $this->translator->trans('footprint.labelp'), - Manufacturer::class => $this->translator->trans('manufacturer.labelp'), - Supplier::class => $this->translator->trans('supplier.labelp'), - Project::class => $this->translator->trans('project.labelp'), + Category::class => $this->getTranslatedOrSynonym('category', $locale), + StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale), + Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale), + Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale), + Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale), + Project::class => $this->getTranslatedOrSynonym('project', $locale), default => $this->translator->trans('tree.root_node.text'), }; } @@ -274,4 +278,24 @@ public function getGenericTree(string $class, ?AbstractStructuralDBElement $pare return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line }); } + + protected function getTranslatedOrSynonym(string $key, string $locale): string + { + $currentTranslation = $this->translator->trans($key . '.labelp'); + + $synonyms = $this->dataSourceSynonymsSettings->getSynonymsAsArray(); + + // Call alternatives from DataSourcesynonyms (if available) + if (!empty($synonyms[$key][$locale])) { + $alternativeTranslation = $synonyms[$key][$locale]; + + // Use alternative translation when it deviates from the standard translation + if ($alternativeTranslation !== $currentTranslation) { + return $alternativeTranslation; + } + } + + // Otherwise return the standard translation + return $currentTranslation; + } } diff --git a/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php b/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php new file mode 100644 index 000000000..74b9a2a1c --- /dev/null +++ b/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php @@ -0,0 +1,73 @@ + '{"en":"", "de":""}']), + options: ['type' => StringType::class], + formType: DataSourceJsonType::class, + formOptions: [ + 'required' => false, + 'data_sources' => [ + 'category' => new TM("settings.behavior.data_source_synonyms.category"), + 'storagelocation' => new TM("settings.behavior.data_source_synonyms.storagelocation"), + 'footprint' => new TM("settings.behavior.data_source_synonyms.footprint"), + 'manufacturer' => new TM("settings.behavior.data_source_synonyms.manufacturer"), + 'supplier' => new TM("settings.behavior.data_source_synonyms.supplier"), + 'project' => new TM("settings.behavior.data_source_synonyms.project"), + ], + 'default_values' => [ + 'category' => '{"en":"Categories", "de":"Kategorien"}', + 'storagelocation' => '{"en":"Storage locations", "de":"Lagerorte"}', + 'footprint' => '{"en":"Footprints", "de":"Footprints"}', + 'manufacturer' => '{"en":"Manufacturers", "de":"Hersteller"}', + 'supplier' => '{"en":"Suppliers", "de":"Lieferanten"}', + 'project' => '{"en":"Projects", "de":"Projekte"}', + ], + ], + )] + #[Assert\Type('array')] + public array $dataSourceSynonyms = [ + 'category' => '{"en":"Categories", "de":"Kategorien"}', + 'storagelocation' => '{"en":"Storage locations", "de":"Lagerorte"}', + 'footprint' => '{"en":"Footprints", "de":"Footprints"}', + 'manufacturer' => '{"en":"Manufacturers", "de":"Hersteller"}', + 'supplier' => '{"en":"Suppliers", "de":"Lieferanten"}', + 'project' => '{"en":"Projects", "de":"Projekte"}', + ]; + + /** + * Get the synonyms data as a structured array. + * + * @return array> The data source synonyms parsed from JSON to array. + */ + public function getSynonymsAsArray(): array + { + $result = []; + foreach ($this->dataSourceSynonyms as $key => $jsonString) { + $result[$key] = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR) ?? []; + } + + return $result; + } + +} diff --git a/src/Settings/SystemSettings/PreferredLocales.php b/src/Settings/SystemSettings/PreferredLocales.php new file mode 100644 index 000000000..1fe38a548 --- /dev/null +++ b/src/Settings/SystemSettings/PreferredLocales.php @@ -0,0 +1,37 @@ +. + */ + +declare(strict_types=1); + +namespace App\Settings\SystemSettings; + +enum PreferredLocales: string +{ + case EN = 'en'; + case DE = 'de'; + case IT = 'it'; + case FR = 'fr'; + case RU = 'ru'; + case JA = 'ja'; + case CS = 'cs'; + case DA = 'da'; + case ZH = 'zh'; + case PL = 'pl'; +} diff --git a/src/Settings/SystemSettings/SystemSettings.php b/src/Settings/SystemSettings/SystemSettings.php index 71dd963d5..3d6c4041d 100644 --- a/src/Settings/SystemSettings/SystemSettings.php +++ b/src/Settings/SystemSettings/SystemSettings.php @@ -24,6 +24,7 @@ namespace App\Settings\SystemSettings; use Jbtronics\SettingsBundle\Settings\EmbeddedSettings; +use App\Settings\BehaviorSettings\DataSourceSynonymsSettings; use Jbtronics\SettingsBundle\Settings\Settings; use Symfony\Component\Translation\TranslatableMessage as TM; @@ -33,6 +34,9 @@ class SystemSettings #[EmbeddedSettings()] public ?LocalizationSettings $localization = null; + #[EmbeddedSettings] + public ?DataSourceSynonymsSettings $dataSourceSynonyms = null; + #[EmbeddedSettings()] public ?CustomizationSettings $customization = null; diff --git a/src/Twig/DataSourceNameExtension.php b/src/Twig/DataSourceNameExtension.php new file mode 100644 index 000000000..d0d8b4b52 --- /dev/null +++ b/src/Twig/DataSourceNameExtension.php @@ -0,0 +1,43 @@ +translator = $translator; + $this->dataSourceSynonyms = $dataSourceSynonymsSettings->getSynonymsAsArray(); + } + + public function getFunctions(): array + { + return [ + new TwigFunction('get_data_source_name', [$this, 'getDataSourceName']), + ]; + } + + /** + * Based on the locale and data source names, gives the right synonym value back or the default translator value. + */ + public function getDataSourceName(string $dataSourceName, string $defaultKey): string + { + $locale = $this->translator->getLocale(); + + // Use alternative dataSource synonym (if available) + if (isset($this->dataSourceSynonyms[$dataSourceName][$locale])) { + return $this->dataSourceSynonyms[$dataSourceName][$locale]; + } + + // Otherwise return the standard translation + return $this->translator->trans($defaultKey); + } +} diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index d87cee7f4..82089a283 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}category.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('category', 'category.labelp') %} + {% set translatedSource = 'category.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_pills %} diff --git a/templates/admin/footprint_admin.html.twig b/templates/admin/footprint_admin.html.twig index a2c3e4afd..a6acbe84e 100644 --- a/templates/admin/footprint_admin.html.twig +++ b/templates/admin/footprint_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}footprint.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('footprint', 'footprint.labelp') %} + {% set translatedSource = 'footprint.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block master_picture_block %} diff --git a/templates/admin/manufacturer_admin.html.twig b/templates/admin/manufacturer_admin.html.twig index 5db892c04..3ce9a124c 100644 --- a/templates/admin/manufacturer_admin.html.twig +++ b/templates/admin/manufacturer_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}manufacturer.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('manufacturer', 'manufacturer.caption') %} + {% set translatedSource = 'manufacturer.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} diff --git a/templates/admin/project_admin.html.twig b/templates/admin/project_admin.html.twig index 1a9950691..8066d5451 100644 --- a/templates/admin/project_admin.html.twig +++ b/templates/admin/project_admin.html.twig @@ -3,7 +3,9 @@ {# @var entity App\Entity\ProjectSystem\Project #} {% block card_title %} - {% trans %}project.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('project', 'project.caption') %} + {% set translatedSource = 'project.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block edit_title %} diff --git a/templates/admin/storelocation_admin.html.twig b/templates/admin/storelocation_admin.html.twig index c93339dc1..1e60eeea2 100644 --- a/templates/admin/storelocation_admin.html.twig +++ b/templates/admin/storelocation_admin.html.twig @@ -2,7 +2,9 @@ {% import "label_system/dropdown_macro.html.twig" as dropdown %} {% block card_title %} - {% trans %}storelocation.labelp{% endtrans %} + {% set dataSourceName = get_data_source_name('storagelocation', 'storelocation.labelp') %} + {% set translatedSource = 'storelocation.labelp'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_controls %} diff --git a/templates/admin/supplier_admin.html.twig b/templates/admin/supplier_admin.html.twig index ce38a5ca4..b5cf7b236 100644 --- a/templates/admin/supplier_admin.html.twig +++ b/templates/admin/supplier_admin.html.twig @@ -1,7 +1,9 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% trans %}supplier.caption{% endtrans %} + {% set dataSourceName = get_data_source_name('supplier', 'supplier.caption') %} + {% set translatedSource = 'supplier.caption'|trans %} + {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} {% endblock %} {% block additional_panes %} diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index 366d42fe8..e82cd3b4b 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -1,13 +1,15 @@ {% macro sidebar_dropdown() %} + {% set currentLocale = app.request.locale %} + {# Format is [mode, route, label, show_condition] #} {% set data_sources = [ - ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read')], - ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read')], - ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')], - ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')], - ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')], - ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')], - ['tools', path('tree_tools'), 'tools.label', true], + ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read'), 'category'], + ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read'), 'storagelocation'], + ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read'), 'footprint'], + ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read'), 'manufacturer'], + ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read'), 'supplier'], + ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read'), 'project'], + ['tools', path('tree_tools'), 'tools.label', true, 'tool'], ] %} @@ -18,9 +20,9 @@ {% for source in data_sources %} {% if source[3] %} {# show_condition #} -
  • + >{{ get_data_source_name(source[4], source[2]) }} {% endif %} {% endfor %} {% endmacro %} @@ -61,4 +63,4 @@
    -{% endmacro %} \ No newline at end of file +{% endmacro %} diff --git a/templates/form/permission_layout.html.twig b/templates/form/permission_layout.html.twig index 747208dd1..07ae6c493 100644 --- a/templates/form/permission_layout.html.twig +++ b/templates/form/permission_layout.html.twig @@ -6,12 +6,34 @@
    {% else %} - {{ form.vars.label | trans }} + def{{ form.vars.label | trans }} {% endif %} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index cd572daea..8757d6c70 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -12801,6 +12801,72 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz <b>Upozorňujeme, že při změně této hodnoty nedochází k převodu měn. Změna výchozí měny po přidání informací o cenách tedy povede k nesprávným cenám!</b> + + + settings.system.localization.preferred_languages + Preferované jazyky + + + + + settings.system.localization.preferred_languages.help + Jazyky, které se zobrazují v uživatelské rozbalovací nabídce + + + + + settings.system.data_source_synonyms + Synonyma zdrojů dat + + + + + settings.system.data_source_synonyms.configuration + Zdroj + + + + + settings.system.data_source_synonyms.configuration.help + Definujte vlastní synonyma pro dané zdroje dat. Očekává se formát JSON s vašimi preferovanými jazykovými ISO kódy. Příklad: %format%. + + + + + settings.behavior.data_source_synonyms.category + Kategorie + + + + + settings.behavior.data_source_synonyms.storagelocation + Skladové umístění + + + + + settings.behavior.data_source_synonyms.footprint + Pouzdro + + + + + settings.behavior.data_source_synonyms.manufacturer + Výrobce + + + + + settings.behavior.data_source_synonyms.supplier + Dodavatel + + + + + settings.behavior.data_source_synonyms.project + Projekt + + settings.system.privacy @@ -13659,5 +13725,11 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Minimální šířka náhledu (px) + + + datasource.synonym + %name% (Váš synonymum: %synonym%) + + diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index 530d91aa9..afa5164f0 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -12328,5 +12328,11 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Du forsøgte at fjerne/tilføje en mængde sat til nul! Der blev ikke foretaget nogen handling. + + + datasource.synonym + %name% (Dit synonym: %synonym%) + + diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 806c2e52b..3fb6d28b1 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -12881,6 +12881,60 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Bitte beachten Sie, dass die Währungen bei einer Änderung dieses Wertes nicht umgerechnet werden. Wenn Sie also die Basiswährung ändern, nachdem Sie bereits Preisinformationen hinzugefügt haben, führt dies zu falschen Preisen!]]> + + + settings.system.data_source_synonyms + Datenquellen-Synonyme + + + + + settings.system.data_source_synonyms.configuration + Quelle + + + + + settings.system.data_source_synonyms.configuration.help + Definieren Sie Ihre eigenen Synonyme für die angegebenen Datenquellen. Erwartet wird ein JSON-Format mit Ihren bevorzugten Sprache-ISO-Codes. Beispiel: %format%. + + + + + settings.behavior.data_source_synonyms.category + Kategorie + + + + + settings.behavior.data_source_synonyms.storagelocation + Lagerort + + + + + settings.behavior.data_source_synonyms.footprint + Footprint + + + + + settings.behavior.data_source_synonyms.manufacturer + Hersteller + + + + + settings.behavior.data_source_synonyms.supplier + Lieferant + + + + + settings.behavior.data_source_synonyms.project + Projekt + + settings.system.privacy @@ -14436,5 +14490,11 @@ Dies ist auf der Informationsquellen Übersichtsseite möglich. Wenn aktiviert, wird die Bauteil-Beschreibung verwendet, um vorhandene Teile mit derselben Beschreibung zu finden und die nächste verfügbare IPN für die Vorschlagsliste zu ermitteln, indem der numerische Suffix entsprechend erhöht wird. + + + datasource.synonym + %name% (Ihr Synonym: %synonym%) + + diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 3618fa3db..43b65a88b 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1667,5 +1667,11 @@ Δημιουργήστε πρώτα ένα εξάρτημα και αντιστοιχίστε το σε μια κατηγορία: με τις υπάρχουσες κατηγορίες και τα δικά τους προθέματα IPN, η ονομασία IPN για το εξάρτημα μπορεί να προταθεί αυτόματα + + + datasource.synonym + %name% (Το συνώνυμό σας: %synonym%) + + diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index a5d86338d..ad772b12f 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12882,6 +12882,60 @@ Please note, that you can not impersonate a disabled user. If you try you will g Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!]]> + + + settings.system.data_source_synonyms + Data source synonyms + + + + + settings.system.data_source_synonyms.configuration + Source + + + + + settings.system.data_source_synonyms.configuration.help + Define your own synonyms for the given data sources. Expected in JSON-format with your preferred language iso-codes. Example: %format%. + + + + + settings.behavior.data_source_synonyms.category + Category + + + + + settings.behavior.data_source_synonyms.storagelocation + Storage location + + + + + settings.behavior.data_source_synonyms.footprint + Footprint + + + + + settings.behavior.data_source_synonyms.manufacturer + Manufacturer + + + + + settings.behavior.data_source_synonyms.supplier + Supplier + + + + + settings.behavior.data_source_synonyms.project + Project + + settings.system.privacy @@ -14468,5 +14522,11 @@ You can do this in the provider info list. A PCRE-compatible regular expression every IPN has to fulfill. Leave empty to allow all everything as IPN. + + + datasource.synonym + %name% (Your synonym: %synonym%) + + diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 57ac5c857..9ee7974e1 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -12500,5 +12500,11 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Este componente contiene más de un stock. Cambie la ubicación manualmente para seleccionar el stock deseado. + + + datasource.synonym + %name% (Tu sinónimo: %synonym%) + + diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index cb3936ef7..90eca23b7 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9229,5 +9229,11 @@ exemple de ville Un préfixe suggéré lors de la saisie de l'IPN d'une pièce. + + + datasource.synonym + %name% (Votre synonyme : %synonym%) + + diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 34540da1b..70422a57e 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -12502,5 +12502,11 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Questo componente contiene più di uno stock. Cambia manualmente la posizione per selezionare quale stock scegliere. + + + datasource.synonym + %name% (Il tuo sinonimo: %synonym%) + + diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 668c51c12..79dd07911 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -8966,5 +8966,11 @@ Exampletown 部品のIPN入力時に提案される接頭辞。 + + + datasource.synonym + %name% (あなたの同義語: %synonym%) + + diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 1c0631879..7d1245569 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -856,5 +856,11 @@ Maak eerst een component en wijs het toe aan een categorie: met de bestaande categorieën en hun eigen IPN-prefixen kan de IPN voor het component automatisch worden voorgesteld + + + datasource.synonym + %name% (Uw synoniem: %synonym%) + + diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 0a9353fb7..686437f48 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -12355,5 +12355,11 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Wygenerowany kod + + + datasource.synonym + %name% (Twój synonim: %synonym%) + + diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index 9c91a4b10..eb77708d4 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -737,7 +737,7 @@ user.edit.tfa.disable_tfa_message - Это выключит <b>все активные двухфакторной способы аутентификации пользователя</b>и удалит <b>резервные коды</b>! + Это выключит <b>все активные двухфакторной способы аутентификации пользователя</b>и удалит <b>резервные коды</b>! <br> Пользователь должен будет снова настроить все методы двухфакторной аутентификации и распечатать новые резервные коды! <br><br> <b>Делайте это только в том случае, если вы абсолютно уверены в личности пользователя (обращающегося за помощью), в противном случае учетная запись может быть взломана злоумышленником!</b> @@ -3806,7 +3806,7 @@ tfa_backup.reset_codes.confirm_message - Это удалит все предыдущие коды и создаст набор новых. Это не может быть отменено. + Это удалит все предыдущие коды и создаст набор новых. Это не может быть отменено. Не забудьте распечатать новы кода и хранить их в безопасном месте! @@ -12455,5 +12455,11 @@ Профиль сохранен! + + + datasource.synonym + %name% (Ваш синоним: %synonym%) + + diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index ee9128008..e05aa7e60 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -12340,5 +12340,11 @@ Element 3 成功创建 %COUNT% 个元素。 + + + datasource.synonym + %name% (您的同义词: %synonym%) + + From d80ec9422778ef77693a56e475ed001e1e6069b3 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 15 Oct 2025 12:38:37 +0200 Subject: [PATCH 02/27] Anpassung aus Analyse --- src/Form/Type/DataSourceJsonType.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Form/Type/DataSourceJsonType.php b/src/Form/Type/DataSourceJsonType.php index 7d067b41e..6d11058a2 100644 --- a/src/Form/Type/DataSourceJsonType.php +++ b/src/Form/Type/DataSourceJsonType.php @@ -95,7 +95,7 @@ public function configureOptions(OptionsResolver $resolver): void * @param string $json * @return bool */ - private static function isValidJson(string $json): bool + public static function isValidJson(string $json): bool { json_decode($json); return json_last_error() === JSON_ERROR_NONE; From a8b3dce899fef03a2137a88b1b5e61a428ff4394 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 5 Nov 2025 12:27:40 +0100 Subject: [PATCH 03/27] Entferne alten JSON-basierten Datenquellen-Synonym-Handler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Die Verwaltung der Datenquellen-Synonyme wurde überarbeitet, um ein flexibleres und strukturiertes Konzept zu ermöglichen. Der bestehende JSON-basierte Ansatz wurde durch eine neue Service-basierte Architektur ersetzt, die eine bessere Handhabung und Erweiterbarkeit erlaubt. --- ...tasource_synonyms_collection_controller.js | 68 +++++++ config/packages/twig.yaml | 4 +- docs/configuration.md | 2 + src/Controller/SettingsController.php | 2 +- src/Controller/ToolsController.php | 2 +- src/Form/Type/DataSourceJsonType.php | 103 ---------- src/Form/Type/DataSourceSynonymRowType.php | 131 +++++++++++++ .../Type/DataSourceSynonymsCollectionType.php | 180 ++++++++++++++++++ src/Form/Type/LocaleSelectType.php | 7 +- .../Misc/DataSourceSynonymResolver.php | 71 +++++++ src/Services/Trees/ToolsTreeBuilder.php | 59 +++--- src/Services/Trees/TreeViewGenerator.php | 37 +--- .../DataSourceSynonymsSettings.php | 65 ++++--- .../SystemSettings/PreferredLocales.php | 37 ---- src/Twig/DataSourceNameExtension.php | 58 ++++-- templates/admin/category_admin.html.twig | 7 +- templates/admin/footprint_admin.html.twig | 7 +- templates/admin/manufacturer_admin.html.twig | 7 +- templates/admin/project_admin.html.twig | 7 +- templates/admin/storelocation_admin.html.twig | 7 +- templates/admin/supplier_admin.html.twig | 7 +- templates/components/tree_macros.html.twig | 11 +- .../datasource_synonyms_collection.html.twig | 48 +++++ templates/form/permission_layout.html.twig | 2 +- translations/messages.cs.xlf | 38 +++- translations/messages.de.xlf | 38 +++- translations/messages.en.xlf | 38 +++- translations/validators.cs.xlf | 12 ++ translations/validators.de.xlf | 12 ++ translations/validators.en.xlf | 12 ++ 30 files changed, 802 insertions(+), 277 deletions(-) create mode 100644 assets/controllers/elements/datasource_synonyms_collection_controller.js delete mode 100644 src/Form/Type/DataSourceJsonType.php create mode 100644 src/Form/Type/DataSourceSynonymRowType.php create mode 100644 src/Form/Type/DataSourceSynonymsCollectionType.php create mode 100644 src/Services/Misc/DataSourceSynonymResolver.php delete mode 100644 src/Settings/SystemSettings/PreferredLocales.php create mode 100644 templates/form/datasource_synonyms_collection.html.twig diff --git a/assets/controllers/elements/datasource_synonyms_collection_controller.js b/assets/controllers/elements/datasource_synonyms_collection_controller.js new file mode 100644 index 000000000..6b2f48110 --- /dev/null +++ b/assets/controllers/elements/datasource_synonyms_collection_controller.js @@ -0,0 +1,68 @@ +/* + * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony). + * + * Copyright (C) 2019 - 2022 Jan Böhmer (https://github.com/jbtronics) + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published + * by the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +import { Controller } from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['items']; + static values = { + prototype: String, + prototypeName: { type: String, default: '__name__' }, + index: { type: Number, default: 0 }, + }; + + connect() { + if (!this.hasIndexValue || Number.isNaN(this.indexValue)) { + this.indexValue = this.itemsTarget?.children.length || 0; + } + } + + add(event) { + event.preventDefault(); + + const encodedProto = this.prototypeValue || ''; + const placeholder = this.prototypeNameValue || '__name__'; + if (!encodedProto || !this.itemsTarget) return; + + const protoHtml = this._decodeHtmlAttribute(encodedProto); + + const idx = this.indexValue; + const html = protoHtml.replace(new RegExp(placeholder, 'g'), String(idx)); + + const wrapper = document.createElement('div'); + wrapper.innerHTML = html; + const newItem = wrapper.firstElementChild; + if (newItem) { + this.itemsTarget.appendChild(newItem); + this.indexValue = idx + 1; + } + } + + remove(event) { + event.preventDefault(); + const row = event.currentTarget.closest('.tc-item'); + if (row) row.remove(); + } + + _decodeHtmlAttribute(str) { + const tmp = document.createElement('textarea'); + tmp.innerHTML = str; + return tmp.value || tmp.textContent || ''; + } +} diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 789560261..b9fdf5bd9 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,13 +1,13 @@ twig: default_path: '%kernel.project_dir%/templates' - form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig'] + form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig', 'form/datasource_synonyms_collection.html.twig'] paths: '%kernel.project_dir%/assets/css': css globals: allow_email_pw_reset: '%partdb.users.email_pw_reset%' - location_settings: '@App\Settings\SystemSettings\LocalizationSettings' + locale_menu: '%partdb.locale_menu%' attachment_manager: '@App\Services\Attachments\AttachmentManager' label_profile_dropdown_helper: '@App\Services\LabelSystem\LabelProfileDropdownHelper' error_page_admin_email: '%partdb.error_pages.admin_email%' diff --git a/docs/configuration.md b/docs/configuration.md index 42cc546a1..4bb46d087 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -272,6 +272,8 @@ command `bin/console cache:clear`. The following options are available: +* `partdb.locale_menu`: The codes of the languages, which should be shown in the language chooser menu (the one with the + user icon in the navbar). The first language in the list will be the default language. * `partdb.gdpr_compliance`: When set to true (default value), IP addresses which are saved in the database will be anonymized, by removing the last byte of the IP. This is required by the GDPR (General Data Protection Regulation) in the EU. diff --git a/src/Controller/SettingsController.php b/src/Controller/SettingsController.php index 3479cf849..f412e469e 100644 --- a/src/Controller/SettingsController.php +++ b/src/Controller/SettingsController.php @@ -64,7 +64,7 @@ public function systemSettings(Request $request, TagAwareCacheInterface $cache): $this->settingsManager->save($settings); //It might be possible, that the tree settings have changed, so clear the cache - $cache->invalidateTags(['tree_treeview', 'sidebar_tree_update']); + $cache->invalidateTags(['tree_tools', 'tree_treeview', 'sidebar_tree_update']); $this->addFlash('success', t('settings.flash.saved')); } diff --git a/src/Controller/ToolsController.php b/src/Controller/ToolsController.php index 5d3536155..d78aff620 100644 --- a/src/Controller/ToolsController.php +++ b/src/Controller/ToolsController.php @@ -61,7 +61,7 @@ public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHel 'default_timezone' => $settings->system->localization->timezone, 'default_currency' => $settings->system->localization->baseCurrency, 'default_theme' => $settings->system->customization->theme, - 'enabled_locales' => array_column($settings->system->localization->preferredLanguages, 'value'), + 'enabled_locales' => $this->getParameter('partdb.locale_menu'), 'demo_mode' => $this->getParameter('partdb.demo_mode'), 'use_gravatar' => $settings->system->privacy->useGravatar, 'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'), diff --git a/src/Form/Type/DataSourceJsonType.php b/src/Form/Type/DataSourceJsonType.php deleted file mode 100644 index 6d11058a2..000000000 --- a/src/Form/Type/DataSourceJsonType.php +++ /dev/null @@ -1,103 +0,0 @@ -settings->dataSourceSynonyms; - } - - foreach ($dataSources as $key => $label) { - $initialData = $existingData[$key] ?? $defaultValues[$key] ?? '{}'; - - $builder->add($key, TextareaType::class, [ - 'label' => $label, - 'required' => false, - 'data' => $initialData, - 'attr' => [ - 'rows' => 3, - 'style' => 'font-family: monospace;', - 'placeholder' => sprintf('%s translations in JSON format', ucfirst($key)), - ], - 'constraints' => [ - new Assert\Callback(function ($value, $context) { - if ($value && !static::isValidJson($value)) { - $context->buildViolation('The field must contain valid JSON.')->addViolation(); - } - }), - ], - ]); - } - - $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event) use ($defaultValues) { - $data = $event->getData(); - - if (!$data) { - $event->setData($defaultValues); - return; - } - - foreach ($defaultValues as $key => $defaultValue) { - if (empty($data[$key])) { - $data[$key] = $defaultValue; - } else { - $decodedValue = json_decode($data[$key], true); - if (json_last_error() === JSON_ERROR_NONE) { - $data[$key] = json_encode($decodedValue, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); - } - } - } - - $event->setData($data); - }); - } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setDefaults([ - 'data_sources' => [], - 'default_values' => [], - ]); - - $resolver->setAllowedTypes('data_sources', 'array'); - $resolver->setAllowedTypes('default_values', 'array'); - } - - /** - * Validates if a string is a valid JSON format. - * - * @param string $json - * @return bool - */ - public static function isValidJson(string $json): bool - { - json_decode($json); - return json_last_error() === JSON_ERROR_NONE; - } -} diff --git a/src/Form/Type/DataSourceSynonymRowType.php b/src/Form/Type/DataSourceSynonymRowType.php new file mode 100644 index 000000000..eeed32cd4 --- /dev/null +++ b/src/Form/Type/DataSourceSynonymRowType.php @@ -0,0 +1,131 @@ +. + */ + +declare(strict_types=1); + +namespace App\Form\Type; + +use App\Settings\SystemSettings\LocalizationSettings; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\Form\AbstractType; +use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\LocaleType; +use Symfony\Component\Form\Extension\Core\Type\TextType; +use Symfony\Component\Form\FormBuilderInterface; +use Symfony\Component\Intl\Locales; +use Symfony\Component\OptionsResolver\OptionsResolver; +use Symfony\Component\Validator\Constraints as Assert; + +/** + * A single translation row: data source + language + translations (singular/plural). + */ +class DataSourceSynonymRowType extends AbstractType +{ + public function __construct( + private readonly LocalizationSettings $localizationSettings, + #[Autowire(param: 'partdb.locale_menu')] private readonly array $preferredLanguagesParam, + ) { + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder + ->add('dataSource', ChoiceType::class, [ + 'label' => 'settings.behavior.data_source_synonyms.row_type.form.datasource', + 'choices' => $this->buildDataSourceChoices($options['data_sources']), + 'required' => true, + 'constraints' => [ + new Assert\NotBlank(message: 'settings.system.data_source_synonyms.row_type.value_not_blank'), + ], + ]) + ->add('locale', LocaleType::class, [ + 'label' => 'settings.behavior.data_source_synonyms.row_type.form.locale', + 'required' => true, + // Restrict to languages configured in the language menu: disable ChoiceLoader and provide explicit choices + 'choice_loader' => null, + 'choices' => $this->buildLocaleChoices(), + 'preferred_choices' => $this->getPreferredLocales(), + 'constraints' => [ + new Assert\NotBlank(message: 'settings.system.data_source_synonyms.row_type.value_not_blank'), + ], + ]) + ->add('translation_singular', TextType::class, [ + 'label' => 'settings.behavior.data_source_synonyms.row_type.form.translation_singular', + 'required' => true, + 'empty_data' => '', + 'constraints' => [ + new Assert\NotBlank(message: 'settings.system.data_source_synonyms.row_type.value_not_blank'), + ], + ]) + ->add('translation_plural', TextType::class, [ + 'label' => 'settings.behavior.data_source_synonyms.row_type.form.translation_plural', + 'required' => true, + 'empty_data' => '', + 'constraints' => [ + new Assert\NotBlank(message: 'settings.system.data_source_synonyms.row_type.value_not_blank'), + ], + ]); + } + + private function buildDataSourceChoices(array $dataSources): array + { + $choices = []; + foreach ($dataSources as $key => $label) { + $choices[(string)$label] = (string)$key; + } + return $choices; + } + + /** + * Returns only locales configured in the language menu (settings) or falls back to the parameter. + * Format: ['German (DE)' => 'de', ...] + */ + private function buildLocaleChoices(): array + { + $locales = $this->getPreferredLocales(); + $choices = []; + foreach ($locales as $code) { + $label = Locales::getName($code); + $choices[$label . ' (' . strtoupper($code) . ')'] = $code; + } + return $choices; + } + + /** + * Source of allowed locales: + * 1) LocalizationSettings->languageMenuEntries (if set) + * 2) Fallback: parameter partdb.locale_menu + */ + private function getPreferredLocales(): array + { + $fromSettings = $this->localizationSettings->languageMenuEntries ?? []; + return !empty($fromSettings) ? array_values($fromSettings) : array_values($this->preferredLanguagesParam); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired('data_sources'); + $resolver->setAllowedTypes('data_sources', 'array'); + + $resolver->setDefaults([ + 'error_translation_domain' => 'validators', + ]); + } +} diff --git a/src/Form/Type/DataSourceSynonymsCollectionType.php b/src/Form/Type/DataSourceSynonymsCollectionType.php new file mode 100644 index 000000000..3853d56ad --- /dev/null +++ b/src/Form/Type/DataSourceSynonymsCollectionType.php @@ -0,0 +1,180 @@ +addModelTransformer(new CallbackTransformer( + // Model -> View + function ($modelValue) { + if (!is_array($modelValue)) { + return [[ + 'dataSource' => null, + 'locale' => null, + 'translation_singular' => null, + 'translation_plural' => null, + ]]; + } + + return $modelValue === [] ? [[ + 'dataSource' => null, + 'locale' => null, + 'translation_singular' => null, + 'translation_plural' => null, + ]] : $modelValue; + }, + // View -> Model (keep list; let existing behavior unchanged) + function ($viewValue) { + if (!is_array($viewValue)) { + return []; + } + $out = []; + foreach ($viewValue as $row) { + if (is_array($row)) { + $out[] = $row; + } + } + return $out; + } + )); + + // Validation and normalization (duplicates + sorting) during SUBMIT + $builder->addEventListener(FormEvents::SUBMIT, function (FormEvent $event): void { + $form = $event->getForm(); + $rows = $event->getData(); + + if (!is_array($rows)) { + return; + } + + // Duplicate check: (dataSource, locale) must be unique + $seen = []; + $hasDuplicate = false; + + foreach ($rows as $idx => $row) { + if (!is_array($row)) { + continue; + } + $ds = $row['dataSource'] ?? null; + $loc = $row['locale'] ?? null; + + if (is_string($ds) && $ds !== '' && is_string($loc) && $loc !== '') { + $key = $ds . '|' . $loc; + if (isset($seen[$key])) { + $hasDuplicate = true; + + if ($form->has((string)$idx)) { + $child = $form->get((string)$idx); + + if ($child->has('dataSource')) { + $child->get('dataSource')->addError( + new FormError($this->translator->trans( + 'settings.system.data_source_synonyms.collection_type.duplicate', + [], 'validators' + )) + ); + } + if ($child->has('locale')) { + $child->get('locale')->addError( + new FormError($this->translator->trans( + 'settings.system.data_source_synonyms.collection_type.duplicate', + [], 'validators' + )) + ); + } + } + } else { + $seen[$key] = true; + } + } + } + + if ($hasDuplicate) { + return; + } + + // Overall sort: first by dataSource key, then by localized language name + $sortable = $rows; + + usort($sortable, static function ($a, $b) { + $aDs = (string)($a['dataSource'] ?? ''); + $bDs = (string)($b['dataSource'] ?? ''); + + $cmpDs = strcasecmp($aDs, $bDs); + if ($cmpDs !== 0) { + return $cmpDs; + } + + $aLoc = (string)($a['locale'] ?? ''); + $bLoc = (string)($b['locale'] ?? ''); + + $aName = Locales::getName($aLoc); + $bName = Locales::getName($bLoc); + + return strcasecmp($aName, $bName); + }); + + $event->setData($sortable); + }); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setRequired(['data_sources']); + $resolver->setAllowedTypes('data_sources', 'array'); + + // Defaults for the collection and entry type + $resolver->setDefaults([ + 'entry_type' => DataSourceSynonymRowType::class, + 'allow_add' => true, + 'allow_delete' => true, + 'by_reference' => false, + 'required' => false, + 'prototype' => true, + 'empty_data' => [], + 'entry_options' => ['label' => false], + 'error_translation_domain' => 'validators', + ]); + + // Pass data_sources automatically to each row (DataSourceSynonymRowType) + $resolver->setNormalizer('entry_options', function (Options $options, $value) { + $value = is_array($value) ? $value : []; + return $value + ['data_sources' => $options['data_sources']]; + }); + } + + public function getParent(): ?string + { + return CollectionType::class; + } + + public function getBlockPrefix(): string + { + return 'datasource_synonyms_collection'; + } +} diff --git a/src/Form/Type/LocaleSelectType.php b/src/Form/Type/LocaleSelectType.php index b87932d12..6dc6f9fc0 100644 --- a/src/Form/Type/LocaleSelectType.php +++ b/src/Form/Type/LocaleSelectType.php @@ -23,19 +23,18 @@ namespace App\Form\Type; -use App\Settings\SystemSettings\LocalizationSettings; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\OptionsResolver\OptionsResolver; /** * A locale select field that uses the preferred languages from the configuration. - */ class LocaleSelectType extends AbstractType { - public function __construct(private LocalizationSettings $localizationSetting) + public function __construct(#[Autowire(param: 'partdb.locale_menu')] private readonly array $preferred_languages) { } @@ -47,7 +46,7 @@ public function getParent(): string public function configureOptions(OptionsResolver $resolver): void { $resolver->setDefaults([ - 'preferred_choices' => array_column($this->localizationSetting->preferredLanguages, 'value'), + 'preferred_choices' => $this->preferred_languages, ]); } } diff --git a/src/Services/Misc/DataSourceSynonymResolver.php b/src/Services/Misc/DataSourceSynonymResolver.php new file mode 100644 index 000000000..8e1646705 --- /dev/null +++ b/src/Services/Misc/DataSourceSynonymResolver.php @@ -0,0 +1,71 @@ +. + */ + +declare(strict_types=1); + +namespace App\Services\Misc; + +use App\Settings\BehaviorSettings\DataSourceSynonymsSettings; +use Symfony\Contracts\Translation\TranslatorInterface; + +readonly class DataSourceSynonymResolver +{ + public function __construct( + private TranslatorInterface $translator, + private DataSourceSynonymsSettings $synonymsSettings, + ) { + } + + public function displayNamePlural(string $dataSource, string $defaultKey, ?string $locale = null): string + { + $locale ??= $this->translator->getLocale(); + $syn = $this->synonyms($dataSource, $locale); + + if ($syn['plural'] !== '') { + return $syn['plural']; + } + + return $this->translator->trans($defaultKey, locale: $locale); + } + + public function displayNameSingular(string $dataSource, string $defaultKey, ?string $locale = null): string + { + $locale ??= $this->translator->getLocale(); + $syn = $this->synonyms($dataSource, $locale); + + if ($syn['singular'] !== '') { + return $syn['singular']; + } + + return $this->translator->trans($defaultKey, locale: $locale); + } + + private function synonyms(string $dataSource, string $locale): array + { + $all = $this->synonymsSettings->getSynonymsAsArray(); + $row = $all[$dataSource][$locale] ?? ['singular' => '', 'plural' => '']; + + return [ + 'singular' => (string)($row['singular'] ?? ''), + 'plural' => (string)($row['plural'] ?? ''), + ]; + } +} diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index 9a22ad4f2..fa22bd0a1 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -38,7 +38,7 @@ use App\Entity\UserSystem\User; use App\Helpers\Trees\TreeViewNode; use App\Services\Cache\UserCacheKeyGenerator; -use App\Settings\BehaviorSettings\DataSourceSynonymsSettings; +use App\Services\Misc\DataSourceSynonymResolver; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Cache\ItemInterface; @@ -57,7 +57,7 @@ public function __construct( protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security, - protected DataSourceSynonymsSettings $dataSourceSynonymsSettings, + protected readonly DataSourceSynonymResolver $synonymResolver, ) { } @@ -173,37 +173,60 @@ protected function getEditNodes(): array } if ($this->security->isGranted('read', new Category())) { $nodes[] = (new TreeViewNode( - $this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()), + $this->synonymResolver->displayNamePlural( + 'category', + 'tree.tools.edit.categories', + $this->translator->getLocale() + ), $this->urlGenerator->generate('category_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-tags'); } if ($this->security->isGranted('read', new Project())) { $nodes[] = (new TreeViewNode( - $this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()), + $this->synonymResolver->displayNamePlural( + 'project', + 'tree.tools.edit.projects', + $this->translator->getLocale()), $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } if ($this->security->isGranted('read', new Supplier())) { $nodes[] = (new TreeViewNode( - $this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()), + $this->synonymResolver->displayNamePlural( + 'supplier', + 'tree.tools.edit.suppliers', + $this->translator->getLocale() + ), $this->urlGenerator->generate('supplier_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-truck'); } if ($this->security->isGranted('read', new Manufacturer())) { $nodes[] = (new TreeViewNode( - $this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()), + $this->synonymResolver->displayNamePlural( + 'manufacturer', + 'tree.tools.edit.manufacturer', + $this->translator->getLocale() + ), $this->urlGenerator->generate('manufacturer_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-industry'); } if ($this->security->isGranted('read', new StorageLocation())) { $nodes[] = (new TreeViewNode( - $this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()), + $this->synonymResolver->displayNamePlural( + 'storagelocation', + 'tree.tools.edit.storelocation', + $this->translator->getLocale() + ), $this->urlGenerator->generate('store_location_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-cube'); } if ($this->security->isGranted('read', new Footprint())) { $nodes[] = (new TreeViewNode( - $this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()), + $this->synonymResolver->displayNamePlural( + 'footprint', + 'tree.tools.edit.footprint', + $this->translator->getLocale() + ), $this->urlGenerator->generate('footprint_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-microchip'); } @@ -317,24 +340,4 @@ protected function getSystemNodes(): array return $nodes; } - - protected function getTranslatedDataSourceOrSynonym(string $dataSource, string $translationKey, string $locale): string - { - $currentTranslation = $this->translator->trans($translationKey); - - $synonyms = $this->dataSourceSynonymsSettings->getSynonymsAsArray(); - - // Call alternatives from DataSourcesynonyms (if available) - if (!empty($synonyms[$dataSource][$locale])) { - $alternativeTranslation = $synonyms[$dataSource][$locale]; - - // Use alternative translation when it deviates from the standard translation - if ($alternativeTranslation !== $currentTranslation) { - return $alternativeTranslation; - } - } - - // Otherwise return the standard translation - return $currentTranslation; - } } diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 4b30cb184..1cd1730f2 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -34,11 +34,10 @@ use App\Helpers\Trees\TreeViewNode; use App\Helpers\Trees\TreeViewNodeIterator; use App\Repository\NamedDBElementRepository; -use App\Repository\StructuralDBElementRepository; use App\Services\Cache\ElementCacheTagGenerator; use App\Services\Cache\UserCacheKeyGenerator; use App\Services\EntityURLGenerator; -use App\Settings\BehaviorSettings\DataSourceSynonymsSettings; +use App\Services\Misc\DataSourceSynonymResolver; use App\Settings\BehaviorSettings\SidebarSettings; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; @@ -68,7 +67,7 @@ public function __construct( protected TranslatorInterface $translator, private readonly UrlGeneratorInterface $router, private readonly SidebarSettings $sidebarSettings, - protected DataSourceSynonymsSettings $dataSourceSynonymsSettings, + protected readonly DataSourceSynonymResolver $synonymResolver ) { $this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled; $this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded; @@ -217,12 +216,12 @@ protected function entityClassToRootNodeString(string $class): string $locale = $this->translator->getLocale(); return match ($class) { - Category::class => $this->getTranslatedOrSynonym('category', $locale), - StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale), - Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale), - Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale), - Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale), - Project::class => $this->getTranslatedOrSynonym('project', $locale), + Category::class => $this->synonymResolver->displayNamePlural('category', $locale), + StorageLocation::class => $this->synonymResolver->displayNamePlural('storelocation', $locale), + Footprint::class => $this->synonymResolver->displayNamePlural('footprint', $locale), + Manufacturer::class => $this->synonymResolver->displayNamePlural('manufacturer', $locale), + Supplier::class => $this->synonymResolver->displayNamePlural('supplier', $locale), + Project::class => $this->synonymResolver->displayNamePlural('project', $locale), default => $this->translator->trans('tree.root_node.text'), }; } @@ -278,24 +277,4 @@ public function getGenericTree(string $class, ?AbstractStructuralDBElement $pare return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line }); } - - protected function getTranslatedOrSynonym(string $key, string $locale): string - { - $currentTranslation = $this->translator->trans($key . '.labelp'); - - $synonyms = $this->dataSourceSynonymsSettings->getSynonymsAsArray(); - - // Call alternatives from DataSourcesynonyms (if available) - if (!empty($synonyms[$key][$locale])) { - $alternativeTranslation = $synonyms[$key][$locale]; - - // Use alternative translation when it deviates from the standard translation - if ($alternativeTranslation !== $currentTranslation) { - return $alternativeTranslation; - } - } - - // Otherwise return the standard translation - return $currentTranslation; - } } diff --git a/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php b/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php index 74b9a2a1c..c10792d67 100644 --- a/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php +++ b/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php @@ -4,7 +4,7 @@ namespace App\Settings\BehaviorSettings; -use App\Form\Type\DataSourceJsonType; +use App\Form\Type\DataSourceSynonymsCollectionType; use App\Settings\SettingsIcon; use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; use Jbtronics\SettingsBundle\ParameterTypes\StringType; @@ -20,11 +20,12 @@ class DataSourceSynonymsSettings { use SettingsTrait; - #[SettingsParameter(ArrayType::class, + #[SettingsParameter( + ArrayType::class, label: new TM("settings.system.data_source_synonyms.configuration"), - description: new TM("settings.system.data_source_synonyms.configuration.help", ['%format%' => '{"en":"", "de":""}']), - options: ['type' => StringType::class], - formType: DataSourceJsonType::class, + description: new TM("settings.system.data_source_synonyms.configuration.help"), + options: ['type' => ArrayType::class, 'options' => ['type' => StringType::class]], + formType: DataSourceSynonymsCollectionType::class, formOptions: [ 'required' => false, 'data_sources' => [ @@ -35,39 +36,55 @@ class DataSourceSynonymsSettings 'supplier' => new TM("settings.behavior.data_source_synonyms.supplier"), 'project' => new TM("settings.behavior.data_source_synonyms.project"), ], - 'default_values' => [ - 'category' => '{"en":"Categories", "de":"Kategorien"}', - 'storagelocation' => '{"en":"Storage locations", "de":"Lagerorte"}', - 'footprint' => '{"en":"Footprints", "de":"Footprints"}', - 'manufacturer' => '{"en":"Manufacturers", "de":"Hersteller"}', - 'supplier' => '{"en":"Suppliers", "de":"Lieferanten"}', - 'project' => '{"en":"Projects", "de":"Projekte"}', - ], ], )] #[Assert\Type('array')] + #[Assert\All([new Assert\Type('array')])] public array $dataSourceSynonyms = [ - 'category' => '{"en":"Categories", "de":"Kategorien"}', - 'storagelocation' => '{"en":"Storage locations", "de":"Lagerorte"}', - 'footprint' => '{"en":"Footprints", "de":"Footprints"}', - 'manufacturer' => '{"en":"Manufacturers", "de":"Hersteller"}', - 'supplier' => '{"en":"Suppliers", "de":"Lieferanten"}', - 'project' => '{"en":"Projects", "de":"Projekte"}', + // flat list of rows, e.g.: + // ['dataSource' => 'category', 'locale' => 'en', 'translation_singular' => 'Category', 'translation_plural' => 'Categories'], ]; /** - * Get the synonyms data as a structured array. + * Normalize to map form: + * [dataSource => [locale => ['singular' => string, 'plural' => string]]] + * No preference/merging is applied; both values are returned as provided (missing ones as empty strings). * - * @return array> The data source synonyms parsed from JSON to array. + * @return array> */ public function getSynonymsAsArray(): array { $result = []; - foreach ($this->dataSourceSynonyms as $key => $jsonString) { - $result[$key] = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR) ?? []; + + foreach ($this->dataSourceSynonyms as $row) { + if (!is_array($row)) { + continue; + } + + $ds = $row['dataSource'] ?? null; + $loc = $row['locale'] ?? null; + + if (!is_string($ds) || $ds === '' || !is_string($loc) || $loc === '') { + continue; + } + + // Read both fields independently; do not prefer one over the other. + $singular = isset($row['translation_singular']) && is_string($row['translation_singular']) + ? $row['translation_singular'] : ''; + $plural = isset($row['translation_plural']) && is_string($row['translation_plural']) + ? $row['translation_plural'] : ''; + + // For legacy data (optional): if only "text" exists and both fields are empty, keep it as given in both slots or leave empty? + // Requirement says: no preference, just return values. We therefore do NOT map legacy automatically. + // If you want to expose legacy "text" as well, handle it outside or migrate data beforehand. + + $result[$ds] ??= []; + $result[$ds][$loc] = [ + 'singular' => $singular, + 'plural' => $plural, + ]; } return $result; } - } diff --git a/src/Settings/SystemSettings/PreferredLocales.php b/src/Settings/SystemSettings/PreferredLocales.php deleted file mode 100644 index 1fe38a548..000000000 --- a/src/Settings/SystemSettings/PreferredLocales.php +++ /dev/null @@ -1,37 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\Settings\SystemSettings; - -enum PreferredLocales: string -{ - case EN = 'en'; - case DE = 'de'; - case IT = 'it'; - case FR = 'fr'; - case RU = 'ru'; - case JA = 'ja'; - case CS = 'cs'; - case DA = 'da'; - case ZH = 'zh'; - case PL = 'pl'; -} diff --git a/src/Twig/DataSourceNameExtension.php b/src/Twig/DataSourceNameExtension.php index d0d8b4b52..693b3a368 100644 --- a/src/Twig/DataSourceNameExtension.php +++ b/src/Twig/DataSourceNameExtension.php @@ -2,42 +2,66 @@ namespace App\Twig; -use App\Settings\BehaviorSettings\DataSourceSynonymsSettings; +use App\Services\Misc\DataSourceSynonymResolver; use Symfony\Contracts\Translation\TranslatorInterface; use Twig\Extension\AbstractExtension; use Twig\TwigFunction; class DataSourceNameExtension extends AbstractExtension { - private TranslatorInterface $translator; - private array $dataSourceSynonyms; - - public function __construct(TranslatorInterface $translator, DataSourceSynonymsSettings $dataSourceSynonymsSettings) - { - $this->translator = $translator; - $this->dataSourceSynonyms = $dataSourceSynonymsSettings->getSynonymsAsArray(); + public function __construct( + private readonly TranslatorInterface $translator, + private readonly DataSourceSynonymResolver $resolver, + ) { } public function getFunctions(): array { return [ - new TwigFunction('get_data_source_name', [$this, 'getDataSourceName']), + new TwigFunction('get_data_source_name_singular', [$this, 'getDataSourceNameSingular']), + new TwigFunction('get_data_source_name_plural', [$this, 'getDataSourceNamePlural']), + new TwigFunction('data_source_name_with_hint', [$this, 'getDataSourceNameWithHint']), ]; } /** - * Based on the locale and data source names, gives the right synonym value back or the default translator value. + * Returns the singular synonym for the given data source in current locale, + * or the translated fallback key if no synonym provided. + */ + public function getDataSourceNameSingular(string $dataSourceName, string $defaultKeySingular): string + { + return $this->resolver->displayNameSingular($dataSourceName, $defaultKeySingular, $this->translator->getLocale()); + } + + /** + * Returns the plural synonym for the given data source in current locale, + * or the translated fallback key if no synonym provided. */ - public function getDataSourceName(string $dataSourceName, string $defaultKey): string + public function getDataSourceNamePlural(string $dataSourceName, string $defaultKeyPlural): string { - $locale = $this->translator->getLocale(); + return $this->resolver->displayNamePlural($dataSourceName, $defaultKeyPlural, $this->translator->getLocale()); + } + + /** + * Like data_source_name, only with a note if a synonym was set (uses translation key 'datasource.synonym'). + */ + public function getDataSourceNameWithHint(string $dataSourceName, string $defaultKey, string $type = 'singular'): string + { + $type = $type === 'singular' ? 'singular' : 'plural'; + + $resolved = $type === 'singular' + ? $this->resolver->displayNameSingular($dataSourceName, $defaultKey, $this->translator->getLocale()) + : $this->resolver->displayNamePlural($dataSourceName, $defaultKey, $this->translator->getLocale()); + + $fallback = $this->translator->trans($defaultKey); - // Use alternative dataSource synonym (if available) - if (isset($this->dataSourceSynonyms[$dataSourceName][$locale])) { - return $this->dataSourceSynonyms[$dataSourceName][$locale]; + if ($resolved !== $fallback) { + return $this->translator->trans('datasource.synonym', [ + '%name%' => $fallback, + '%synonym%' => $resolved, + ]); } - // Otherwise return the standard translation - return $this->translator->trans($defaultKey); + return $fallback; } } diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index 82089a283..7478de11d 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -1,9 +1,8 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% set dataSourceName = get_data_source_name('category', 'category.labelp') %} - {% set translatedSource = 'category.labelp'|trans %} - {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} + + {{ data_source_name_with_hint('category', 'category.label') }} {% endblock %} {% block additional_pills %} @@ -63,4 +62,4 @@ {{ form_row(form.eda_info.kicad_symbol) }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/footprint_admin.html.twig b/templates/admin/footprint_admin.html.twig index a6acbe84e..e76fa52f6 100644 --- a/templates/admin/footprint_admin.html.twig +++ b/templates/admin/footprint_admin.html.twig @@ -1,9 +1,8 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% set dataSourceName = get_data_source_name('footprint', 'footprint.labelp') %} - {% set translatedSource = 'footprint.labelp'|trans %} - {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} + + {{ data_source_name_with_hint('footprint', 'footprint.labelp') }} {% endblock %} {% block master_picture_block %} @@ -36,4 +35,4 @@ {{ form_row(form.eda_info.kicad_footprint) }} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/manufacturer_admin.html.twig b/templates/admin/manufacturer_admin.html.twig index 3ce9a124c..3289fb08d 100644 --- a/templates/admin/manufacturer_admin.html.twig +++ b/templates/admin/manufacturer_admin.html.twig @@ -1,9 +1,8 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% set dataSourceName = get_data_source_name('manufacturer', 'manufacturer.caption') %} - {% set translatedSource = 'manufacturer.caption'|trans %} - {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} + + {{ data_source_name_with_hint('manufacturer', 'manufacturer.caption') }} {% endblock %} {% block edit_title %} @@ -12,4 +11,4 @@ {% block new_title %} {% trans %}manufacturer.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/project_admin.html.twig b/templates/admin/project_admin.html.twig index 8066d5451..044f50a5b 100644 --- a/templates/admin/project_admin.html.twig +++ b/templates/admin/project_admin.html.twig @@ -3,9 +3,8 @@ {# @var entity App\Entity\ProjectSystem\Project #} {% block card_title %} - {% set dataSourceName = get_data_source_name('project', 'project.caption') %} - {% set translatedSource = 'project.caption'|trans %} - {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} + + {{ data_source_name_with_hint('project', 'project.labelp') }} {% endblock %} {% block edit_title %} @@ -61,4 +60,4 @@ {% endif %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/storelocation_admin.html.twig b/templates/admin/storelocation_admin.html.twig index 1e60eeea2..954c03222 100644 --- a/templates/admin/storelocation_admin.html.twig +++ b/templates/admin/storelocation_admin.html.twig @@ -2,9 +2,8 @@ {% import "label_system/dropdown_macro.html.twig" as dropdown %} {% block card_title %} - {% set dataSourceName = get_data_source_name('storagelocation', 'storelocation.labelp') %} - {% set translatedSource = 'storelocation.labelp'|trans %} - {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} + + {{ data_source_name_with_hint('storagelocation', 'storelocation.labelp') }} {% endblock %} {% block additional_controls %} @@ -40,4 +39,4 @@ {% block new_title %} {% trans %}storelocation.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/supplier_admin.html.twig b/templates/admin/supplier_admin.html.twig index b5cf7b236..7b0813d48 100644 --- a/templates/admin/supplier_admin.html.twig +++ b/templates/admin/supplier_admin.html.twig @@ -1,9 +1,8 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - {% set dataSourceName = get_data_source_name('supplier', 'supplier.caption') %} - {% set translatedSource = 'supplier.caption'|trans %} - {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %} + + {{ data_source_name_with_hint('supplier', 'supplier.labelp') }} {% endblock %} {% block additional_panes %} @@ -21,4 +20,4 @@ {% block new_title %} {% trans %}supplier.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index e82cd3b4b..673b81080 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -20,9 +20,14 @@ {% for source in data_sources %} {% if source[3] %} {# show_condition #} -
  • +
  • + +
  • {% endif %} {% endfor %} {% endmacro %} diff --git a/templates/form/datasource_synonyms_collection.html.twig b/templates/form/datasource_synonyms_collection.html.twig new file mode 100644 index 000000000..8b759791d --- /dev/null +++ b/templates/form/datasource_synonyms_collection.html.twig @@ -0,0 +1,48 @@ +{% block datasource_synonyms_collection_widget %} + {% set _attrs = attr|default({}) %} + {% set _attrs = _attrs|merge({ + class: (_attrs.class|default('') ~ ' datasource-synonyms-collection-widget')|trim + }) %} + + {% set has_proto = prototype is defined %} + {% if has_proto %} + {% set __proto %} +
    + {{ form_widget(prototype) }} +
    + +
    +
    + {% endset %} + {% set _proto_html = __proto|e('html_attr') %} + {% set _proto_name = form.vars.prototype_name|default('__name__') %} + {% set _index = form|length %} + {% endif %} + +
    +
    + {% for child in form %} +
    + {{ form_widget(child) }} +
    + +
    +
    + {% endfor %} +
    + +
    +{% endblock %} diff --git a/templates/form/permission_layout.html.twig b/templates/form/permission_layout.html.twig index 07ae6c493..aefd9edd9 100644 --- a/templates/form/permission_layout.html.twig +++ b/templates/form/permission_layout.html.twig @@ -21,7 +21,7 @@ {% set dataSource = 'project' %} {% endif %} - {% set dataSourceName = get_data_source_name(dataSource, form.vars.label) %} + {% set dataSourceName = get_data_source_name_plural(dataSource, form.vars.label) %} {% set translatedSource = form.vars.label|trans %} {% if dataSourceName != translatedSource %} {{ translatedSource }} diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 8757d6c70..cefc9bb38 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -12828,7 +12828,7 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz settings.system.data_source_synonyms.configuration.help - Definujte vlastní synonyma pro dané zdroje dat. Očekává se formát JSON s vašimi preferovanými jazykovými ISO kódy. Příklad: %format%. + Definujte vlastní synonyma pro zadané zdroje dat. Volně přidávat zdroj dat, jazyk a překlady; Jazyky, které se nepoužívají, zůstávají prázdné. @@ -12867,6 +12867,42 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Projekt + + + settings.behavior.data_source_synonyms.collection.add_entry + Přidat položku + + + + + settings.behavior.data_source_synonyms.collection.remove_entry + Odebrat položku + + + + + settings.behavior.data_source_synonyms.row_type.form.datasource + Zdroj dat + + + + + settings.behavior.data_source_synonyms.row_type.form.locale + Místní nastavení + + + + + settings.behavior.data_source_synonyms.row_type.form.translation_singular + Překlad (jednotné číslo) + + + + + settings.behavior.data_source_synonyms.row_type.form.translation_plural + Překlad (množné číslo) + + settings.system.privacy diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 3fb6d28b1..646281a53 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -12896,7 +12896,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön settings.system.data_source_synonyms.configuration.help - Definieren Sie Ihre eigenen Synonyme für die angegebenen Datenquellen. Erwartet wird ein JSON-Format mit Ihren bevorzugten Sprache-ISO-Codes. Beispiel: %format%. + Definieren Sie Ihre eigenen Synonyme für die angegebenen Datenquellen. Datenquelle, Sprache und Übersetzungen frei hinzufügen; Nicht verwendete Sprachen bleiben leer. @@ -12935,6 +12935,42 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Projekt + + + settings.behavior.data_source_synonyms.collection.add_entry + Eintrag hinzufügen + + + + + settings.behavior.data_source_synonyms.collection.remove_entry + Eintrag entfernen + + + + + settings.behavior.data_source_synonyms.row_type.form.datasource + Datenquelle + + + + + settings.behavior.data_source_synonyms.row_type.form.locale + Sprache + + + + + settings.behavior.data_source_synonyms.row_type.form.translation_singular + Übersetzung (Singular) + + + + + settings.behavior.data_source_synonyms.row_type.form.translation_plural + Übersetzung (Plural) + + settings.system.privacy diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index ad772b12f..5d2c404e0 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12897,7 +12897,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g settings.system.data_source_synonyms.configuration.help - Define your own synonyms for the given data sources. Expected in JSON-format with your preferred language iso-codes. Example: %format%. + Define your own synonyms for the given data sources. Add data source, language and translations freely; unused languages remain empty. @@ -12936,6 +12936,42 @@ Please note, that you can not impersonate a disabled user. If you try you will g Project + + + settings.behavior.data_source_synonyms.collection.add_entry + Add entry + + + + + settings.behavior.data_source_synonyms.collection.remove_entry + Remove entry + + + + + settings.behavior.data_source_synonyms.row_type.form.datasource + Data source + + + + + settings.behavior.data_source_synonyms.row_type.form.locale + Locale + + + + + settings.behavior.data_source_synonyms.row_type.form.translation_singular + Translation singular + + + + + settings.behavior.data_source_synonyms.row_type.form.translation_plural + Translation plural + + settings.system.privacy diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index c298266af..5f1f9a540 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -365,5 +365,17 @@ Neplatný kód. Zkontrolujte, zda je vaše ověřovací aplikace správně nastavena a zda je čas správně nastaven jak na serveru, tak na ověřovacím zařízení. + + + settings.system.data_source_synonyms.row_type.value_not_blank + Tato hodnota nemůže být prázdná. + + + + + settings.system.data_source_synonyms.collection_type.duplicate + Dvojitá kombinace zdroje dat a jazyka. + + diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 5cccd3887..5d226d6a2 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -365,5 +365,17 @@ Ungültiger Code. Überprüfen Sie, ob die Authenticator App korrekt eingerichtet ist und ob der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben. + + + settings.system.data_source_synonyms.row_type.value_not_blank + Dieser Wert darf nicht leer sein. + + + + + settings.system.data_source_synonyms.collection_type.duplicate + Doppelte Kombination aus Datenquelle und Sprache. + + diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 6ad144607..e0f4be15e 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -365,5 +365,17 @@ Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly. + + + settings.system.data_source_synonyms.row_type.value_not_blank + This value should not be blank. + + + + + settings.system.data_source_synonyms.collection_type.duplicate + Duplicate combination of data source and locale. + + From 8c139fbe04b7803f9014391e7951967b8dff2835 Mon Sep 17 00:00:00 2001 From: Marcel Diegelmann Date: Wed, 5 Nov 2025 14:37:38 +0100 Subject: [PATCH 04/27] =?UTF-8?q?Erm=C3=B6gliche=20R=C3=BCckgabe=20aller?= =?UTF-8?q?=20m=C3=B6glichen=20Sprachoptionen=20in=20Verbindung=20mit=20de?= =?UTF-8?q?n=20vom=20Nutzer=20freigeschalteten.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Form/Type/DataSourceSynonymRowType.php | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/Form/Type/DataSourceSynonymRowType.php b/src/Form/Type/DataSourceSynonymRowType.php index eeed32cd4..58970ce7b 100644 --- a/src/Form/Type/DataSourceSynonymRowType.php +++ b/src/Form/Type/DataSourceSynonymRowType.php @@ -60,7 +60,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => true, // Restrict to languages configured in the language menu: disable ChoiceLoader and provide explicit choices 'choice_loader' => null, - 'choices' => $this->buildLocaleChoices(), + 'choices' => $this->buildLocaleChoices(true), 'preferred_choices' => $this->getPreferredLocales(), 'constraints' => [ new Assert\NotBlank(message: 'settings.system.data_source_synonyms.row_type.value_not_blank'), @@ -97,9 +97,14 @@ private function buildDataSourceChoices(array $dataSources): array * Returns only locales configured in the language menu (settings) or falls back to the parameter. * Format: ['German (DE)' => 'de', ...] */ - private function buildLocaleChoices(): array + private function buildLocaleChoices(bool $returnPossible = false): array { $locales = $this->getPreferredLocales(); + + if ($returnPossible) { + $locales = $this->getPossibleLocales(); + } + $choices = []; foreach ($locales as $code) { $label = Locales::getName($code); @@ -119,6 +124,11 @@ private function getPreferredLocales(): array return !empty($fromSettings) ? array_values($fromSettings) : array_values($this->preferredLanguagesParam); } + private function getPossibleLocales(): array + { + return array_values($this->preferredLanguagesParam); + } + public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('data_sources'); From 15e9d9e81f2d236fc01b29cab5696d6c55a2fe06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 9 Nov 2025 16:53:02 +0100 Subject: [PATCH 05/27] Removed unnecessary service definition The tag is applied via autoconfiguration --- config/services.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/config/services.yaml b/config/services.yaml index fce32d47f..b48b3eff2 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -188,13 +188,6 @@ services: $fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/' $tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/' - #################################################################################################################### - # Twig Extensions - #################################################################################################################### - - App\Twig\DataSourceNameExtension: - tags: [ 'twig.extension' ] - #################################################################################################################### # Part info provider system #################################################################################################################### From c6ea46b702410e688ee36ac4ff6a9080c4e4b286 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 9 Nov 2025 17:08:07 +0100 Subject: [PATCH 06/27] Use default translations for the NotBlank constraint --- src/Form/Type/DataSourceSynonymRowType.php | 12 ++++-------- translations/validators.cs.xlf | 6 ------ translations/validators.de.xlf | 6 ------ translations/validators.en.xlf | 6 ------ 4 files changed, 4 insertions(+), 26 deletions(-) diff --git a/src/Form/Type/DataSourceSynonymRowType.php b/src/Form/Type/DataSourceSynonymRowType.php index 58970ce7b..2c6dcdbf4 100644 --- a/src/Form/Type/DataSourceSynonymRowType.php +++ b/src/Form/Type/DataSourceSynonymRowType.php @@ -52,7 +52,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'choices' => $this->buildDataSourceChoices($options['data_sources']), 'required' => true, 'constraints' => [ - new Assert\NotBlank(message: 'settings.system.data_source_synonyms.row_type.value_not_blank'), + new Assert\NotBlank(), ], ]) ->add('locale', LocaleType::class, [ @@ -63,7 +63,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'choices' => $this->buildLocaleChoices(true), 'preferred_choices' => $this->getPreferredLocales(), 'constraints' => [ - new Assert\NotBlank(message: 'settings.system.data_source_synonyms.row_type.value_not_blank'), + new Assert\NotBlank(), ], ]) ->add('translation_singular', TextType::class, [ @@ -71,7 +71,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => true, 'empty_data' => '', 'constraints' => [ - new Assert\NotBlank(message: 'settings.system.data_source_synonyms.row_type.value_not_blank'), + new Assert\NotBlank(), ], ]) ->add('translation_plural', TextType::class, [ @@ -79,7 +79,7 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => true, 'empty_data' => '', 'constraints' => [ - new Assert\NotBlank(message: 'settings.system.data_source_synonyms.row_type.value_not_blank'), + new Assert\NotBlank(), ], ]); } @@ -133,9 +133,5 @@ public function configureOptions(OptionsResolver $resolver): void { $resolver->setRequired('data_sources'); $resolver->setAllowedTypes('data_sources', 'array'); - - $resolver->setDefaults([ - 'error_translation_domain' => 'validators', - ]); } } diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 5f1f9a540..4c1f396d9 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -365,12 +365,6 @@ Neplatný kód. Zkontrolujte, zda je vaše ověřovací aplikace správně nastavena a zda je čas správně nastaven jak na serveru, tak na ověřovacím zařízení. - - - settings.system.data_source_synonyms.row_type.value_not_blank - Tato hodnota nemůže být prázdná. - - settings.system.data_source_synonyms.collection_type.duplicate diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 5d226d6a2..3d125c9a7 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -365,12 +365,6 @@ Ungültiger Code. Überprüfen Sie, ob die Authenticator App korrekt eingerichtet ist und ob der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben. - - - settings.system.data_source_synonyms.row_type.value_not_blank - Dieser Wert darf nicht leer sein. - - settings.system.data_source_synonyms.collection_type.duplicate diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index e0f4be15e..c679dac62 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -365,12 +365,6 @@ Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly. - - - settings.system.data_source_synonyms.row_type.value_not_blank - This value should not be blank. - - settings.system.data_source_synonyms.collection_type.duplicate From 0d0effb29024940b45013f7e89af21cf6cd65533 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 9 Nov 2025 18:04:21 +0100 Subject: [PATCH 07/27] Started refactoring ElementTypeNameGenerator --- src/Services/ElementTypeNameGenerator.php | 86 +++----- src/Services/ElementTypes.php | 228 ++++++++++++++++++++++ 2 files changed, 257 insertions(+), 57 deletions(-) create mode 100644 src/Services/ElementTypes.php diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index deb4cf30c..7102c1e5f 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -24,30 +24,15 @@ use App\Entity\Attachments\Attachment; use App\Entity\Attachments\AttachmentContainingDBElement; -use App\Entity\Attachments\AttachmentType; use App\Entity\Base\AbstractDBElement; use App\Entity\Contracts\NamedElementInterface; -use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob; -use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart; -use App\Entity\LabelSystem\LabelProfile; use App\Entity\Parameters\AbstractParameter; -use App\Entity\Parts\Category; -use App\Entity\Parts\Footprint; -use App\Entity\Parts\Manufacturer; -use App\Entity\Parts\MeasurementUnit; use App\Entity\Parts\Part; -use App\Entity\Parts\PartAssociation; -use App\Entity\Parts\PartCustomState; use App\Entity\Parts\PartLot; -use App\Entity\Parts\StorageLocation; -use App\Entity\Parts\Supplier; -use App\Entity\PriceInformations\Currency; use App\Entity\PriceInformations\Orderdetail; use App\Entity\PriceInformations\Pricedetail; use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; -use App\Entity\UserSystem\Group; -use App\Entity\UserSystem\User; use App\Exceptions\EntityNotSupportedException; use Symfony\Contracts\Translation\TranslatorInterface; @@ -56,36 +41,9 @@ */ class ElementTypeNameGenerator { - protected array $mapping; public function __construct(protected TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator) { - //Child classes has to become before parent classes - $this->mapping = [ - Attachment::class => $this->translator->trans('attachment.label'), - Category::class => $this->translator->trans('category.label'), - AttachmentType::class => $this->translator->trans('attachment_type.label'), - Project::class => $this->translator->trans('project.label'), - ProjectBOMEntry::class => $this->translator->trans('project_bom_entry.label'), - Footprint::class => $this->translator->trans('footprint.label'), - Manufacturer::class => $this->translator->trans('manufacturer.label'), - MeasurementUnit::class => $this->translator->trans('measurement_unit.label'), - Part::class => $this->translator->trans('part.label'), - PartLot::class => $this->translator->trans('part_lot.label'), - StorageLocation::class => $this->translator->trans('storelocation.label'), - Supplier::class => $this->translator->trans('supplier.label'), - Currency::class => $this->translator->trans('currency.label'), - Orderdetail::class => $this->translator->trans('orderdetail.label'), - Pricedetail::class => $this->translator->trans('pricedetail.label'), - Group::class => $this->translator->trans('group.label'), - User::class => $this->translator->trans('user.label'), - AbstractParameter::class => $this->translator->trans('parameter.label'), - LabelProfile::class => $this->translator->trans('label_profile.label'), - PartAssociation::class => $this->translator->trans('part_association.label'), - BulkInfoProviderImportJob::class => $this->translator->trans('bulk_info_provider_import_job.label'), - BulkInfoProviderImportJobPart::class => $this->translator->trans('bulk_info_provider_import_job_part.label'), - PartCustomState::class => $this->translator->trans('part_custom_state.label'), - ]; } /** @@ -99,27 +57,41 @@ public function __construct(protected TranslatorInterface $translator, private r * @return string the localized label for the entity type * * @throws EntityNotSupportedException when the passed entity is not supported + * @deprecated Use label() instead */ public function getLocalizedTypeLabel(object|string $entity): string { - $class = is_string($entity) ? $entity : $entity::class; + return $this->typeLabel($entity); + } - //Check if we have a direct array entry for our entity class, then we can use it - if (isset($this->mapping[$class])) { - return $this->mapping[$class]; - } + /** + * Gets a localized label for the type of the entity. If user defined synonyms are defined, + * these are used instead of the default labels. + * @param object|string $entity + * @param string|null $locale + * @return string + */ + public function typeLabel(object|string $entity, ?string $locale = null): string + { + $type = ElementTypes::fromValue($entity); - //Otherwise iterate over array and check for inheritance (needed when the proxy element from doctrine are passed) - foreach ($this->mapping as $class_to_check => $translation) { - if (is_a($entity, $class_to_check, true)) { - return $translation; - } - } + return $this->translator->trans($type->getDefaultLabelKey(), locale: $locale); + } + + /** + * Similar to label(), but returns the plural version of the label. + * @param object|string $entity + * @param string|null $locale + * @return string + */ + public function typeLabelPlural(object|string $entity, ?string $locale = null): string + { + $type = ElementTypes::fromValue($entity); - //When nothing was found throw an exception - throw new EntityNotSupportedException(sprintf('No localized label for the element with type %s was found!', is_object($entity) ? $entity::class : (string) $entity)); + return $this->translator->trans($type->getDefaultPluralLabelKey(), locale: $locale); } + /** * Returns a string like in the format ElementType: ElementName. * For example this could be something like: "Part: BC547". @@ -134,7 +106,7 @@ public function getLocalizedTypeLabel(object|string $entity): string */ public function getTypeNameCombination(NamedElementInterface $entity, bool $use_html = false): string { - $type = $this->getLocalizedTypeLabel($entity); + $type = $this->typeLabel($entity); if ($use_html) { return '' . $type . ': ' . htmlspecialchars($entity->getName()); } @@ -144,7 +116,7 @@ public function getTypeNameCombination(NamedElementInterface $entity, bool $use_ /** - * Returns a HTML formatted label for the given enitity in the format "Type: Name" (on elements with a name) and + * Returns a HTML formatted label for the given entity in the format "Type: Name" (on elements with a name) and * "Type: ID" (on elements without a name). If possible the value is given as a link to the element. * @param AbstractDBElement $entity The entity for which the label should be generated * @param bool $include_associated If set to true, the associated entity (like the part belonging to a part lot) is included in the label to give further information diff --git a/src/Services/ElementTypes.php b/src/Services/ElementTypes.php new file mode 100644 index 000000000..0ac922263 --- /dev/null +++ b/src/Services/ElementTypes.php @@ -0,0 +1,228 @@ +. + */ + +declare(strict_types=1); + + +namespace App\Services; + +use App\Entity\Attachments\Attachment; +use App\Entity\Attachments\AttachmentType; +use App\Entity\InfoProviderSystem\BulkInfoProviderImportJob; +use App\Entity\InfoProviderSystem\BulkInfoProviderImportJobPart; +use App\Entity\LabelSystem\LabelProfile; +use App\Entity\Parameters\AbstractParameter; +use App\Entity\Parts\Category; +use App\Entity\Parts\Footprint; +use App\Entity\Parts\Manufacturer; +use App\Entity\Parts\MeasurementUnit; +use App\Entity\Parts\Part; +use App\Entity\Parts\PartAssociation; +use App\Entity\Parts\PartCustomState; +use App\Entity\Parts\PartLot; +use App\Entity\Parts\StorageLocation; +use App\Entity\Parts\Supplier; +use App\Entity\PriceInformations\Currency; +use App\Entity\PriceInformations\Orderdetail; +use App\Entity\PriceInformations\Pricedetail; +use App\Entity\ProjectSystem\Project; +use App\Entity\ProjectSystem\ProjectBOMEntry; +use App\Entity\UserSystem\Group; +use App\Entity\UserSystem\User; +use App\Exceptions\EntityNotSupportedException; +use Symfony\Contracts\Translation\TranslatableInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +enum ElementTypes: string implements TranslatableInterface +{ + case ATTACHMENT = "attachment"; + case CATEGORY = "category"; + case ATTACHMENT_TYPE = "attachment_type"; + case PROJECT = "project"; + case PROJECT_BOM_ENTRY = "project_bom_entry"; + case FOOTPRINT = "footprint"; + case MANUFACTURER = "manufacturer"; + case MEASUREMENT_UNIT = "measurement_unit"; + case PART = "part"; + case PART_LOT = "part_lot"; + case STORAGE_LOCATION = "storage_location"; + case SUPPLIER = "supplier"; + case CURRENCY = "currency"; + case ORDERDETAIL = "orderdetail"; + case PRICEDETAIL = "pricedetail"; + case GROUP = "group"; + case USER = "user"; + case PARAMETER = "parameter"; + case LABEL_PROFILE = "label_profile"; + case PART_ASSOCIATION = "part_association"; + case BULK_INFO_PROVIDER_IMPORT_JOB = "bulk_info_provider_import_job"; + case BULK_INFO_PROVIDER_IMPORT_JOB_PART = "bulk_info_provider_import_job_part"; + case PART_CUSTOM_STATE = "part_custom_state"; + + //Child classes has to become before parent classes + private const CLASS_MAPPING = [ + Attachment::class => self::ATTACHMENT, + Category::class => self::CATEGORY, + AttachmentType::class => self::ATTACHMENT_TYPE, + Project::class => self::PROJECT, + ProjectBOMEntry::class => self::PROJECT_BOM_ENTRY, + Footprint::class => self::FOOTPRINT, + Manufacturer::class => self::MANUFACTURER, + MeasurementUnit::class => self::MEASUREMENT_UNIT, + Part::class => self::PART, + PartLot::class => self::PART_LOT, + StorageLocation::class => self::STORAGE_LOCATION, + Supplier::class => self::SUPPLIER, + Currency::class => self::CURRENCY, + Orderdetail::class => self::ORDERDETAIL, + Pricedetail::class => self::PRICEDETAIL, + Group::class => self::GROUP, + User::class => self::USER, + AbstractParameter::class => self::PARAMETER, + LabelProfile::class => self::LABEL_PROFILE, + PartAssociation::class => self::PART_ASSOCIATION, + BulkInfoProviderImportJob::class => self::BULK_INFO_PROVIDER_IMPORT_JOB, + BulkInfoProviderImportJobPart::class => self::BULK_INFO_PROVIDER_IMPORT_JOB_PART, + PartCustomState::class => self::PART_CUSTOM_STATE, + ]; + + /** + * Gets the default translation key for the label of the element type (singular form). + */ + public function getDefaultLabelKey(): string + { + return match ($this) { + self::ATTACHMENT => 'attachment.label', + self::CATEGORY => 'category.label', + self::ATTACHMENT_TYPE => 'attachment_type.label', + self::PROJECT => 'project.label', + self::PROJECT_BOM_ENTRY => 'project_bom_entry.label', + self::FOOTPRINT => 'footprint.label', + self::MANUFACTURER => 'manufacturer.label', + self::MEASUREMENT_UNIT => 'measurement_unit.label', + self::PART => 'part.label', + self::PART_LOT => 'part_lot.label', + self::STORAGE_LOCATION => 'storelocation.label', + self::SUPPLIER => 'supplier.label', + self::CURRENCY => 'currency.label', + self::ORDERDETAIL => 'orderdetail.label', + self::PRICEDETAIL => 'pricedetail.label', + self::GROUP => 'group.label', + self::USER => 'user.label', + self::PARAMETER => 'parameter.label', + self::LABEL_PROFILE => 'label_profile.label', + self::PART_ASSOCIATION => 'part_association.label', + self::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.label', + self::BULK_INFO_PROVIDER_IMPORT_JOB_PART => 'bulk_info_provider_import_job_part.label', + self::PART_CUSTOM_STATE => 'part_custom_state.label', + }; + } + + public function getDefaultPluralLabelKey(): string + { + return match ($this) { + self::ATTACHMENT => 'attachment.labelp', + self::CATEGORY => 'category.labelp', + self::ATTACHMENT_TYPE => 'attachment_type.labelp', + self::PROJECT => 'project.labelp', + self::PROJECT_BOM_ENTRY => 'project_bom_entry.labelp', + self::FOOTPRINT => 'footprint.labelp', + self::MANUFACTURER => 'manufacturer.labelp', + self::MEASUREMENT_UNIT => 'measurement_unit.labelp', + self::PART => 'part.labelp', + self::PART_LOT => 'part_lot.labelp', + self::STORAGE_LOCATION => 'storelocation.labelp', + self::SUPPLIER => 'supplier.labelp', + self::CURRENCY => 'currency.labelp', + self::ORDERDETAIL => 'orderdetail.labelp', + self::PRICEDETAIL => 'pricedetail.labelp', + self::GROUP => 'group.labelp', + self::USER => 'user.labelp', + self::PARAMETER => 'parameter.labelp', + self::LABEL_PROFILE => 'label_profile.labelp', + self::PART_ASSOCIATION => 'part_association.labelp', + self::BULK_INFO_PROVIDER_IMPORT_JOB => 'bulk_info_provider_import_job.labelp', + self::BULK_INFO_PROVIDER_IMPORT_JOB_PART => 'bulk_info_provider_import_job_part.labelp', + self::PART_CUSTOM_STATE => 'part_custom_state.labelp', + }; + } + + /** + * Used to get a user-friendly representation of the object that can be translated. + * For this the singular default label key is used. + * @param TranslatorInterface $translator + * @param string|null $locale + * @return string + */ + public function trans(TranslatorInterface $translator, ?string $locale = null): string + { + return $translator->trans($this->getDefaultLabelKey(), locale: $locale); + } + + /** + * Determines the ElementType from a value, which can either be an enum value, an ElementTypes instance, a class name or an object instance. + * @param string|object $value + * @return self + */ + public static function fromValue(string|object $value): self + { + if ($value instanceof self) { + return $value; + } + if (is_object($value)) { + return self::fromClass($value); + } + + + //Otherwise try to parse it as enum value first + $enumValue = self::tryFrom($value); + + //Otherwise try to get it from class name + return $enumValue ?? self::fromClass($value); + } + + /** + * Determines the ElementType from a class name or object instance. + * @param string|object $class + * @throws EntityNotSupportedException if the class is not supported + * @return self + */ + public static function fromClass(string|object $class): self + { + if (is_object($class)) { + $className = get_class($class); + } else { + $className = $class; + } + + if (array_key_exists($className, self::CLASS_MAPPING)) { + return self::CLASS_MAPPING[$className]; + } + + //Otherwise we need to check for inheritance + foreach (self::CLASS_MAPPING as $entityClass => $elementType) { + if (is_a($className, $entityClass, true)) { + return $elementType; + } + } + + throw new EntityNotSupportedException(sprintf('No localized label for the element with type %s was found!', $className)); + } +} From 92ca46210a3ac96559e3f050d551b75f50367907 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 9 Nov 2025 18:05:04 +0100 Subject: [PATCH 08/27] Made ElementTypeNameGenerator class readonly --- src/Services/ElementTypeNameGenerator.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index 7102c1e5f..d4b039c8c 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -39,10 +39,10 @@ /** * @see \App\Tests\Services\ElementTypeNameGeneratorTest */ -class ElementTypeNameGenerator +final readonly class ElementTypeNameGenerator { - public function __construct(protected TranslatorInterface $translator, private readonly EntityURLGenerator $entityURLGenerator) + public function __construct(private TranslatorInterface $translator, private EntityURLGenerator $entityURLGenerator) { } From 1ec34a2bb0b595d310711e266c364bd3a5b9f293 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 9 Nov 2025 23:02:40 +0100 Subject: [PATCH 09/27] Modified form to work properly with new datastructure --- .../Type/DataSourceSynonymsCollectionType.php | 81 +++++++++++------ .../Misc/DataSourceSynonymResolver.php | 4 +- .../DataSourceSynonymsSettings.php | 90 ------------------- .../DataSourceSynonymsSettings.php | 81 +++++++++++++++++ .../SystemSettings/SystemSettings.php | 1 - 5 files changed, 139 insertions(+), 118 deletions(-) delete mode 100644 src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php create mode 100644 src/Settings/SystemSettings/DataSourceSynonymsSettings.php diff --git a/src/Form/Type/DataSourceSynonymsCollectionType.php b/src/Form/Type/DataSourceSynonymsCollectionType.php index 3853d56ad..5507d5077 100644 --- a/src/Form/Type/DataSourceSynonymsCollectionType.php +++ b/src/Form/Type/DataSourceSynonymsCollectionType.php @@ -27,38 +27,69 @@ public function __construct(private readonly TranslatorInterface $translator) { } - public function buildForm(FormBuilderInterface $builder, array $options): void + private function flattenStructure(array $modelValue): array { - $builder->addModelTransformer(new CallbackTransformer( - // Model -> View - function ($modelValue) { - if (!is_array($modelValue)) { - return [[ - 'dataSource' => null, - 'locale' => null, - 'translation_singular' => null, - 'translation_plural' => null, - ]]; + //If the model is already flattened, return as is + if (array_is_list($modelValue)) { + return $modelValue; + } + + $out = []; + foreach ($modelValue as $dataSource => $locales) { + if (!is_array($locales)) { + continue; + } + foreach ($locales as $locale => $translations) { + if (!is_array($translations)) { + continue; } + $out[] = [ + 'dataSource' => $dataSource, + 'locale' => $locale, + 'translation_singular' => $translations['singular'] ?? '', + 'translation_plural' => $translations['plural'] ?? '', + ]; + } + } + return $out; + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $builder->addEventListener(FormEvents::PRE_SET_DATA, function (FormEvent $event): void { + //Flatten the structure + $data = $event->getData(); + $event->setData($this->flattenStructure($data)); + }); - return $modelValue === [] ? [[ - 'dataSource' => null, - 'locale' => null, - 'translation_singular' => null, - 'translation_plural' => null, - ]] : $modelValue; - }, + $builder->addModelTransformer(new CallbackTransformer( + // Model -> View + $this->flattenStructure(...), // View -> Model (keep list; let existing behavior unchanged) - function ($viewValue) { - if (!is_array($viewValue)) { - return []; - } - $out = []; + function (array $viewValue) { + //Turn our flat list back into the structured array + foreach ($viewValue as $row) { - if (is_array($row)) { - $out[] = $row; + if (!is_array($row)) { + continue; + } + $dataSource = $row['dataSource'] ?? null; + $locale = $row['locale'] ?? null; + $translation_singular = $row['translation_singular'] ?? null; + $translation_plural = $row['translation_plural'] ?? null; + + if (!is_string($dataSource) || $dataSource === '' + || !is_string($locale) || $locale === '' + ) { + continue; } + + $out[$dataSource][$locale] = [ + 'singular' => is_string($translation_singular) ? $translation_singular : '', + 'plural' => is_string($translation_plural) ? $translation_plural : '', + ]; } + return $out; } )); diff --git a/src/Services/Misc/DataSourceSynonymResolver.php b/src/Services/Misc/DataSourceSynonymResolver.php index 8e1646705..57e3d642a 100644 --- a/src/Services/Misc/DataSourceSynonymResolver.php +++ b/src/Services/Misc/DataSourceSynonymResolver.php @@ -23,7 +23,7 @@ namespace App\Services\Misc; -use App\Settings\BehaviorSettings\DataSourceSynonymsSettings; +use App\Settings\SystemSettings\DataSourceSynonymsSettings; use Symfony\Contracts\Translation\TranslatorInterface; readonly class DataSourceSynonymResolver @@ -60,7 +60,7 @@ public function displayNameSingular(string $dataSource, string $defaultKey, ?str private function synonyms(string $dataSource, string $locale): array { - $all = $this->synonymsSettings->getSynonymsAsArray(); + $all = []; $row = $all[$dataSource][$locale] ?? ['singular' => '', 'plural' => '']; return [ diff --git a/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php b/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php deleted file mode 100644 index c10792d67..000000000 --- a/src/Settings/BehaviorSettings/DataSourceSynonymsSettings.php +++ /dev/null @@ -1,90 +0,0 @@ - ArrayType::class, 'options' => ['type' => StringType::class]], - formType: DataSourceSynonymsCollectionType::class, - formOptions: [ - 'required' => false, - 'data_sources' => [ - 'category' => new TM("settings.behavior.data_source_synonyms.category"), - 'storagelocation' => new TM("settings.behavior.data_source_synonyms.storagelocation"), - 'footprint' => new TM("settings.behavior.data_source_synonyms.footprint"), - 'manufacturer' => new TM("settings.behavior.data_source_synonyms.manufacturer"), - 'supplier' => new TM("settings.behavior.data_source_synonyms.supplier"), - 'project' => new TM("settings.behavior.data_source_synonyms.project"), - ], - ], - )] - #[Assert\Type('array')] - #[Assert\All([new Assert\Type('array')])] - public array $dataSourceSynonyms = [ - // flat list of rows, e.g.: - // ['dataSource' => 'category', 'locale' => 'en', 'translation_singular' => 'Category', 'translation_plural' => 'Categories'], - ]; - - /** - * Normalize to map form: - * [dataSource => [locale => ['singular' => string, 'plural' => string]]] - * No preference/merging is applied; both values are returned as provided (missing ones as empty strings). - * - * @return array> - */ - public function getSynonymsAsArray(): array - { - $result = []; - - foreach ($this->dataSourceSynonyms as $row) { - if (!is_array($row)) { - continue; - } - - $ds = $row['dataSource'] ?? null; - $loc = $row['locale'] ?? null; - - if (!is_string($ds) || $ds === '' || !is_string($loc) || $loc === '') { - continue; - } - - // Read both fields independently; do not prefer one over the other. - $singular = isset($row['translation_singular']) && is_string($row['translation_singular']) - ? $row['translation_singular'] : ''; - $plural = isset($row['translation_plural']) && is_string($row['translation_plural']) - ? $row['translation_plural'] : ''; - - // For legacy data (optional): if only "text" exists and both fields are empty, keep it as given in both slots or leave empty? - // Requirement says: no preference, just return values. We therefore do NOT map legacy automatically. - // If you want to expose legacy "text" as well, handle it outside or migrate data beforehand. - - $result[$ds] ??= []; - $result[$ds][$loc] = [ - 'singular' => $singular, - 'plural' => $plural, - ]; - } - - return $result; - } -} diff --git a/src/Settings/SystemSettings/DataSourceSynonymsSettings.php b/src/Settings/SystemSettings/DataSourceSynonymsSettings.php new file mode 100644 index 000000000..ab0cb0532 --- /dev/null +++ b/src/Settings/SystemSettings/DataSourceSynonymsSettings.php @@ -0,0 +1,81 @@ +. + */ + +declare(strict_types=1); + +namespace App\Settings\SystemSettings; + +use App\Form\Type\DataSourceSynonymsCollectionType; +use App\Services\ElementTypes; +use App\Settings\SettingsIcon; +use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; +use Jbtronics\SettingsBundle\ParameterTypes\SerializeType; +use Jbtronics\SettingsBundle\ParameterTypes\StringType; +use Jbtronics\SettingsBundle\Settings\Settings; +use Jbtronics\SettingsBundle\Settings\SettingsParameter; +use Jbtronics\SettingsBundle\Settings\SettingsTrait; +use Symfony\Component\Translation\TranslatableMessage as TM; +use Symfony\Component\Validator\Constraints as Assert; + +#[Settings(label: new TM("settings.system.data_source_synonyms"))] +#[SettingsIcon("fa-language")] +class DataSourceSynonymsSettings +{ + use SettingsTrait; + + #[SettingsParameter( + ArrayType::class, + label: new TM("settings.system.data_source_synonyms.configuration"), + description: new TM("settings.system.data_source_synonyms.configuration.help"), + options: ['type' => SerializeType::class], + formType: DataSourceSynonymsCollectionType::class, + formOptions: [ + 'required' => false, + 'data_sources' => [ + 'category' => new TM("settings.behavior.data_source_synonyms.category"), + 'storagelocation' => new TM("settings.behavior.data_source_synonyms.storagelocation"), + 'footprint' => new TM("settings.behavior.data_source_synonyms.footprint"), + 'manufacturer' => new TM("settings.behavior.data_source_synonyms.manufacturer"), + 'supplier' => new TM("settings.behavior.data_source_synonyms.supplier"), + 'project' => new TM("settings.behavior.data_source_synonyms.project"), + ], + ], + )] + #[Assert\Type('array')] + #[Assert\All([new Assert\Type('array')])] + /** + * @var array> $customTypeLabels + * An array of the form: [ + * 'category' => [ + * 'en' => ['singular' => 'Category', 'plural' => 'Categories'], + * 'de' => ['singular' => 'Kategorie', 'plural' => 'Kategorien'], + * ], + * 'manufacturer' => [ + * 'en' => ['singular' => 'Manufacturer', 'plural' =>'Manufacturers'], + * ], + * ] + */ + public array $customTypeLabels = []; + + public function isCustomLabelDefinedForType(ElementTypes $type): bool + { + return isset($this->customTypeLabels[$type->value]); + } +} diff --git a/src/Settings/SystemSettings/SystemSettings.php b/src/Settings/SystemSettings/SystemSettings.php index 3d6c4041d..2e8871167 100644 --- a/src/Settings/SystemSettings/SystemSettings.php +++ b/src/Settings/SystemSettings/SystemSettings.php @@ -24,7 +24,6 @@ namespace App\Settings\SystemSettings; use Jbtronics\SettingsBundle\Settings\EmbeddedSettings; -use App\Settings\BehaviorSettings\DataSourceSynonymsSettings; use Jbtronics\SettingsBundle\Settings\Settings; use Symfony\Component\Translation\TranslatableMessage as TM; From 88d34447aaa096996da0b0a57461de65572bacad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 9 Nov 2025 23:47:20 +0100 Subject: [PATCH 10/27] Made the form more beautiful and space saving --- src/Form/Type/DataSourceSynonymRowType.php | 24 ++++++++- .../datasource_synonyms_collection.html.twig | 49 ++++++++++--------- .../form/vertical_bootstrap_layout.html.twig | 26 ++++++++++ 3 files changed, 74 insertions(+), 25 deletions(-) create mode 100644 templates/form/vertical_bootstrap_layout.html.twig diff --git a/src/Form/Type/DataSourceSynonymRowType.php b/src/Form/Type/DataSourceSynonymRowType.php index 2c6dcdbf4..445d5c4e0 100644 --- a/src/Form/Type/DataSourceSynonymRowType.php +++ b/src/Form/Type/DataSourceSynonymRowType.php @@ -48,16 +48,24 @@ public function buildForm(FormBuilderInterface $builder, array $options): void { $builder ->add('dataSource', ChoiceType::class, [ - 'label' => 'settings.behavior.data_source_synonyms.row_type.form.datasource', + //'label' => 'settings.behavior.data_source_synonyms.row_type.form.datasource', + 'label' => false, 'choices' => $this->buildDataSourceChoices($options['data_sources']), 'required' => true, + 'row_attr' => [ + 'class' => 'form-floating', + ], 'constraints' => [ new Assert\NotBlank(), ], ]) ->add('locale', LocaleType::class, [ - 'label' => 'settings.behavior.data_source_synonyms.row_type.form.locale', + //'label' => 'settings.behavior.data_source_synonyms.row_type.form.locale', + 'label' => false, 'required' => true, + 'row_attr' => [ + 'class' => 'form-floating', + ], // Restrict to languages configured in the language menu: disable ChoiceLoader and provide explicit choices 'choice_loader' => null, 'choices' => $this->buildLocaleChoices(true), @@ -73,14 +81,26 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'constraints' => [ new Assert\NotBlank(), ], + 'row_attr' => [ + 'class' => 'form-floating', + ], + 'attr' => [ + 'placeholder' => '' // to show floating label even when empty + ] ]) ->add('translation_plural', TextType::class, [ 'label' => 'settings.behavior.data_source_synonyms.row_type.form.translation_plural', + 'row_attr' => [ + 'class' => 'form-floating', + ], 'required' => true, 'empty_data' => '', 'constraints' => [ new Assert\NotBlank(), ], + 'attr' => [ + 'placeholder' => '' // to show floating label even when empty + ] ]); } diff --git a/templates/form/datasource_synonyms_collection.html.twig b/templates/form/datasource_synonyms_collection.html.twig index 8b759791d..0c81a54eb 100644 --- a/templates/form/datasource_synonyms_collection.html.twig +++ b/templates/form/datasource_synonyms_collection.html.twig @@ -1,3 +1,20 @@ +{% macro renderForm(child) %} +
    + {% form_theme child "form/vertical_bootstrap_layout.html.twig" %} +
    +
    {{ form_row(child.dataSource) }}
    +
    {{ form_row(child.locale) }}
    +
    {{ form_row(child.translation_singular) }}
    +
    {{ form_row(child.translation_plural) }}
    +
    + +
    +
    +
    +{% endmacro %} + {% block datasource_synonyms_collection_widget %} {% set _attrs = attr|default({}) %} {% set _attrs = _attrs|merge({ @@ -7,14 +24,7 @@ {% set has_proto = prototype is defined %} {% if has_proto %} {% set __proto %} -
    - {{ form_widget(prototype) }} -
    - -
    -
    + {{- _self.renderForm(prototype) -}} {% endset %} {% set _proto_html = __proto|e('html_attr') %} {% set _proto_name = form.vars.prototype_name|default('__name__') %} @@ -29,20 +39,13 @@ }) }} {{ block('widget_container_attributes')|raw }}{% for k,v in _attrs %} {{ k }}="{{ v }}"{% endfor %} > -
    - {% for child in form %} -
    - {{ form_widget(child) }} -
    - -
    -
    - {% endfor %} -
    - +
    + {% for child in form %} + {{ _self.renderForm(child) }} + {% endfor %} +
    + {% endblock %} diff --git a/templates/form/vertical_bootstrap_layout.html.twig b/templates/form/vertical_bootstrap_layout.html.twig new file mode 100644 index 000000000..5f41d82ed --- /dev/null +++ b/templates/form/vertical_bootstrap_layout.html.twig @@ -0,0 +1,26 @@ +{% extends 'bootstrap_5_layout.html.twig' %} + + +{%- block choice_widget_collapsed -%} + {# Only add the BS5 form-select class if we dont use bootstrap-selectpicker #} + {# {% if attr["data-controller"] is defined and attr["data-controller"] not in ["elements--selectpicker"] %} + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-select')|trim}) -%} + {% else %} + {# If it is an selectpicker add form-control class to fill whole width + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-control')|trim}) -%} + {% endif %} + #} + + {%- set attr = attr|merge({class: (attr.class|default('') ~ ' form-select')|trim}) -%} + + {# If no data-controller was explictly defined add data-controller=elements--select #} + {% if attr["data-controller"] is not defined %} + {%- set attr = attr|merge({"data-controller": "elements--select"}) -%} + + {% if attr["data-empty-message"] is not defined %} + {%- set attr = attr|merge({"data-empty-message": ("selectpicker.nothing_selected"|trans)}) -%} + {% endif %} + {% endif %} + + {{- block("choice_widget_collapsed", "bootstrap_base_layout.html.twig") -}} +{%- endblock choice_widget_collapsed -%} From 2c55669ea013a44ca0eb76914724c43db8ae175b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 9 Nov 2025 23:55:56 +0100 Subject: [PATCH 11/27] Made synonym form even more space saving --- src/Form/Type/DataSourceSynonymRowType.php | 32 +++++++------------ .../datasource_synonyms_collection.html.twig | 10 +++++- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/Form/Type/DataSourceSynonymRowType.php b/src/Form/Type/DataSourceSynonymRowType.php index 445d5c4e0..abef12dea 100644 --- a/src/Form/Type/DataSourceSynonymRowType.php +++ b/src/Form/Type/DataSourceSynonymRowType.php @@ -52,20 +52,16 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'label' => false, 'choices' => $this->buildDataSourceChoices($options['data_sources']), 'required' => true, - 'row_attr' => [ - 'class' => 'form-floating', - ], 'constraints' => [ new Assert\NotBlank(), ], + 'row_attr' => ['class' => 'mb-0'], + 'attr' => ['class' => 'form-select-sm'] ]) ->add('locale', LocaleType::class, [ //'label' => 'settings.behavior.data_source_synonyms.row_type.form.locale', 'label' => false, 'required' => true, - 'row_attr' => [ - 'class' => 'form-floating', - ], // Restrict to languages configured in the language menu: disable ChoiceLoader and provide explicit choices 'choice_loader' => null, 'choices' => $this->buildLocaleChoices(true), @@ -73,34 +69,30 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'constraints' => [ new Assert\NotBlank(), ], + 'row_attr' => ['class' => 'mb-0'], + 'attr' => ['class' => 'form-select-sm'] ]) ->add('translation_singular', TextType::class, [ - 'label' => 'settings.behavior.data_source_synonyms.row_type.form.translation_singular', + //'label' => 'settings.behavior.data_source_synonyms.row_type.form.translation_singular', + 'label' => false, 'required' => true, 'empty_data' => '', 'constraints' => [ new Assert\NotBlank(), ], - 'row_attr' => [ - 'class' => 'form-floating', - ], - 'attr' => [ - 'placeholder' => '' // to show floating label even when empty - ] + 'row_attr' => ['class' => 'mb-0'], + 'attr' => ['class' => 'form-select-sm'] ]) ->add('translation_plural', TextType::class, [ - 'label' => 'settings.behavior.data_source_synonyms.row_type.form.translation_plural', - 'row_attr' => [ - 'class' => 'form-floating', - ], + //'label' => 'settings.behavior.data_source_synonyms.row_type.form.translation_plural', + 'label' => false, 'required' => true, 'empty_data' => '', 'constraints' => [ new Assert\NotBlank(), ], - 'attr' => [ - 'placeholder' => '' // to show floating label even when empty - ] + 'row_attr' => ['class' => 'mb-0'], + 'attr' => ['class' => 'form-select-sm'] ]); } diff --git a/templates/form/datasource_synonyms_collection.html.twig b/templates/form/datasource_synonyms_collection.html.twig index 0c81a54eb..27c4a0513 100644 --- a/templates/form/datasource_synonyms_collection.html.twig +++ b/templates/form/datasource_synonyms_collection.html.twig @@ -7,7 +7,7 @@
    {{ form_row(child.translation_singular) }}
    {{ form_row(child.translation_plural) }}
    -
    @@ -39,6 +39,14 @@ }) }} {{ block('widget_container_attributes')|raw }}{% for k,v in _attrs %} {{ k }}="{{ v }}"{% endfor %} > +
    +
    {% trans %}settings.behavior.data_source_synonyms.row_type.form.datasource{% endtrans %}
    +
    {% trans %}settings.behavior.data_source_synonyms.row_type.form.locale{% endtrans %}
    +
    {% trans %}settings.behavior.data_source_synonyms.row_type.form.translation_singular{% endtrans %}
    +
    {% trans %}settings.behavior.data_source_synonyms.row_type.form.translation_plural{% endtrans %}
    +
    +
    +
    {% for child in form %} {{ _self.renderForm(child) }} From 96418db4e9fcfe15fcc10cd716163bc3a7ff713a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 10 Nov 2025 00:07:44 +0100 Subject: [PATCH 12/27] Allow to define overrides for any element label there is --- src/Form/Type/DataSourceSynonymRowType.php | 38 +++++++++---------- .../Type/DataSourceSynonymsCollectionType.php | 24 ++++-------- src/Services/ElementTypes.php | 1 + .../DataSourceSynonymsSettings.php | 8 ---- 4 files changed, 26 insertions(+), 45 deletions(-) diff --git a/src/Form/Type/DataSourceSynonymRowType.php b/src/Form/Type/DataSourceSynonymRowType.php index abef12dea..c35ad81f2 100644 --- a/src/Form/Type/DataSourceSynonymRowType.php +++ b/src/Form/Type/DataSourceSynonymRowType.php @@ -22,10 +22,12 @@ namespace App\Form\Type; +use App\Services\ElementTypes; use App\Settings\SystemSettings\LocalizationSettings; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; @@ -38,6 +40,16 @@ */ class DataSourceSynonymRowType extends AbstractType { + + private const PREFFERED_TYPES = [ + ElementTypes::CATEGORY, + ElementTypes::STORAGE_LOCATION, + ElementTypes::FOOTPRINT, + ElementTypes::MANUFACTURER, + ElementTypes::SUPPLIER, + ElementTypes::PROJECT, + ]; + public function __construct( private readonly LocalizationSettings $localizationSettings, #[Autowire(param: 'partdb.locale_menu')] private readonly array $preferredLanguagesParam, @@ -47,19 +59,19 @@ public function __construct( public function buildForm(FormBuilderInterface $builder, array $options): void { $builder - ->add('dataSource', ChoiceType::class, [ - //'label' => 'settings.behavior.data_source_synonyms.row_type.form.datasource', + ->add('dataSource', EnumType::class, [ + 'class' => ElementTypes::class, 'label' => false, - 'choices' => $this->buildDataSourceChoices($options['data_sources']), + //'choices' => $this->buildDataSourceChoices($options['data_sources']), 'required' => true, 'constraints' => [ new Assert\NotBlank(), ], 'row_attr' => ['class' => 'mb-0'], - 'attr' => ['class' => 'form-select-sm'] + 'attr' => ['class' => 'form-select-sm'], + 'preferred_choices' => self::PREFFERED_TYPES ]) ->add('locale', LocaleType::class, [ - //'label' => 'settings.behavior.data_source_synonyms.row_type.form.locale', 'label' => false, 'required' => true, // Restrict to languages configured in the language menu: disable ChoiceLoader and provide explicit choices @@ -73,7 +85,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'attr' => ['class' => 'form-select-sm'] ]) ->add('translation_singular', TextType::class, [ - //'label' => 'settings.behavior.data_source_synonyms.row_type.form.translation_singular', 'label' => false, 'required' => true, 'empty_data' => '', @@ -84,7 +95,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'attr' => ['class' => 'form-select-sm'] ]) ->add('translation_plural', TextType::class, [ - //'label' => 'settings.behavior.data_source_synonyms.row_type.form.translation_plural', 'label' => false, 'required' => true, 'empty_data' => '', @@ -96,14 +106,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ]); } - private function buildDataSourceChoices(array $dataSources): array - { - $choices = []; - foreach ($dataSources as $key => $label) { - $choices[(string)$label] = (string)$key; - } - return $choices; - } /** * Returns only locales configured in the language menu (settings) or falls back to the parameter. @@ -140,10 +142,4 @@ private function getPossibleLocales(): array { return array_values($this->preferredLanguagesParam); } - - public function configureOptions(OptionsResolver $resolver): void - { - $resolver->setRequired('data_sources'); - $resolver->setAllowedTypes('data_sources', 'array'); - } } diff --git a/src/Form/Type/DataSourceSynonymsCollectionType.php b/src/Form/Type/DataSourceSynonymsCollectionType.php index 5507d5077..c8c44a504 100644 --- a/src/Form/Type/DataSourceSynonymsCollectionType.php +++ b/src/Form/Type/DataSourceSynonymsCollectionType.php @@ -4,6 +4,7 @@ namespace App\Form\Type; +use App\Services\ElementTypes; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\Form\Extension\Core\Type\CollectionType; @@ -12,7 +13,6 @@ use Symfony\Component\Form\FormEvent; use Symfony\Component\Form\FormEvents; use Symfony\Component\Intl\Locales; -use Symfony\Component\OptionsResolver\Options; use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Contracts\Translation\TranslatorInterface; @@ -44,7 +44,8 @@ private function flattenStructure(array $modelValue): array continue; } $out[] = [ - 'dataSource' => $dataSource, + //Convert string to enum value + 'dataSource' => ElementTypes::from($dataSource), 'locale' => $locale, 'translation_singular' => $translations['singular'] ?? '', 'translation_plural' => $translations['plural'] ?? '', @@ -78,13 +79,13 @@ function (array $viewValue) { $translation_singular = $row['translation_singular'] ?? null; $translation_plural = $row['translation_plural'] ?? null; - if (!is_string($dataSource) || $dataSource === '' - || !is_string($locale) || $locale === '' + if ($dataSource === null || + !is_string($locale) || $locale === '' ) { continue; } - $out[$dataSource][$locale] = [ + $out[$dataSource->value][$locale] = [ 'singular' => is_string($translation_singular) ? $translation_singular : '', 'plural' => is_string($translation_plural) ? $translation_plural : '', ]; @@ -153,8 +154,8 @@ function (array $viewValue) { $sortable = $rows; usort($sortable, static function ($a, $b) { - $aDs = (string)($a['dataSource'] ?? ''); - $bDs = (string)($b['dataSource'] ?? ''); + $aDs = $a['dataSource']?->value ?? ''; + $bDs = $b['dataSource']?->value ?? ''; $cmpDs = strcasecmp($aDs, $bDs); if ($cmpDs !== 0) { @@ -176,8 +177,6 @@ function (array $viewValue) { public function configureOptions(OptionsResolver $resolver): void { - $resolver->setRequired(['data_sources']); - $resolver->setAllowedTypes('data_sources', 'array'); // Defaults for the collection and entry type $resolver->setDefaults([ @@ -189,14 +188,7 @@ public function configureOptions(OptionsResolver $resolver): void 'prototype' => true, 'empty_data' => [], 'entry_options' => ['label' => false], - 'error_translation_domain' => 'validators', ]); - - // Pass data_sources automatically to each row (DataSourceSynonymRowType) - $resolver->setNormalizer('entry_options', function (Options $options, $value) { - $value = is_array($value) ? $value : []; - return $value + ['data_sources' => $options['data_sources']]; - }); } public function getParent(): ?string diff --git a/src/Services/ElementTypes.php b/src/Services/ElementTypes.php index 0ac922263..6ce8f8514 100644 --- a/src/Services/ElementTypes.php +++ b/src/Services/ElementTypes.php @@ -225,4 +225,5 @@ public static function fromClass(string|object $class): self throw new EntityNotSupportedException(sprintf('No localized label for the element with type %s was found!', $className)); } + } diff --git a/src/Settings/SystemSettings/DataSourceSynonymsSettings.php b/src/Settings/SystemSettings/DataSourceSynonymsSettings.php index ab0cb0532..9ebe557d1 100644 --- a/src/Settings/SystemSettings/DataSourceSynonymsSettings.php +++ b/src/Settings/SystemSettings/DataSourceSynonymsSettings.php @@ -48,14 +48,6 @@ class DataSourceSynonymsSettings formType: DataSourceSynonymsCollectionType::class, formOptions: [ 'required' => false, - 'data_sources' => [ - 'category' => new TM("settings.behavior.data_source_synonyms.category"), - 'storagelocation' => new TM("settings.behavior.data_source_synonyms.storagelocation"), - 'footprint' => new TM("settings.behavior.data_source_synonyms.footprint"), - 'manufacturer' => new TM("settings.behavior.data_source_synonyms.manufacturer"), - 'supplier' => new TM("settings.behavior.data_source_synonyms.supplier"), - 'project' => new TM("settings.behavior.data_source_synonyms.project"), - ], ], )] #[Assert\Type('array')] From e95197b0699c0529b9bf2653f71fb55a96a47e39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 10 Nov 2025 00:23:00 +0100 Subject: [PATCH 13/27] Use defined synonyms in ElementTypeNameGenerator --- src/Services/ElementTypeNameGenerator.php | 43 ++++++++++++++++--- .../DataSourceSynonymsSettings.php | 31 ++++++++++++- 2 files changed, 68 insertions(+), 6 deletions(-) diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index d4b039c8c..2d8bb1069 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -34,6 +34,7 @@ use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Exceptions\EntityNotSupportedException; +use App\Settings\SystemSettings\DataSourceSynonymsSettings; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -42,7 +43,11 @@ final readonly class ElementTypeNameGenerator { - public function __construct(private TranslatorInterface $translator, private EntityURLGenerator $entityURLGenerator) + public function __construct( + private TranslatorInterface $translator, + private EntityURLGenerator $entityURLGenerator, + private DataSourceSynonymsSettings $synonymsSettings, + ) { } @@ -64,6 +69,32 @@ public function getLocalizedTypeLabel(object|string $entity): string return $this->typeLabel($entity); } + private function resolveSynonymLabel(ElementTypes $type, ?string $locale, bool $plural): ?string + { + $locale ??= $this->translator->getLocale(); + + if ($this->synonymsSettings->isSynonymDefinedForType($type)) { + if ($plural) { + $syn = $this->synonymsSettings->getSingularSynonymForType($type, $locale); + } else { + $syn = $this->synonymsSettings->getPluralSynonymForType($type, $locale); + } + + if ($syn === null) { + //Try to fall back to english + if ($plural) { + $syn = $this->synonymsSettings->getSingularSynonymForType($type, 'en'); + } else { + $syn = $this->synonymsSettings->getPluralSynonymForType($type, 'en'); + } + } + + return $syn; + } + + return null; + } + /** * Gets a localized label for the type of the entity. If user defined synonyms are defined, * these are used instead of the default labels. @@ -75,7 +106,8 @@ public function typeLabel(object|string $entity, ?string $locale = null): string { $type = ElementTypes::fromValue($entity); - return $this->translator->trans($type->getDefaultLabelKey(), locale: $locale); + return $this->resolveSynonymLabel($type, $locale, false) + ?? $this->translator->trans($type->getDefaultLabelKey(), locale: $locale); } /** @@ -88,7 +120,8 @@ public function typeLabelPlural(object|string $entity, ?string $locale = null): { $type = ElementTypes::fromValue($entity); - return $this->translator->trans($type->getDefaultPluralLabelKey(), locale: $locale); + return $this->resolveSynonymLabel($type, $locale, true) + ?? $this->translator->trans($type->getDefaultPluralLabelKey(), locale: $locale); } @@ -137,7 +170,7 @@ public function formatLabelHTMLForEntity(AbstractDBElement $entity, bool $includ } else { //Target does not have a name $tmp = sprintf( '%s: %s', - $this->getLocalizedTypeLabel($entity), + $this->typeLabel($entity), $entity->getID() ); } @@ -181,7 +214,7 @@ public function formatElementDeletedHTML(string $class, int $id): string { return sprintf( '%s: %s [%s]', - $this->getLocalizedTypeLabel($class), + $this->typeLabel($class), $id, $this->translator->trans('log.target_deleted') ); diff --git a/src/Settings/SystemSettings/DataSourceSynonymsSettings.php b/src/Settings/SystemSettings/DataSourceSynonymsSettings.php index 9ebe557d1..03cb75e35 100644 --- a/src/Settings/SystemSettings/DataSourceSynonymsSettings.php +++ b/src/Settings/SystemSettings/DataSourceSynonymsSettings.php @@ -66,8 +66,37 @@ class DataSourceSynonymsSettings */ public array $customTypeLabels = []; - public function isCustomLabelDefinedForType(ElementTypes $type): bool + /** + * Checks if there is any synonym defined for the given type (no matter which language). + * @param ElementTypes $type + * @return bool + */ + public function isSynonymDefinedForType(ElementTypes $type): bool { return isset($this->customTypeLabels[$type->value]); } + + /** + * Returns the singular synonym for the given type and locale, or null if none is defined. + * @param ElementTypes $type + * @param string $locale + * @return string|null + */ + public function getSingularSynonymForType(ElementTypes $type, string $locale): ?string + { + return $this->customTypeLabels[$type->value][$locale]['singular'] ?? null; + } + + /** + * Returns the plural synonym for the given type and locale, or null if none is defined. + * @param ElementTypes $type + * @param string|null $locale + * @return string|null + */ + public function getPluralSynonymForType(ElementTypes $type, ?string $locale): ?string + { + return $this->customTypeLabels[$type->value][$locale]['plural'] + ?? $this->customTypeLabels[$type->value][$locale]['singular'] + ?? null; + } } From c372109a2ffd9a8d27574892457a0d165ba841b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Mon, 10 Nov 2025 00:48:25 +0100 Subject: [PATCH 14/27] Use ElementTypeNameGenerator where possible --- src/Services/ElementTypeNameGenerator.php | 8 ++-- src/Services/Trees/ToolsTreeBuilder.php | 49 ++++++---------------- src/Services/Trees/TreeViewGenerator.php | 16 ++----- src/Twig/EntityExtension.php | 2 + templates/components/tree_macros.html.twig | 22 ++++++---- 5 files changed, 37 insertions(+), 60 deletions(-) diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index 2d8bb1069..ed546fb63 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -75,17 +75,17 @@ private function resolveSynonymLabel(ElementTypes $type, ?string $locale, bool $ if ($this->synonymsSettings->isSynonymDefinedForType($type)) { if ($plural) { - $syn = $this->synonymsSettings->getSingularSynonymForType($type, $locale); - } else { $syn = $this->synonymsSettings->getPluralSynonymForType($type, $locale); + } else { + $syn = $this->synonymsSettings->getSingularSynonymForType($type, $locale); } if ($syn === null) { //Try to fall back to english if ($plural) { - $syn = $this->synonymsSettings->getSingularSynonymForType($type, 'en'); - } else { $syn = $this->synonymsSettings->getPluralSynonymForType($type, 'en'); + } else { + $syn = $this->synonymsSettings->getSingularSynonymForType($type, 'en'); } } diff --git a/src/Services/Trees/ToolsTreeBuilder.php b/src/Services/Trees/ToolsTreeBuilder.php index fa22bd0a1..37a09b09b 100644 --- a/src/Services/Trees/ToolsTreeBuilder.php +++ b/src/Services/Trees/ToolsTreeBuilder.php @@ -38,7 +38,7 @@ use App\Entity\UserSystem\User; use App\Helpers\Trees\TreeViewNode; use App\Services\Cache\UserCacheKeyGenerator; -use App\Services\Misc\DataSourceSynonymResolver; +use App\Services\ElementTypeNameGenerator; use Symfony\Bundle\SecurityBundle\Security; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Contracts\Cache\ItemInterface; @@ -57,7 +57,7 @@ public function __construct( protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security, - protected readonly DataSourceSynonymResolver $synonymResolver, + private readonly ElementTypeNameGenerator $elementTypeNameGenerator, ) { } @@ -167,90 +167,67 @@ protected function getEditNodes(): array if ($this->security->isGranted('read', new AttachmentType())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.attachment_types'), + $this->elementTypeNameGenerator->typeLabelPlural(AttachmentType::class), $this->urlGenerator->generate('attachment_type_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-file-alt'); } if ($this->security->isGranted('read', new Category())) { $nodes[] = (new TreeViewNode( - $this->synonymResolver->displayNamePlural( - 'category', - 'tree.tools.edit.categories', - $this->translator->getLocale() - ), + $this->elementTypeNameGenerator->typeLabelPlural(Category::class), $this->urlGenerator->generate('category_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-tags'); } if ($this->security->isGranted('read', new Project())) { $nodes[] = (new TreeViewNode( - $this->synonymResolver->displayNamePlural( - 'project', - 'tree.tools.edit.projects', - $this->translator->getLocale()), + $this->elementTypeNameGenerator->typeLabelPlural(Project::class), $this->urlGenerator->generate('project_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); } if ($this->security->isGranted('read', new Supplier())) { $nodes[] = (new TreeViewNode( - $this->synonymResolver->displayNamePlural( - 'supplier', - 'tree.tools.edit.suppliers', - $this->translator->getLocale() - ), + $this->elementTypeNameGenerator->typeLabelPlural(Supplier::class), $this->urlGenerator->generate('supplier_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-truck'); } if ($this->security->isGranted('read', new Manufacturer())) { $nodes[] = (new TreeViewNode( - $this->synonymResolver->displayNamePlural( - 'manufacturer', - 'tree.tools.edit.manufacturer', - $this->translator->getLocale() - ), + $this->elementTypeNameGenerator->typeLabelPlural(Manufacturer::class), $this->urlGenerator->generate('manufacturer_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-industry'); } if ($this->security->isGranted('read', new StorageLocation())) { $nodes[] = (new TreeViewNode( - $this->synonymResolver->displayNamePlural( - 'storagelocation', - 'tree.tools.edit.storelocation', - $this->translator->getLocale() - ), + $this->elementTypeNameGenerator->typeLabelPlural(StorageLocation::class), $this->urlGenerator->generate('store_location_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-cube'); } if ($this->security->isGranted('read', new Footprint())) { $nodes[] = (new TreeViewNode( - $this->synonymResolver->displayNamePlural( - 'footprint', - 'tree.tools.edit.footprint', - $this->translator->getLocale() - ), + $this->elementTypeNameGenerator->typeLabelPlural(Footprint::class), $this->urlGenerator->generate('footprint_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-microchip'); } if ($this->security->isGranted('read', new Currency())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.currency'), + $this->elementTypeNameGenerator->typeLabelPlural(Currency::class), $this->urlGenerator->generate('currency_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-coins'); } if ($this->security->isGranted('read', new MeasurementUnit())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.measurement_unit'), + $this->elementTypeNameGenerator->typeLabelPlural(MeasurementUnit::class), $this->urlGenerator->generate('measurement_unit_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-balance-scale'); } if ($this->security->isGranted('read', new LabelProfile())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.label_profile'), + $this->elementTypeNameGenerator->typeLabelPlural(LabelProfile::class), $this->urlGenerator->generate('label_profile_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-qrcode'); } if ($this->security->isGranted('read', new PartCustomState())) { $nodes[] = (new TreeViewNode( - $this->translator->trans('tree.tools.edit.part_custom_state'), + $this->elementTypeNameGenerator->typeLabelPlural(PartCustomState::class), $this->urlGenerator->generate('part_custom_state_new') ))->setIcon('fa-fw fa-treeview fa-solid fa-tools'); } diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index 1cd1730f2..f1d37e6c5 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -36,6 +36,7 @@ use App\Repository\NamedDBElementRepository; use App\Services\Cache\ElementCacheTagGenerator; use App\Services\Cache\UserCacheKeyGenerator; +use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; use App\Services\Misc\DataSourceSynonymResolver; use App\Settings\BehaviorSettings\SidebarSettings; @@ -67,7 +68,8 @@ public function __construct( protected TranslatorInterface $translator, private readonly UrlGeneratorInterface $router, private readonly SidebarSettings $sidebarSettings, - protected readonly DataSourceSynonymResolver $synonymResolver + protected readonly DataSourceSynonymResolver $synonymResolver, + private readonly ElementTypeNameGenerator $elementTypeNameGenerator ) { $this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled; $this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded; @@ -213,17 +215,7 @@ protected function entityClassToRootNodeHref(string $class): ?string protected function entityClassToRootNodeString(string $class): string { - $locale = $this->translator->getLocale(); - - return match ($class) { - Category::class => $this->synonymResolver->displayNamePlural('category', $locale), - StorageLocation::class => $this->synonymResolver->displayNamePlural('storelocation', $locale), - Footprint::class => $this->synonymResolver->displayNamePlural('footprint', $locale), - Manufacturer::class => $this->synonymResolver->displayNamePlural('manufacturer', $locale), - Supplier::class => $this->synonymResolver->displayNamePlural('supplier', $locale), - Project::class => $this->synonymResolver->displayNamePlural('project', $locale), - default => $this->translator->trans('tree.root_node.text'), - }; + return $this->elementTypeNameGenerator->typeLabelPlural($class); } protected function entityClassToRootNodeIcon(string $class): ?string diff --git a/src/Twig/EntityExtension.php b/src/Twig/EntityExtension.php index b5e5c3ca2..427a39b57 100644 --- a/src/Twig/EntityExtension.php +++ b/src/Twig/EntityExtension.php @@ -76,6 +76,8 @@ public function getFunctions(): array /* Gets a human readable label for the type of the given entity */ new TwigFunction('entity_type_label', fn(object|string $entity): string => $this->nameGenerator->getLocalizedTypeLabel($entity)), + new TwigFunction('type_label', fn(object|string $entity): string => $this->nameGenerator->typeLabel($entity)), + new TwigFunction('type_label_p', fn(object|string $entity): string => $this->nameGenerator->typeLabelPlural($entity)), ]; } diff --git a/templates/components/tree_macros.html.twig b/templates/components/tree_macros.html.twig index 673b81080..aaa871eaa 100644 --- a/templates/components/tree_macros.html.twig +++ b/templates/components/tree_macros.html.twig @@ -3,12 +3,12 @@ {# Format is [mode, route, label, show_condition] #} {% set data_sources = [ - ['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read'), 'category'], - ['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read'), 'storagelocation'], - ['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read'), 'footprint'], - ['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read'), 'manufacturer'], - ['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read'), 'supplier'], - ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read'), 'project'], + ['categories', path('tree_category_root'), '@category@@', is_granted('@categories.read') and is_granted('@parts.read')], + ['locations', path('tree_location_root'), '@storage_location@@', is_granted('@storelocations.read') and is_granted('@parts.read'), ], + ['footprints', path('tree_footprint_root'), '@footprint@@', is_granted('@footprints.read') and is_granted('@parts.read')], + ['manufacturers', path('tree_manufacturer_root'), '@manufacturer@@', is_granted('@manufacturers.read') and is_granted('@parts.read'), 'manufacturer'], + ['suppliers', path('tree_supplier_root'), '@supplier@@', is_granted('@suppliers.read') and is_granted('@parts.read'), 'supplier'], + ['projects', path('tree_device_root'), '@project@@', is_granted('@projects.read'), 'project'], ['tools', path('tree_tools'), 'tools.label', true, 'tool'], ] %} @@ -21,12 +21,18 @@ {% for source in data_sources %} {% if source[3] %} {# show_condition #}
  • + {% if source[2] starts with '@' %} + {% set label = type_label_p(source[2]|replace({'@': ''})) %} + {% else %} + {% set label = source[2]|trans %} + {% endif %} + + >{{ label }}
  • {% endif %} {% endfor %} From d0a65cbb941a14d42f424abdabf0921aa9dec245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 11 Nov 2025 22:41:23 +0100 Subject: [PATCH 15/27] Register synonyms for element types as global translation parameters --- src/Controller/SettingsController.php | 2 +- ...egisterSynonymsAsTranslationParameters.php | 93 +++++++++++++++++++ 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 src/EventListener/RegisterSynonymsAsTranslationParameters.php diff --git a/src/Controller/SettingsController.php b/src/Controller/SettingsController.php index f412e469e..15c945f6a 100644 --- a/src/Controller/SettingsController.php +++ b/src/Controller/SettingsController.php @@ -64,7 +64,7 @@ public function systemSettings(Request $request, TagAwareCacheInterface $cache): $this->settingsManager->save($settings); //It might be possible, that the tree settings have changed, so clear the cache - $cache->invalidateTags(['tree_tools', 'tree_treeview', 'sidebar_tree_update']); + $cache->invalidateTags(['tree_tools', 'tree_treeview', 'sidebar_tree_update', 'synonyms']); $this->addFlash('success', t('settings.flash.saved')); } diff --git a/src/EventListener/RegisterSynonymsAsTranslationParameters.php b/src/EventListener/RegisterSynonymsAsTranslationParameters.php new file mode 100644 index 000000000..e55a0f9a6 --- /dev/null +++ b/src/EventListener/RegisterSynonymsAsTranslationParameters.php @@ -0,0 +1,93 @@ +. + */ + +declare(strict_types=1); + + +namespace App\EventListener; + +use App\Services\ElementTypeNameGenerator; +use App\Services\ElementTypes; +use Symfony\Component\DependencyInjection\Attribute\Autowire; +use Symfony\Component\EventDispatcher\Attribute\AsEventListener; +use Symfony\Component\HttpKernel\Event\RequestEvent; +use Symfony\Component\Translation\Translator; +use Symfony\Contracts\Cache\CacheInterface; +use Symfony\Contracts\Cache\ItemInterface; +use Symfony\Contracts\Cache\TagAwareCacheInterface; +use Symfony\Contracts\Translation\TranslatorInterface; + +#[AsEventListener] +readonly class RegisterSynonymsAsTranslationParameters +{ + private Translator $translator; + + public function __construct( + #[Autowire(service: 'translator.default')] TranslatorInterface $translator, + private TagAwareCacheInterface $cache, + private ElementTypeNameGenerator $typeNameGenerator) + { + if (!$translator instanceof Translator) { + throw new \RuntimeException('Translator must be an instance of Symfony\Component\Translation\Translator or this listener cannot be used.'); + } + $this->translator = $translator; + } + + public function getSynonymPlaceholders(): array + { + return $this->cache->get('partdb_synonym_placeholders', function (ItemInterface $item) { + $item->tag('synonyms'); + + + $placeholders = []; + + //Generate a placeholder for each element type + foreach (ElementTypes::cases() as $elementType) { + //We have a placeholder for singular + $placeholders['{' . $elementType->value . '}'] = $this->typeNameGenerator->typeLabel($elementType); + //We have a placeholder for plural + $placeholders['{{' . $elementType->value . '}}'] = $this->typeNameGenerator->typeLabelPlural($elementType); + + //And we have lowercase versions for both + $placeholders['[' . $elementType->value . ']'] = mb_strtolower($this->typeNameGenerator->typeLabel($elementType)); + $placeholders['[' . $elementType->value . ']'] = mb_strtolower($this->typeNameGenerator->typeLabelPlural($elementType)); + } + + return $placeholders; + }); + } + + public function __invoke(RequestEvent $event): void + { + //If we already added the parameters, skip adding them again + if (isset($this->translator->getGlobalParameters()['@@partdb_synonyms_registered@@'])) { + return; + } + + //Register all placeholders for synonyms + $placeholders = $this->getSynonymPlaceholders(); + foreach ($placeholders as $key => $value) { + $this->translator->addGlobalParameter($key, $value); + } + + //Register the marker parameter to avoid double registration + $this->translator->addGlobalParameter('@@partdb_synonyms_registered@@', 'registered'); + } +} From cb2710172935b965bcd1e99cf91f2c23bdabb873 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 11 Nov 2025 22:44:46 +0100 Subject: [PATCH 16/27] Revert changes done to permission layout --- templates/form/permission_layout.html.twig | 26 ++-------------------- 1 file changed, 2 insertions(+), 24 deletions(-) diff --git a/templates/form/permission_layout.html.twig b/templates/form/permission_layout.html.twig index aefd9edd9..747208dd1 100644 --- a/templates/form/permission_layout.html.twig +++ b/templates/form/permission_layout.html.twig @@ -6,34 +6,12 @@
    {% else %} - def{{ form.vars.label | trans }} + {{ form.vars.label | trans }} {% endif %} From f61ecc9738a925f0be3b0ee4ab0d0de1358cd275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 11 Nov 2025 23:06:28 +0100 Subject: [PATCH 17/27] Use new synonym system for admin page titles --- .../admin/attachment_type_admin.html.twig | 2 +- templates/admin/category_admin.html.twig | 3 +- templates/admin/currency_admin.html.twig | 4 +- templates/admin/footprint_admin.html.twig | 3 +- templates/admin/group_admin.html.twig | 4 +- templates/admin/label_profile_admin.html.twig | 4 +- templates/admin/manufacturer_admin.html.twig | 3 +- .../admin/measurement_unit_admin.html.twig | 2 +- .../admin/part_custom_state_admin.html.twig | 2 +- templates/admin/project_admin.html.twig | 3 +- templates/admin/storelocation_admin.html.twig | 3 +- templates/admin/supplier_admin.html.twig | 3 +- templates/admin/user_admin.html.twig | 4 +- translations/messages.en.xlf | 181 +++++++----------- 14 files changed, 83 insertions(+), 138 deletions(-) diff --git a/templates/admin/attachment_type_admin.html.twig b/templates/admin/attachment_type_admin.html.twig index 06a8c09df..87a053afb 100644 --- a/templates/admin/attachment_type_admin.html.twig +++ b/templates/admin/attachment_type_admin.html.twig @@ -15,4 +15,4 @@ {% block new_title %} {% trans %}attachment_type.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/category_admin.html.twig b/templates/admin/category_admin.html.twig index 7478de11d..3ddc14726 100644 --- a/templates/admin/category_admin.html.twig +++ b/templates/admin/category_admin.html.twig @@ -1,8 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - - {{ data_source_name_with_hint('category', 'category.label') }} + {{ type_label_p(entity) }} {% endblock %} {% block additional_pills %} diff --git a/templates/admin/currency_admin.html.twig b/templates/admin/currency_admin.html.twig index fbd3822cd..a5d599707 100644 --- a/templates/admin/currency_admin.html.twig +++ b/templates/admin/currency_admin.html.twig @@ -3,7 +3,7 @@ {% import "vars.macro.twig" as vars %} {% block card_title %} - {% trans %}currency.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block additional_controls %} @@ -41,4 +41,4 @@ {% block new_title %} {% trans %}currency.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/footprint_admin.html.twig b/templates/admin/footprint_admin.html.twig index e76fa52f6..1ed39e9f4 100644 --- a/templates/admin/footprint_admin.html.twig +++ b/templates/admin/footprint_admin.html.twig @@ -1,8 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - - {{ data_source_name_with_hint('footprint', 'footprint.labelp') }} + {{ type_label_p(entity) }} {% endblock %} {% block master_picture_block %} diff --git a/templates/admin/group_admin.html.twig b/templates/admin/group_admin.html.twig index 91975524a..831c08d58 100644 --- a/templates/admin/group_admin.html.twig +++ b/templates/admin/group_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}group.edit.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} @@ -27,4 +27,4 @@ {% block new_title %} {% trans %}group.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/label_profile_admin.html.twig b/templates/admin/label_profile_admin.html.twig index 10c2320f4..8702b18a6 100644 --- a/templates/admin/label_profile_admin.html.twig +++ b/templates/admin/label_profile_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}label_profile.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block additional_pills %} @@ -58,4 +58,4 @@ {% block new_title %} {% trans %}label_profile.new{% endtrans %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/templates/admin/manufacturer_admin.html.twig b/templates/admin/manufacturer_admin.html.twig index 3289fb08d..4f8f1c2b0 100644 --- a/templates/admin/manufacturer_admin.html.twig +++ b/templates/admin/manufacturer_admin.html.twig @@ -1,8 +1,7 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - - {{ data_source_name_with_hint('manufacturer', 'manufacturer.caption') }} + {{ type_label_p(entity) }} {% endblock %} {% block edit_title %} diff --git a/templates/admin/measurement_unit_admin.html.twig b/templates/admin/measurement_unit_admin.html.twig index 317485091..14df73646 100644 --- a/templates/admin/measurement_unit_admin.html.twig +++ b/templates/admin/measurement_unit_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}measurement_unit.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block edit_title %} diff --git a/templates/admin/part_custom_state_admin.html.twig b/templates/admin/part_custom_state_admin.html.twig index 004ceb657..9d8576468 100644 --- a/templates/admin/part_custom_state_admin.html.twig +++ b/templates/admin/part_custom_state_admin.html.twig @@ -1,7 +1,7 @@ {% extends "admin/base_admin.html.twig" %} {% block card_title %} - {% trans %}part_custom_state.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block edit_title %} diff --git a/templates/admin/project_admin.html.twig b/templates/admin/project_admin.html.twig index 044f50a5b..d199b63ce 100644 --- a/templates/admin/project_admin.html.twig +++ b/templates/admin/project_admin.html.twig @@ -3,8 +3,7 @@ {# @var entity App\Entity\ProjectSystem\Project #} {% block card_title %} - - {{ data_source_name_with_hint('project', 'project.labelp') }} + {{ type_label_p(entity) }} {% endblock %} {% block edit_title %} diff --git a/templates/admin/storelocation_admin.html.twig b/templates/admin/storelocation_admin.html.twig index 954c03222..b01ecc73a 100644 --- a/templates/admin/storelocation_admin.html.twig +++ b/templates/admin/storelocation_admin.html.twig @@ -2,8 +2,7 @@ {% import "label_system/dropdown_macro.html.twig" as dropdown %} {% block card_title %} - - {{ data_source_name_with_hint('storagelocation', 'storelocation.labelp') }} + {{ type_label_p(entity) }} {% endblock %} {% block additional_controls %} diff --git a/templates/admin/supplier_admin.html.twig b/templates/admin/supplier_admin.html.twig index 7b0813d48..d0ca85aa0 100644 --- a/templates/admin/supplier_admin.html.twig +++ b/templates/admin/supplier_admin.html.twig @@ -1,8 +1,7 @@ {% extends "admin/base_company_admin.html.twig" %} {% block card_title %} - - {{ data_source_name_with_hint('supplier', 'supplier.labelp') }} + {{ type_label_p(entity) }} {% endblock %} {% block additional_panes %} diff --git a/templates/admin/user_admin.html.twig b/templates/admin/user_admin.html.twig index 772b42d9c..9b241e562 100644 --- a/templates/admin/user_admin.html.twig +++ b/templates/admin/user_admin.html.twig @@ -5,7 +5,7 @@ {# @var entity \App\Entity\UserSystem\User #} {% block card_title %} - {% trans %}user.edit.caption{% endtrans %} + {{ type_label_p(entity) }} {% endblock %} {% block comment %}{% endblock %} @@ -111,4 +111,4 @@ {% block preview_picture %} -{% endblock %} \ No newline at end of file +{% endblock %} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 5d2c404e0..c148db2a8 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -97,16 +97,6 @@ New category - - - Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:4 - Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:4 - - - currency.caption - Currency - - Part-DB1\templates\AdminPages\CurrencyAdmin.html.twig:12 @@ -418,16 +408,6 @@ New footprint - - - Part-DB1\templates\AdminPages\GroupAdmin.html.twig:4 - Part-DB1\templates\AdminPages\GroupAdmin.html.twig:4 - - - group.edit.caption - Groups - - Part-DB1\templates\AdminPages\GroupAdmin.html.twig:9 @@ -460,15 +440,6 @@ New group - - - Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:4 - - - label_profile.caption - Label profiles - - Part-DB1\templates\AdminPages\LabelProfileAdmin.html.twig:8 @@ -507,17 +478,6 @@ New label profile - - - Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:4 - Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:4 - templates\AdminPages\ManufacturerAdmin.html.twig:4 - - - manufacturer.caption - Manufacturers - - Part-DB1\templates\AdminPages\ManufacturerAdmin.html.twig:8 @@ -538,22 +498,6 @@ New manufacturer - - - Part-DB1\templates\AdminPages\MeasurementUnitAdmin.html.twig:4 - Part-DB1\templates\AdminPages\MeasurementUnitAdmin.html.twig:4 - - - measurement_unit.caption - Measurement Unit - - - - - part_custom_state.caption - Custom part states - - Part-DB1\templates\AdminPages\StorelocationAdmin.html.twig:5 @@ -620,16 +564,6 @@ New supplier - - - Part-DB1\templates\AdminPages\UserAdmin.html.twig:8 - Part-DB1\templates\AdminPages\UserAdmin.html.twig:8 - - - user.edit.caption - Users - - Part-DB1\templates\AdminPages\UserAdmin.html.twig:14 @@ -4897,7 +4831,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measurement Unit - + part.table.partCustomState Custom part state @@ -5767,7 +5701,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measuring unit - + part.edit.partCustomState Custom part state @@ -6060,7 +5994,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measurement unit - + part_custom_state.label Custom part state @@ -6309,7 +6243,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can Measurement Unit - + tree.tools.edit.part_custom_state Custom part states @@ -8594,7 +8528,7 @@ Element 1 -> Element 1.2]]> Measurement unit - + perm.part_custom_states Custom part state @@ -10995,7 +10929,7 @@ Element 1 -> Element 1.2]]> Measuring Unit - + log.element_edited.changed_fields.partCustomState Custom part state @@ -11265,13 +11199,13 @@ Element 1 -> Element 1.2]]> Edit Measurement Unit - + part_custom_state.new New custom part state - + part_custom_state.edit Edit custom part state @@ -12882,91 +12816,91 @@ Please note, that you can not impersonate a disabled user. If you try you will g Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!]]> - + settings.system.data_source_synonyms Data source synonyms - + settings.system.data_source_synonyms.configuration Source - + settings.system.data_source_synonyms.configuration.help Define your own synonyms for the given data sources. Add data source, language and translations freely; unused languages remain empty. - + settings.behavior.data_source_synonyms.category Category - + settings.behavior.data_source_synonyms.storagelocation Storage location - + settings.behavior.data_source_synonyms.footprint Footprint - + settings.behavior.data_source_synonyms.manufacturer Manufacturer - + settings.behavior.data_source_synonyms.supplier Supplier - + settings.behavior.data_source_synonyms.project Project - + settings.behavior.data_source_synonyms.collection.add_entry Add entry - + settings.behavior.data_source_synonyms.collection.remove_entry Remove entry - + settings.behavior.data_source_synonyms.row_type.form.datasource Data source - + settings.behavior.data_source_synonyms.row_type.form.locale Locale - + settings.behavior.data_source_synonyms.row_type.form.translation_singular Translation singular - + settings.behavior.data_source_synonyms.row_type.form.translation_plural Translation plural @@ -14496,31 +14430,6 @@ Please note, that you can not impersonate a disabled user. If you try you will g - - - project.builds.no_bom_entries - Project has no BOM entries - - - - - settings.behavior.sidebar.data_structure_nodes_table_include_children - Tables should include children nodes by default - - - - - settings.behavior.sidebar.data_structure_nodes_table_include_children.help - If checked, the part tables for categories, footprints, etc. should include all parts of child categories. If not checked, only parts that strictly belong to the clicked node are shown. - - - - - info_providers.search.error.oauth_reconnect - You need to reconnect OAuth for following providers: %provider% -You can do this in the provider info list. - - project.builds.no_bom_entries @@ -14558,11 +14467,53 @@ You can do this in the provider info list. A PCRE-compatible regular expression every IPN has to fulfill. Leave empty to allow all everything as IPN. - + datasource.synonym %name% (Your synonym: %synonym%) + + + user.labelp + Users + + + + + currency.labelp + Currencies + + + + + measurement_unit.labelp + Measurement units + + + + + attachment_type.labelp + Attachment types + + + + + label_profile.labelp + Label profiles + + + + + part_custom_state.labelp + Custom part states + + + + + group.labelp + Groups + + From a0a12b86924e23bfac69a788c4c32393a39e97ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 11 Nov 2025 23:08:40 +0100 Subject: [PATCH 18/27] Removed now unnecessary services --- .../Misc/DataSourceSynonymResolver.php | 71 ------------------- src/Twig/DataSourceNameExtension.php | 67 ----------------- 2 files changed, 138 deletions(-) delete mode 100644 src/Services/Misc/DataSourceSynonymResolver.php delete mode 100644 src/Twig/DataSourceNameExtension.php diff --git a/src/Services/Misc/DataSourceSynonymResolver.php b/src/Services/Misc/DataSourceSynonymResolver.php deleted file mode 100644 index 57e3d642a..000000000 --- a/src/Services/Misc/DataSourceSynonymResolver.php +++ /dev/null @@ -1,71 +0,0 @@ -. - */ - -declare(strict_types=1); - -namespace App\Services\Misc; - -use App\Settings\SystemSettings\DataSourceSynonymsSettings; -use Symfony\Contracts\Translation\TranslatorInterface; - -readonly class DataSourceSynonymResolver -{ - public function __construct( - private TranslatorInterface $translator, - private DataSourceSynonymsSettings $synonymsSettings, - ) { - } - - public function displayNamePlural(string $dataSource, string $defaultKey, ?string $locale = null): string - { - $locale ??= $this->translator->getLocale(); - $syn = $this->synonyms($dataSource, $locale); - - if ($syn['plural'] !== '') { - return $syn['plural']; - } - - return $this->translator->trans($defaultKey, locale: $locale); - } - - public function displayNameSingular(string $dataSource, string $defaultKey, ?string $locale = null): string - { - $locale ??= $this->translator->getLocale(); - $syn = $this->synonyms($dataSource, $locale); - - if ($syn['singular'] !== '') { - return $syn['singular']; - } - - return $this->translator->trans($defaultKey, locale: $locale); - } - - private function synonyms(string $dataSource, string $locale): array - { - $all = []; - $row = $all[$dataSource][$locale] ?? ['singular' => '', 'plural' => '']; - - return [ - 'singular' => (string)($row['singular'] ?? ''), - 'plural' => (string)($row['plural'] ?? ''), - ]; - } -} diff --git a/src/Twig/DataSourceNameExtension.php b/src/Twig/DataSourceNameExtension.php deleted file mode 100644 index 693b3a368..000000000 --- a/src/Twig/DataSourceNameExtension.php +++ /dev/null @@ -1,67 +0,0 @@ -resolver->displayNameSingular($dataSourceName, $defaultKeySingular, $this->translator->getLocale()); - } - - /** - * Returns the plural synonym for the given data source in current locale, - * or the translated fallback key if no synonym provided. - */ - public function getDataSourceNamePlural(string $dataSourceName, string $defaultKeyPlural): string - { - return $this->resolver->displayNamePlural($dataSourceName, $defaultKeyPlural, $this->translator->getLocale()); - } - - /** - * Like data_source_name, only with a note if a synonym was set (uses translation key 'datasource.synonym'). - */ - public function getDataSourceNameWithHint(string $dataSourceName, string $defaultKey, string $type = 'singular'): string - { - $type = $type === 'singular' ? 'singular' : 'plural'; - - $resolved = $type === 'singular' - ? $this->resolver->displayNameSingular($dataSourceName, $defaultKey, $this->translator->getLocale()) - : $this->resolver->displayNamePlural($dataSourceName, $defaultKey, $this->translator->getLocale()); - - $fallback = $this->translator->trans($defaultKey); - - if ($resolved !== $fallback) { - return $this->translator->trans('datasource.synonym', [ - '%name%' => $fallback, - '%synonym%' => $resolved, - ]); - } - - return $fallback; - } -} From 446f4a662ddbb9d05fbd47486e9eb71eea0251f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 11 Nov 2025 23:29:38 +0100 Subject: [PATCH 19/27] Reworked settings name and translation --- src/Services/ElementTypeNameGenerator.php | 4 +- src/Services/Trees/TreeViewGenerator.php | 2 - src/Settings/AppSettings.php | 6 ++ ...nonymsSettings.php => SynonymSettings.php} | 24 +++--- .../SystemSettings/SystemSettings.php | 3 +- templates/settings/settings.html.twig | 2 +- translations/messages.cs.xlf | 54 ------------- translations/messages.de.xlf | 54 ------------- translations/messages.en.xlf | 79 ++++++------------- 9 files changed, 46 insertions(+), 182 deletions(-) rename src/Settings/{SystemSettings/DataSourceSynonymsSettings.php => SynonymSettings.php} (79%) diff --git a/src/Services/ElementTypeNameGenerator.php b/src/Services/ElementTypeNameGenerator.php index ed546fb63..19bb19f58 100644 --- a/src/Services/ElementTypeNameGenerator.php +++ b/src/Services/ElementTypeNameGenerator.php @@ -34,7 +34,7 @@ use App\Entity\ProjectSystem\Project; use App\Entity\ProjectSystem\ProjectBOMEntry; use App\Exceptions\EntityNotSupportedException; -use App\Settings\SystemSettings\DataSourceSynonymsSettings; +use App\Settings\SynonymSettings; use Symfony\Contracts\Translation\TranslatorInterface; /** @@ -46,7 +46,7 @@ public function __construct( private TranslatorInterface $translator, private EntityURLGenerator $entityURLGenerator, - private DataSourceSynonymsSettings $synonymsSettings, + private SynonymSettings $synonymsSettings, ) { } diff --git a/src/Services/Trees/TreeViewGenerator.php b/src/Services/Trees/TreeViewGenerator.php index f1d37e6c5..d55c87b7c 100644 --- a/src/Services/Trees/TreeViewGenerator.php +++ b/src/Services/Trees/TreeViewGenerator.php @@ -38,7 +38,6 @@ use App\Services\Cache\UserCacheKeyGenerator; use App\Services\ElementTypeNameGenerator; use App\Services\EntityURLGenerator; -use App\Services\Misc\DataSourceSynonymResolver; use App\Settings\BehaviorSettings\SidebarSettings; use Doctrine\ORM\EntityManagerInterface; use InvalidArgumentException; @@ -68,7 +67,6 @@ public function __construct( protected TranslatorInterface $translator, private readonly UrlGeneratorInterface $router, private readonly SidebarSettings $sidebarSettings, - protected readonly DataSourceSynonymResolver $synonymResolver, private readonly ElementTypeNameGenerator $elementTypeNameGenerator ) { $this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled; diff --git a/src/Settings/AppSettings.php b/src/Settings/AppSettings.php index 42831d087..14d9395e0 100644 --- a/src/Settings/AppSettings.php +++ b/src/Settings/AppSettings.php @@ -47,6 +47,12 @@ class AppSettings #[EmbeddedSettings()] public ?InfoProviderSettings $infoProviders = null; + #[EmbeddedSettings] + public ?SynonymSettings $synonyms = null; + #[EmbeddedSettings()] public ?MiscSettings $miscSettings = null; + + + } diff --git a/src/Settings/SystemSettings/DataSourceSynonymsSettings.php b/src/Settings/SynonymSettings.php similarity index 79% rename from src/Settings/SystemSettings/DataSourceSynonymsSettings.php rename to src/Settings/SynonymSettings.php index 03cb75e35..e952d97a2 100644 --- a/src/Settings/SystemSettings/DataSourceSynonymsSettings.php +++ b/src/Settings/SynonymSettings.php @@ -20,30 +20,28 @@ declare(strict_types=1); -namespace App\Settings\SystemSettings; +namespace App\Settings; use App\Form\Type\DataSourceSynonymsCollectionType; use App\Services\ElementTypes; -use App\Settings\SettingsIcon; use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; use Jbtronics\SettingsBundle\ParameterTypes\SerializeType; -use Jbtronics\SettingsBundle\ParameterTypes\StringType; use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Symfony\Component\Translation\TranslatableMessage as TM; use Symfony\Component\Validator\Constraints as Assert; -#[Settings(label: new TM("settings.system.data_source_synonyms"))] +#[Settings(label: new TM("settings.system.synonyms"), description: "settings.system.synonyms.help")] #[SettingsIcon("fa-language")] -class DataSourceSynonymsSettings +class SynonymSettings { use SettingsTrait; #[SettingsParameter( ArrayType::class, - label: new TM("settings.system.data_source_synonyms.configuration"), - description: new TM("settings.system.data_source_synonyms.configuration.help"), + label: new TM("settings.system.synonyms.type_synonyms"), + description: new TM("settings.system.synonyms.type_synonyms.help"), options: ['type' => SerializeType::class], formType: DataSourceSynonymsCollectionType::class, formOptions: [ @@ -53,7 +51,7 @@ class DataSourceSynonymsSettings #[Assert\Type('array')] #[Assert\All([new Assert\Type('array')])] /** - * @var array> $customTypeLabels + * @var array> $typeSynonyms * An array of the form: [ * 'category' => [ * 'en' => ['singular' => 'Category', 'plural' => 'Categories'], @@ -64,7 +62,7 @@ class DataSourceSynonymsSettings * ], * ] */ - public array $customTypeLabels = []; + public array $typeSynonyms = []; /** * Checks if there is any synonym defined for the given type (no matter which language). @@ -73,7 +71,7 @@ class DataSourceSynonymsSettings */ public function isSynonymDefinedForType(ElementTypes $type): bool { - return isset($this->customTypeLabels[$type->value]); + return isset($this->typeSynonyms[$type->value]); } /** @@ -84,7 +82,7 @@ public function isSynonymDefinedForType(ElementTypes $type): bool */ public function getSingularSynonymForType(ElementTypes $type, string $locale): ?string { - return $this->customTypeLabels[$type->value][$locale]['singular'] ?? null; + return $this->typeSynonyms[$type->value][$locale]['singular'] ?? null; } /** @@ -95,8 +93,8 @@ public function getSingularSynonymForType(ElementTypes $type, string $locale): ? */ public function getPluralSynonymForType(ElementTypes $type, ?string $locale): ?string { - return $this->customTypeLabels[$type->value][$locale]['plural'] - ?? $this->customTypeLabels[$type->value][$locale]['singular'] + return $this->typeSynonyms[$type->value][$locale]['plural'] + ?? $this->typeSynonyms[$type->value][$locale]['singular'] ?? null; } } diff --git a/src/Settings/SystemSettings/SystemSettings.php b/src/Settings/SystemSettings/SystemSettings.php index 2e8871167..8cbeb560b 100644 --- a/src/Settings/SystemSettings/SystemSettings.php +++ b/src/Settings/SystemSettings/SystemSettings.php @@ -33,8 +33,7 @@ class SystemSettings #[EmbeddedSettings()] public ?LocalizationSettings $localization = null; - #[EmbeddedSettings] - public ?DataSourceSynonymsSettings $dataSourceSynonyms = null; + #[EmbeddedSettings()] public ?CustomizationSettings $customization = null; diff --git a/templates/settings/settings.html.twig b/templates/settings/settings.html.twig index 5ddbd9004..96e0f2098 100644 --- a/templates/settings/settings.html.twig +++ b/templates/settings/settings.html.twig @@ -36,7 +36,7 @@ {% for section_widget in tab_widget %} {% set settings_object = section_widget.vars.value %} - {% if section_widget.vars.compound ?? false %} + {% if section_widget.vars.embedded_settings_metadata is defined %} {# Check if we have nested embedded settings or not #}
    diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index cefc9bb38..7a15bbe71 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -12813,60 +12813,6 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Jazyky, které se zobrazují v uživatelské rozbalovací nabídce - - - settings.system.data_source_synonyms - Synonyma zdrojů dat - - - - - settings.system.data_source_synonyms.configuration - Zdroj - - - - - settings.system.data_source_synonyms.configuration.help - Definujte vlastní synonyma pro zadané zdroje dat. Volně přidávat zdroj dat, jazyk a překlady; Jazyky, které se nepoužívají, zůstávají prázdné. - - - - - settings.behavior.data_source_synonyms.category - Kategorie - - - - - settings.behavior.data_source_synonyms.storagelocation - Skladové umístění - - - - - settings.behavior.data_source_synonyms.footprint - Pouzdro - - - - - settings.behavior.data_source_synonyms.manufacturer - Výrobce - - - - - settings.behavior.data_source_synonyms.supplier - Dodavatel - - - - - settings.behavior.data_source_synonyms.project - Projekt - - settings.behavior.data_source_synonyms.collection.add_entry diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 646281a53..8d6f2a548 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -12881,60 +12881,6 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Bitte beachten Sie, dass die Währungen bei einer Änderung dieses Wertes nicht umgerechnet werden. Wenn Sie also die Basiswährung ändern, nachdem Sie bereits Preisinformationen hinzugefügt haben, führt dies zu falschen Preisen!]]> - - - settings.system.data_source_synonyms - Datenquellen-Synonyme - - - - - settings.system.data_source_synonyms.configuration - Quelle - - - - - settings.system.data_source_synonyms.configuration.help - Definieren Sie Ihre eigenen Synonyme für die angegebenen Datenquellen. Datenquelle, Sprache und Übersetzungen frei hinzufügen; Nicht verwendete Sprachen bleiben leer. - - - - - settings.behavior.data_source_synonyms.category - Kategorie - - - - - settings.behavior.data_source_synonyms.storagelocation - Lagerort - - - - - settings.behavior.data_source_synonyms.footprint - Footprint - - - - - settings.behavior.data_source_synonyms.manufacturer - Hersteller - - - - - settings.behavior.data_source_synonyms.supplier - Lieferant - - - - - settings.behavior.data_source_synonyms.project - Projekt - - settings.behavior.data_source_synonyms.collection.add_entry diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index c148db2a8..431595876 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12816,60 +12816,6 @@ Please note, that you can not impersonate a disabled user. If you try you will g Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!]]> - - - settings.system.data_source_synonyms - Data source synonyms - - - - - settings.system.data_source_synonyms.configuration - Source - - - - - settings.system.data_source_synonyms.configuration.help - Define your own synonyms for the given data sources. Add data source, language and translations freely; unused languages remain empty. - - - - - settings.behavior.data_source_synonyms.category - Category - - - - - settings.behavior.data_source_synonyms.storagelocation - Storage location - - - - - settings.behavior.data_source_synonyms.footprint - Footprint - - - - - settings.behavior.data_source_synonyms.manufacturer - Manufacturer - - - - - settings.behavior.data_source_synonyms.supplier - Supplier - - - - - settings.behavior.data_source_synonyms.project - Project - - settings.behavior.data_source_synonyms.collection.add_entry @@ -14515,5 +14461,30 @@ You can do this in the provider info list. Groups + + + settings.system.synonyms + Synonyms + + + + + settings.system.synonyms.help + The synonyms systems allow overriding how Part-DB call certain things. This can be useful, especially if Part-DB is used in a different context than electronics. +Please note that this system is currently experimental, and the synonyms defined here might not show up at all places. + + + + + settings.system.synonyms.type_synonyms + Type synonyms + + + + + settings.system.synonyms.type_synonyms.help + Type synonyms allow you to replace the labels of built-in data types. For example, you can rename "Footprint" to something else. + + From 1234f447fdeb7bbfda701694c6eb8be056c9fe59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 11 Nov 2025 23:44:06 +0100 Subject: [PATCH 20/27] Renamed all files to Synonyms --- .../synonyms_collection_controller.js} | 0 config/packages/twig.yaml | 2 +- .../LanguageMenuEntriesType.php | 3 +- .../TypeSynonymRowType.php} | 11 +++--- .../TypeSynonymsCollectionType.php} | 26 +++++++++++--- src/Settings/SynonymSettings.php | 4 +-- .../SystemSettings/LocalizationSettings.php | 2 +- ...tml.twig => synonyms_collection.html.twig} | 24 ++++++------- translations/messages.en.xlf | 36 +++++++++++++++++++ 9 files changed, 79 insertions(+), 29 deletions(-) rename assets/controllers/{elements/datasource_synonyms_collection_controller.js => pages/synonyms_collection_controller.js} (100%) rename src/Form/{Type => Settings}/LanguageMenuEntriesType.php (95%) rename src/Form/{Type/DataSourceSynonymRowType.php => Settings/TypeSynonymRowType.php} (92%) rename src/Form/{Type/DataSourceSynonymsCollectionType.php => Settings/TypeSynonymsCollectionType.php} (87%) rename templates/form/{datasource_synonyms_collection.html.twig => synonyms_collection.html.twig} (56%) diff --git a/assets/controllers/elements/datasource_synonyms_collection_controller.js b/assets/controllers/pages/synonyms_collection_controller.js similarity index 100% rename from assets/controllers/elements/datasource_synonyms_collection_controller.js rename to assets/controllers/pages/synonyms_collection_controller.js diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index b9fdf5bd9..95ae4f3b1 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -1,6 +1,6 @@ twig: default_path: '%kernel.project_dir%/templates' - form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig', 'form/datasource_synonyms_collection.html.twig'] + form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'form/extended_bootstrap_layout.html.twig', 'form/permission_layout.html.twig', 'form/filter_types_layout.html.twig', 'form/synonyms_collection.html.twig'] paths: '%kernel.project_dir%/assets/css': css diff --git a/src/Form/Type/LanguageMenuEntriesType.php b/src/Form/Settings/LanguageMenuEntriesType.php similarity index 95% rename from src/Form/Type/LanguageMenuEntriesType.php rename to src/Form/Settings/LanguageMenuEntriesType.php index a3bba77f5..9bc2e850b 100644 --- a/src/Form/Type/LanguageMenuEntriesType.php +++ b/src/Form/Settings/LanguageMenuEntriesType.php @@ -21,12 +21,11 @@ declare(strict_types=1); -namespace App\Form\Type; +namespace App\Form\Settings; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\LanguageType; -use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\Intl\Languages; use Symfony\Component\OptionsResolver\OptionsResolver; diff --git a/src/Form/Type/DataSourceSynonymRowType.php b/src/Form/Settings/TypeSynonymRowType.php similarity index 92% rename from src/Form/Type/DataSourceSynonymRowType.php rename to src/Form/Settings/TypeSynonymRowType.php index c35ad81f2..332db907a 100644 --- a/src/Form/Type/DataSourceSynonymRowType.php +++ b/src/Form/Settings/TypeSynonymRowType.php @@ -20,28 +20,26 @@ declare(strict_types=1); -namespace App\Form\Type; +namespace App\Form\Settings; use App\Services\ElementTypes; use App\Settings\SystemSettings\LocalizationSettings; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\ChoiceType; use Symfony\Component\Form\Extension\Core\Type\EnumType; use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\Form\Extension\Core\Type\TextType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Intl\Locales; -use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\Validator\Constraints as Assert; /** * A single translation row: data source + language + translations (singular/plural). */ -class DataSourceSynonymRowType extends AbstractType +class TypeSynonymRowType extends AbstractType { - private const PREFFERED_TYPES = [ + private const PREFERRED_TYPES = [ ElementTypes::CATEGORY, ElementTypes::STORAGE_LOCATION, ElementTypes::FOOTPRINT, @@ -62,14 +60,13 @@ public function buildForm(FormBuilderInterface $builder, array $options): void ->add('dataSource', EnumType::class, [ 'class' => ElementTypes::class, 'label' => false, - //'choices' => $this->buildDataSourceChoices($options['data_sources']), 'required' => true, 'constraints' => [ new Assert\NotBlank(), ], 'row_attr' => ['class' => 'mb-0'], 'attr' => ['class' => 'form-select-sm'], - 'preferred_choices' => self::PREFFERED_TYPES + 'preferred_choices' => self::PREFERRED_TYPES ]) ->add('locale', LocaleType::class, [ 'label' => false, diff --git a/src/Form/Type/DataSourceSynonymsCollectionType.php b/src/Form/Settings/TypeSynonymsCollectionType.php similarity index 87% rename from src/Form/Type/DataSourceSynonymsCollectionType.php rename to src/Form/Settings/TypeSynonymsCollectionType.php index c8c44a504..9c239c21e 100644 --- a/src/Form/Type/DataSourceSynonymsCollectionType.php +++ b/src/Form/Settings/TypeSynonymsCollectionType.php @@ -1,8 +1,26 @@ . + */ declare(strict_types=1); -namespace App\Form\Type; +namespace App\Form\Settings; use App\Services\ElementTypes; use Symfony\Component\Form\AbstractType; @@ -21,7 +39,7 @@ * View data: list [{dataSource, locale, translation_singular, translation_plural}, ...] * Model data: same structure (list). Optionally expands a nested map to a list. */ -class DataSourceSynonymsCollectionType extends AbstractType +class TypeSynonymsCollectionType extends AbstractType { public function __construct(private readonly TranslatorInterface $translator) { @@ -180,7 +198,7 @@ public function configureOptions(OptionsResolver $resolver): void // Defaults for the collection and entry type $resolver->setDefaults([ - 'entry_type' => DataSourceSynonymRowType::class, + 'entry_type' => TypeSynonymRowType::class, 'allow_add' => true, 'allow_delete' => true, 'by_reference' => false, @@ -198,6 +216,6 @@ public function getParent(): ?string public function getBlockPrefix(): string { - return 'datasource_synonyms_collection'; + return 'type_synonyms_collection'; } } diff --git a/src/Settings/SynonymSettings.php b/src/Settings/SynonymSettings.php index e952d97a2..4a8e94ab3 100644 --- a/src/Settings/SynonymSettings.php +++ b/src/Settings/SynonymSettings.php @@ -22,7 +22,7 @@ namespace App\Settings; -use App\Form\Type\DataSourceSynonymsCollectionType; +use App\Form\Settings\TypeSynonymsCollectionType; use App\Services\ElementTypes; use Jbtronics\SettingsBundle\ParameterTypes\ArrayType; use Jbtronics\SettingsBundle\ParameterTypes\SerializeType; @@ -43,7 +43,7 @@ class SynonymSettings label: new TM("settings.system.synonyms.type_synonyms"), description: new TM("settings.system.synonyms.type_synonyms.help"), options: ['type' => SerializeType::class], - formType: DataSourceSynonymsCollectionType::class, + formType: TypeSynonymsCollectionType::class, formOptions: [ 'required' => false, ], diff --git a/src/Settings/SystemSettings/LocalizationSettings.php b/src/Settings/SystemSettings/LocalizationSettings.php index 7c83d1ef2..c6780c6c9 100644 --- a/src/Settings/SystemSettings/LocalizationSettings.php +++ b/src/Settings/SystemSettings/LocalizationSettings.php @@ -23,7 +23,7 @@ namespace App\Settings\SystemSettings; -use App\Form\Type\LanguageMenuEntriesType; +use App\Form\Settings\LanguageMenuEntriesType; use App\Form\Type\LocaleSelectType; use App\Settings\SettingsIcon; use Jbtronics\SettingsBundle\Metadata\EnvVarMode; diff --git a/templates/form/datasource_synonyms_collection.html.twig b/templates/form/synonyms_collection.html.twig similarity index 56% rename from templates/form/datasource_synonyms_collection.html.twig rename to templates/form/synonyms_collection.html.twig index 27c4a0513..ee69dffc6 100644 --- a/templates/form/datasource_synonyms_collection.html.twig +++ b/templates/form/synonyms_collection.html.twig @@ -7,18 +7,18 @@
    {{ form_row(child.translation_singular) }}
    {{ form_row(child.translation_plural) }}
    -
    {% endmacro %} -{% block datasource_synonyms_collection_widget %} +{% block type_synonyms_collection_widget %} {% set _attrs = attr|default({}) %} {% set _attrs = _attrs|merge({ - class: (_attrs.class|default('') ~ ' datasource-synonyms-collection-widget')|trim + class: (_attrs.class|default('') ~ ' type_synonyms_collection-widget')|trim }) %} {% set has_proto = prototype is defined %} @@ -32,7 +32,7 @@ {% endif %}
    -
    {% trans %}settings.behavior.data_source_synonyms.row_type.form.datasource{% endtrans %}
    -
    {% trans %}settings.behavior.data_source_synonyms.row_type.form.locale{% endtrans %}
    -
    {% trans %}settings.behavior.data_source_synonyms.row_type.form.translation_singular{% endtrans %}
    -
    {% trans %}settings.behavior.data_source_synonyms.row_type.form.translation_plural{% endtrans %}
    +
    {% trans%}settings.synonyms.type_synonym.type{% endtrans%}
    +
    {% trans%}settings.synonyms.type_synonym.language{% endtrans%}
    +
    {% trans%}settings.synonyms.type_synonym.translation_singular{% endtrans%}
    +
    {% trans%}settings.synonyms.type_synonym.translation_plural{% endtrans%}
    -
    +
    {% for child in form %} {{ _self.renderForm(child) }} {% endfor %}
    -
    {% endblock %} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 431595876..ac46c069c 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -14486,5 +14486,41 @@ Please note that this system is currently experimental, and the synonyms defined Type synonyms allow you to replace the labels of built-in data types. For example, you can rename "Footprint" to something else. + + + settings.synonyms.type_synonym.type + Type + + + + + settings.synonyms.type_synonym.language + Language + + + + + settings.synonyms.type_synonym.translation_singular + Translation Singular + + + + + settings.synonyms.type_synonym.translation_plural + Translation Plural + + + + + settings.synonyms.type_synonym.add_entry + Add entry + + + + + settings.synonyms.type_synonym.remove_entry + Remove entry + + From 230ae0f447006baf4a1cf5179d1fc31d30339bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 11 Nov 2025 23:48:02 +0100 Subject: [PATCH 21/27] Removed unnecessary translations --- translations/messages.cs.xlf | 48 ------------------------------------ translations/messages.da.xlf | 6 ----- translations/messages.de.xlf | 42 ------------------------------- translations/messages.el.xlf | 6 ----- translations/messages.en.xlf | 42 ------------------------------- translations/messages.es.xlf | 6 ----- translations/messages.fr.xlf | 6 ----- translations/messages.it.xlf | 6 ----- translations/messages.ja.xlf | 6 ----- translations/messages.nl.xlf | 6 ----- translations/messages.pl.xlf | 6 ----- translations/messages.ru.xlf | 6 ----- translations/messages.zh.xlf | 6 ----- 13 files changed, 192 deletions(-) diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 7a15bbe71..559b0d8e0 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -12801,54 +12801,6 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz <b>Upozorňujeme, že při změně této hodnoty nedochází k převodu měn. Změna výchozí měny po přidání informací o cenách tedy povede k nesprávným cenám!</b> - - - settings.system.localization.preferred_languages - Preferované jazyky - - - - - settings.system.localization.preferred_languages.help - Jazyky, které se zobrazují v uživatelské rozbalovací nabídce - - - - - settings.behavior.data_source_synonyms.collection.add_entry - Přidat položku - - - - - settings.behavior.data_source_synonyms.collection.remove_entry - Odebrat položku - - - - - settings.behavior.data_source_synonyms.row_type.form.datasource - Zdroj dat - - - - - settings.behavior.data_source_synonyms.row_type.form.locale - Místní nastavení - - - - - settings.behavior.data_source_synonyms.row_type.form.translation_singular - Překlad (jednotné číslo) - - - - - settings.behavior.data_source_synonyms.row_type.form.translation_plural - Překlad (množné číslo) - - settings.system.privacy diff --git a/translations/messages.da.xlf b/translations/messages.da.xlf index afa5164f0..530d91aa9 100644 --- a/translations/messages.da.xlf +++ b/translations/messages.da.xlf @@ -12328,11 +12328,5 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver Du forsøgte at fjerne/tilføje en mængde sat til nul! Der blev ikke foretaget nogen handling. - - - datasource.synonym - %name% (Dit synonym: %synonym%) - - diff --git a/translations/messages.de.xlf b/translations/messages.de.xlf index 8d6f2a548..806c2e52b 100644 --- a/translations/messages.de.xlf +++ b/translations/messages.de.xlf @@ -12881,42 +12881,6 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön Bitte beachten Sie, dass die Währungen bei einer Änderung dieses Wertes nicht umgerechnet werden. Wenn Sie also die Basiswährung ändern, nachdem Sie bereits Preisinformationen hinzugefügt haben, führt dies zu falschen Preisen!]]> - - - settings.behavior.data_source_synonyms.collection.add_entry - Eintrag hinzufügen - - - - - settings.behavior.data_source_synonyms.collection.remove_entry - Eintrag entfernen - - - - - settings.behavior.data_source_synonyms.row_type.form.datasource - Datenquelle - - - - - settings.behavior.data_source_synonyms.row_type.form.locale - Sprache - - - - - settings.behavior.data_source_synonyms.row_type.form.translation_singular - Übersetzung (Singular) - - - - - settings.behavior.data_source_synonyms.row_type.form.translation_plural - Übersetzung (Plural) - - settings.system.privacy @@ -14472,11 +14436,5 @@ Dies ist auf der Informationsquellen Übersichtsseite möglich. Wenn aktiviert, wird die Bauteil-Beschreibung verwendet, um vorhandene Teile mit derselben Beschreibung zu finden und die nächste verfügbare IPN für die Vorschlagsliste zu ermitteln, indem der numerische Suffix entsprechend erhöht wird. - - - datasource.synonym - %name% (Ihr Synonym: %synonym%) - - diff --git a/translations/messages.el.xlf b/translations/messages.el.xlf index 43b65a88b..3618fa3db 100644 --- a/translations/messages.el.xlf +++ b/translations/messages.el.xlf @@ -1667,11 +1667,5 @@ Δημιουργήστε πρώτα ένα εξάρτημα και αντιστοιχίστε το σε μια κατηγορία: με τις υπάρχουσες κατηγορίες και τα δικά τους προθέματα IPN, η ονομασία IPN για το εξάρτημα μπορεί να προταθεί αυτόματα - - - datasource.synonym - %name% (Το συνώνυμό σας: %synonym%) - - diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index ac46c069c..2890d2d3f 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -12816,42 +12816,6 @@ Please note, that you can not impersonate a disabled user. If you try you will g Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!]]> - - - settings.behavior.data_source_synonyms.collection.add_entry - Add entry - - - - - settings.behavior.data_source_synonyms.collection.remove_entry - Remove entry - - - - - settings.behavior.data_source_synonyms.row_type.form.datasource - Data source - - - - - settings.behavior.data_source_synonyms.row_type.form.locale - Locale - - - - - settings.behavior.data_source_synonyms.row_type.form.translation_singular - Translation singular - - - - - settings.behavior.data_source_synonyms.row_type.form.translation_plural - Translation plural - - settings.system.privacy @@ -14413,12 +14377,6 @@ You can do this in the provider info list. A PCRE-compatible regular expression every IPN has to fulfill. Leave empty to allow all everything as IPN. - - - datasource.synonym - %name% (Your synonym: %synonym%) - - user.labelp diff --git a/translations/messages.es.xlf b/translations/messages.es.xlf index 9ee7974e1..57ac5c857 100644 --- a/translations/messages.es.xlf +++ b/translations/messages.es.xlf @@ -12500,11 +12500,5 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S Este componente contiene más de un stock. Cambie la ubicación manualmente para seleccionar el stock deseado. - - - datasource.synonym - %name% (Tu sinónimo: %synonym%) - - diff --git a/translations/messages.fr.xlf b/translations/messages.fr.xlf index 90eca23b7..cb3936ef7 100644 --- a/translations/messages.fr.xlf +++ b/translations/messages.fr.xlf @@ -9229,11 +9229,5 @@ exemple de ville Un préfixe suggéré lors de la saisie de l'IPN d'une pièce. - - - datasource.synonym - %name% (Votre synonyme : %synonym%) - - diff --git a/translations/messages.it.xlf b/translations/messages.it.xlf index 70422a57e..34540da1b 100644 --- a/translations/messages.it.xlf +++ b/translations/messages.it.xlf @@ -12502,11 +12502,5 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a Questo componente contiene più di uno stock. Cambia manualmente la posizione per selezionare quale stock scegliere. - - - datasource.synonym - %name% (Il tuo sinonimo: %synonym%) - - diff --git a/translations/messages.ja.xlf b/translations/messages.ja.xlf index 79dd07911..668c51c12 100644 --- a/translations/messages.ja.xlf +++ b/translations/messages.ja.xlf @@ -8966,11 +8966,5 @@ Exampletown 部品のIPN入力時に提案される接頭辞。 - - - datasource.synonym - %name% (あなたの同義語: %synonym%) - - diff --git a/translations/messages.nl.xlf b/translations/messages.nl.xlf index 7d1245569..1c0631879 100644 --- a/translations/messages.nl.xlf +++ b/translations/messages.nl.xlf @@ -856,11 +856,5 @@ Maak eerst een component en wijs het toe aan een categorie: met de bestaande categorieën en hun eigen IPN-prefixen kan de IPN voor het component automatisch worden voorgesteld - - - datasource.synonym - %name% (Uw synoniem: %synonym%) - - diff --git a/translations/messages.pl.xlf b/translations/messages.pl.xlf index 686437f48..0a9353fb7 100644 --- a/translations/messages.pl.xlf +++ b/translations/messages.pl.xlf @@ -12355,11 +12355,5 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli Wygenerowany kod - - - datasource.synonym - %name% (Twój synonim: %synonym%) - - diff --git a/translations/messages.ru.xlf b/translations/messages.ru.xlf index eb77708d4..0fbf7a42d 100644 --- a/translations/messages.ru.xlf +++ b/translations/messages.ru.xlf @@ -12455,11 +12455,5 @@ Профиль сохранен! - - - datasource.synonym - %name% (Ваш синоним: %synonym%) - - diff --git a/translations/messages.zh.xlf b/translations/messages.zh.xlf index e05aa7e60..ee9128008 100644 --- a/translations/messages.zh.xlf +++ b/translations/messages.zh.xlf @@ -12340,11 +12340,5 @@ Element 3 成功创建 %COUNT% 个元素。 - - - datasource.synonym - %name% (您的同义词: %synonym%) - - From ac8b119414127033ca02935364c547f0ddc963a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 11 Nov 2025 23:52:02 +0100 Subject: [PATCH 22/27] Removed unnecessary translations --- src/Form/Settings/TypeSynonymsCollectionType.php | 4 ++-- translations/messages.cs.xlf | 6 ------ translations/validators.cs.xlf | 6 ------ translations/validators.de.xlf | 6 ------ translations/validators.en.xlf | 6 ------ 5 files changed, 2 insertions(+), 26 deletions(-) diff --git a/src/Form/Settings/TypeSynonymsCollectionType.php b/src/Form/Settings/TypeSynonymsCollectionType.php index 9c239c21e..dacacfdf0 100644 --- a/src/Form/Settings/TypeSynonymsCollectionType.php +++ b/src/Form/Settings/TypeSynonymsCollectionType.php @@ -144,7 +144,7 @@ function (array $viewValue) { if ($child->has('dataSource')) { $child->get('dataSource')->addError( new FormError($this->translator->trans( - 'settings.system.data_source_synonyms.collection_type.duplicate', + 'settings.synonyms.type_synonyms.collection_type.duplicate', [], 'validators' )) ); @@ -152,7 +152,7 @@ function (array $viewValue) { if ($child->has('locale')) { $child->get('locale')->addError( new FormError($this->translator->trans( - 'settings.system.data_source_synonyms.collection_type.duplicate', + 'settings.synonyms.type_synonyms.collection_type.duplicate', [], 'validators' )) ); diff --git a/translations/messages.cs.xlf b/translations/messages.cs.xlf index 559b0d8e0..cd572daea 100644 --- a/translations/messages.cs.xlf +++ b/translations/messages.cs.xlf @@ -13659,11 +13659,5 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz Minimální šířka náhledu (px) - - - datasource.synonym - %name% (Váš synonymum: %synonym%) - - diff --git a/translations/validators.cs.xlf b/translations/validators.cs.xlf index 4c1f396d9..c298266af 100644 --- a/translations/validators.cs.xlf +++ b/translations/validators.cs.xlf @@ -365,11 +365,5 @@ Neplatný kód. Zkontrolujte, zda je vaše ověřovací aplikace správně nastavena a zda je čas správně nastaven jak na serveru, tak na ověřovacím zařízení. - - - settings.system.data_source_synonyms.collection_type.duplicate - Dvojitá kombinace zdroje dat a jazyka. - - diff --git a/translations/validators.de.xlf b/translations/validators.de.xlf index 3d125c9a7..5cccd3887 100644 --- a/translations/validators.de.xlf +++ b/translations/validators.de.xlf @@ -365,11 +365,5 @@ Ungültiger Code. Überprüfen Sie, ob die Authenticator App korrekt eingerichtet ist und ob der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben. - - - settings.system.data_source_synonyms.collection_type.duplicate - Doppelte Kombination aus Datenquelle und Sprache. - - diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index c679dac62..6ad144607 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -365,11 +365,5 @@ Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly. - - - settings.system.data_source_synonyms.collection_type.duplicate - Duplicate combination of data source and locale. - - From 181658501b554e6907d9fdd67b2af5137c030762 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Tue, 11 Nov 2025 23:56:21 +0100 Subject: [PATCH 23/27] Fixed duplicate check --- src/Form/Settings/TypeSynonymsCollectionType.php | 6 ++++-- translations/validators.en.xlf | 10 ++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/Form/Settings/TypeSynonymsCollectionType.php b/src/Form/Settings/TypeSynonymsCollectionType.php index dacacfdf0..9c95db821 100644 --- a/src/Form/Settings/TypeSynonymsCollectionType.php +++ b/src/Form/Settings/TypeSynonymsCollectionType.php @@ -88,6 +88,8 @@ public function buildForm(FormBuilderInterface $builder, array $options): void function (array $viewValue) { //Turn our flat list back into the structured array + $out = []; + foreach ($viewValue as $row) { if (!is_array($row)) { continue; @@ -133,8 +135,8 @@ function (array $viewValue) { $ds = $row['dataSource'] ?? null; $loc = $row['locale'] ?? null; - if (is_string($ds) && $ds !== '' && is_string($loc) && $loc !== '') { - $key = $ds . '|' . $loc; + if ($ds !== null && is_string($loc) && $loc !== '') { + $key = $ds->value . '|' . $loc; if (isset($seen[$key])) { $hasDuplicate = true; diff --git a/translations/validators.en.xlf b/translations/validators.en.xlf index 6ad144607..86045227b 100644 --- a/translations/validators.en.xlf +++ b/translations/validators.en.xlf @@ -347,13 +347,13 @@ Due to technical limitations, it is not possible to select dates after the 2038-01-19 on 32-bit systems! - + validator.fileSize.invalidFormat Invalid file size format. Use an integer number plus K, M, G as suffix for Kilo, Mega or Gigabytes. - + validator.invalid_range The given range is not valid! @@ -365,5 +365,11 @@ Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly. + + + settings.synonyms.type_synonyms.collection_type.duplicate + There is already a translation defined for this type and language! + + From 8c634443c9752ef7d43b3298d1437255d9113201 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 12 Nov 2025 00:01:26 +0100 Subject: [PATCH 24/27] Renamed synoynms translations --- src/Settings/SynonymSettings.php | 6 ++-- translations/messages.en.xlf | 50 ++++++++++++++++---------------- 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/Settings/SynonymSettings.php b/src/Settings/SynonymSettings.php index 4a8e94ab3..1d1475ece 100644 --- a/src/Settings/SynonymSettings.php +++ b/src/Settings/SynonymSettings.php @@ -32,7 +32,7 @@ use Symfony\Component\Translation\TranslatableMessage as TM; use Symfony\Component\Validator\Constraints as Assert; -#[Settings(label: new TM("settings.system.synonyms"), description: "settings.system.synonyms.help")] +#[Settings(label: new TM("settings.synonyms"), description: "settings.synonyms.help")] #[SettingsIcon("fa-language")] class SynonymSettings { @@ -40,8 +40,8 @@ class SynonymSettings #[SettingsParameter( ArrayType::class, - label: new TM("settings.system.synonyms.type_synonyms"), - description: new TM("settings.system.synonyms.type_synonyms.help"), + label: new TM("settings.synonyms.type_synonyms"), + description: new TM("settings.synonyms.type_synonyms.help"), options: ['type' => SerializeType::class], formType: TypeSynonymsCollectionType::class, formOptions: [ diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 2890d2d3f..7d4a4885c 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -14419,31 +14419,6 @@ You can do this in the provider info list. Groups - - - settings.system.synonyms - Synonyms - - - - - settings.system.synonyms.help - The synonyms systems allow overriding how Part-DB call certain things. This can be useful, especially if Part-DB is used in a different context than electronics. -Please note that this system is currently experimental, and the synonyms defined here might not show up at all places. - - - - - settings.system.synonyms.type_synonyms - Type synonyms - - - - - settings.system.synonyms.type_synonyms.help - Type synonyms allow you to replace the labels of built-in data types. For example, you can rename "Footprint" to something else. - - settings.synonyms.type_synonym.type @@ -14480,5 +14455,30 @@ Please note that this system is currently experimental, and the synonyms defined Remove entry + + + settings.synonyms + Synonyms + + + + + settings.synonyms.help + The synonyms systems allow overriding how Part-DB call certain things. This can be useful, especially if Part-DB is used in a different context than electronics. +Please note that this system is currently experimental, and the synonyms defined here might not show up at all places. + + + + + settings.synonyms.type_synonyms + Type synonyms + + + + + settings.synonyms.type_synonyms.help + Type synonyms allow you to replace the labels of built-in data types. For example, you can rename "Footprint" to something else. + + From 551c7f7e7624c2d4df6b1e806e69bf7d446c91d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 12 Nov 2025 00:06:15 +0100 Subject: [PATCH 25/27] Use our synonyms for permission translations --- config/permissions.yaml | 24 +++++----- translations/messages.en.xlf | 92 +++--------------------------------- 2 files changed, 18 insertions(+), 98 deletions(-) diff --git a/config/permissions.yaml b/config/permissions.yaml index 7acee7f0e..5adfb79d6 100644 --- a/config/permissions.yaml +++ b/config/permissions.yaml @@ -18,7 +18,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co parts: # e.g. this maps to perms_parts in User/Group database group: "data" - label: "perm.parts" + label: "{{part}}" operations: # Here are all possible operations are listed => the op name is mapped to bit value read: label: "perm.read" @@ -71,7 +71,7 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co storelocations: &PART_CONTAINING - label: "perm.storelocations" + label: "{{storage_location}}" group: "data" operations: read: @@ -103,39 +103,39 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co footprints: <<: *PART_CONTAINING - label: "perm.part.footprints" + label: "{{footprint}}" categories: <<: *PART_CONTAINING - label: "perm.part.categories" + label: "{{category}}" suppliers: <<: *PART_CONTAINING - label: "perm.part.supplier" + label: "{{supplier}}" manufacturers: <<: *PART_CONTAINING - label: "perm.part.manufacturers" + label: "{{manufacturer}}" projects: <<: *PART_CONTAINING - label: "perm.projects" + label: "{{project}}" attachment_types: <<: *PART_CONTAINING - label: "perm.part.attachment_types" + label: "{{attachment_type}}" currencies: <<: *PART_CONTAINING - label: "perm.currencies" + label: "{{currency}}" measurement_units: <<: *PART_CONTAINING - label: "perm.measurement_units" + label: "{{measurement_unit}}" part_custom_states: <<: *PART_CONTAINING - label: "perm.part_custom_states" + label: "{{part_custom_state}}" tools: label: "perm.part.tools" @@ -377,4 +377,4 @@ perms: # Here comes a list with all Permission names (they have a perm_[name] co manage_tokens: label: "perm.api.manage_tokens" alsoSet: ['access_api'] - apiTokenRole: ROLE_API_FULL \ No newline at end of file + apiTokenRole: ROLE_API_FULL diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 7d4a4885c..acaee8699 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -7658,16 +7658,6 @@ Element 1 -> Element 1.2]]> System - - - obsolete - obsolete - - - perm.parts - Parts - - obsolete @@ -7928,16 +7918,6 @@ Element 1 -> Element 1.2]]> Orders - - - obsolete - obsolete - - - perm.storelocations - Storage locations - - obsolete @@ -7958,66 +7938,6 @@ Element 1 -> Element 1.2]]> List parts - - - obsolete - obsolete - - - perm.part.footprints - Footprints - - - - - obsolete - obsolete - - - perm.part.categories - Categories - - - - - obsolete - obsolete - - - perm.part.supplier - Suppliers - - - - - obsolete - obsolete - - - perm.part.manufacturers - Manufacturers - - - - - obsolete - obsolete - - - perm.projects - Projects - - - - - obsolete - obsolete - - - perm.part.attachment_types - Attachment types - - obsolete @@ -8528,12 +8448,6 @@ Element 1 -> Element 1.2]]> Measurement unit - - - perm.part_custom_states - Custom part state - - obsolete @@ -14480,5 +14394,11 @@ Please note that this system is currently experimental, and the synonyms defined Type synonyms allow you to replace the labels of built-in data types. For example, you can rename "Footprint" to something else. + + + {{part}} + Parts + + From 8f2ff50dd0742649408a5125e04ca53d79e8184b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 12 Nov 2025 00:08:29 +0100 Subject: [PATCH 26/27] Fixed phpstan issue --- src/Form/Settings/TypeSynonymsCollectionType.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Form/Settings/TypeSynonymsCollectionType.php b/src/Form/Settings/TypeSynonymsCollectionType.php index 9c95db821..4756930ad 100644 --- a/src/Form/Settings/TypeSynonymsCollectionType.php +++ b/src/Form/Settings/TypeSynonymsCollectionType.php @@ -174,8 +174,8 @@ function (array $viewValue) { $sortable = $rows; usort($sortable, static function ($a, $b) { - $aDs = $a['dataSource']?->value ?? ''; - $bDs = $b['dataSource']?->value ?? ''; + $aDs = $a['dataSource']->value ?? ''; + $bDs = $b['dataSource']->value ?? ''; $cmpDs = strcasecmp($aDs, $bDs); if ($cmpDs !== 0) { From e49048b666f49f697b2d825e4dcea236df034c0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Wed, 12 Nov 2025 21:31:44 +0100 Subject: [PATCH 27/27] Added tests --- ...nonymsAsTranslationParametersListener.php} | 4 +- src/Settings/SynonymSettings.php | 18 ++++- ...terSynonymsAsTranslationParametersTest.php | 49 ++++++++++++ .../Services/ElementTypeNameGeneratorTest.php | 41 +++++++++- tests/Services/ElementTypesTest.php | 79 +++++++++++++++++++ tests/Settings/SynonymSettingsTest.php | 76 ++++++++++++++++++ translations/messages.en.xlf | 12 +++ 7 files changed, 272 insertions(+), 7 deletions(-) rename src/EventListener/{RegisterSynonymsAsTranslationParameters.php => RegisterSynonymsAsTranslationParametersListener.php} (94%) create mode 100644 tests/EventListener/RegisterSynonymsAsTranslationParametersTest.php create mode 100644 tests/Services/ElementTypesTest.php create mode 100644 tests/Settings/SynonymSettingsTest.php diff --git a/src/EventListener/RegisterSynonymsAsTranslationParameters.php b/src/EventListener/RegisterSynonymsAsTranslationParametersListener.php similarity index 94% rename from src/EventListener/RegisterSynonymsAsTranslationParameters.php rename to src/EventListener/RegisterSynonymsAsTranslationParametersListener.php index e55a0f9a6..b216aad45 100644 --- a/src/EventListener/RegisterSynonymsAsTranslationParameters.php +++ b/src/EventListener/RegisterSynonymsAsTranslationParametersListener.php @@ -35,7 +35,7 @@ use Symfony\Contracts\Translation\TranslatorInterface; #[AsEventListener] -readonly class RegisterSynonymsAsTranslationParameters +readonly class RegisterSynonymsAsTranslationParametersListener { private Translator $translator; @@ -67,7 +67,7 @@ public function getSynonymPlaceholders(): array //And we have lowercase versions for both $placeholders['[' . $elementType->value . ']'] = mb_strtolower($this->typeNameGenerator->typeLabel($elementType)); - $placeholders['[' . $elementType->value . ']'] = mb_strtolower($this->typeNameGenerator->typeLabelPlural($elementType)); + $placeholders['[[' . $elementType->value . ']]'] = mb_strtolower($this->typeNameGenerator->typeLabelPlural($elementType)); } return $placeholders; diff --git a/src/Settings/SynonymSettings.php b/src/Settings/SynonymSettings.php index 1d1475ece..25fc87e9f 100644 --- a/src/Settings/SynonymSettings.php +++ b/src/Settings/SynonymSettings.php @@ -71,7 +71,7 @@ class SynonymSettings */ public function isSynonymDefinedForType(ElementTypes $type): bool { - return isset($this->typeSynonyms[$type->value]); + return isset($this->typeSynonyms[$type->value]) && count($this->typeSynonyms[$type->value]) > 0; } /** @@ -97,4 +97,20 @@ public function getPluralSynonymForType(ElementTypes $type, ?string $locale): ?s ?? $this->typeSynonyms[$type->value][$locale]['singular'] ?? null; } + + /** + * Sets a synonym for the given type and locale. + * @param ElementTypes $type + * @param string $locale + * @param string $singular + * @param string $plural + * @return void + */ + public function setSynonymForType(ElementTypes $type, string $locale, string $singular, string $plural): void + { + $this->typeSynonyms[$type->value][$locale] = [ + 'singular' => $singular, + 'plural' => $plural, + ]; + } } diff --git a/tests/EventListener/RegisterSynonymsAsTranslationParametersTest.php b/tests/EventListener/RegisterSynonymsAsTranslationParametersTest.php new file mode 100644 index 000000000..d08edecb8 --- /dev/null +++ b/tests/EventListener/RegisterSynonymsAsTranslationParametersTest.php @@ -0,0 +1,49 @@ +. + */ + +namespace App\Tests\EventListener; + +use App\EventListener\RegisterSynonymsAsTranslationParametersListener; +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase; + +class RegisterSynonymsAsTranslationParametersTest extends KernelTestCase +{ + + private RegisterSynonymsAsTranslationParametersListener $listener; + + public function setUp(): void + { + self::bootKernel(); + $this->listener = self::getContainer()->get(RegisterSynonymsAsTranslationParametersListener::class); + } + + public function testGetSynonymPlaceholders(): void + { + $placeholders = $this->listener->getSynonymPlaceholders(); + + $this->assertIsArray($placeholders); + $this->assertSame('Part', $placeholders['{part}']); + $this->assertSame('Parts', $placeholders['{{part}}']); + //Lowercase versions: + $this->assertSame('part', $placeholders['[part]']); + $this->assertSame('parts', $placeholders['[[part]]']); + } +} diff --git a/tests/Services/ElementTypeNameGeneratorTest.php b/tests/Services/ElementTypeNameGeneratorTest.php index f99b0676b..8739dd06e 100644 --- a/tests/Services/ElementTypeNameGeneratorTest.php +++ b/tests/Services/ElementTypeNameGeneratorTest.php @@ -30,20 +30,27 @@ use App\Entity\Parts\Part; use App\Exceptions\EntityNotSupportedException; use App\Services\ElementTypeNameGenerator; +use App\Services\ElementTypes; use App\Services\Formatters\AmountFormatter; +use App\Settings\SynonymSettings; use Symfony\Bundle\FrameworkBundle\Test\WebTestCase; class ElementTypeNameGeneratorTest extends WebTestCase { - /** - * @var AmountFormatter - */ - protected $service; + protected ElementTypeNameGenerator $service; + private SynonymSettings $synonymSettings; protected function setUp(): void { //Get an service instance. $this->service = self::getContainer()->get(ElementTypeNameGenerator::class); + $this->synonymSettings = self::getContainer()->get(SynonymSettings::class); + } + + protected function tearDown(): void + { + //Clean up synonym settings + $this->synonymSettings->typeSynonyms = []; } public function testGetLocalizedTypeNameCombination(): void @@ -84,4 +91,30 @@ public function getIDString(): string } }); } + + public function testTypeLabel(): void + { + //If no synonym is defined, the default label should be used + $this->assertSame('Part', $this->service->typeLabel(Part::class)); + $this->assertSame('Part', $this->service->typeLabel(new Part())); + $this->assertSame('Part', $this->service->typeLabel(ElementTypes::PART)); + $this->assertSame('Part', $this->service->typeLabel('part')); + + //Define a synonym for parts in english + $this->synonymSettings->setSynonymForType(ElementTypes::PART, 'en', 'Singular', 'Plurals'); + $this->assertSame('Singular', $this->service->typeLabel(Part::class)); + } + + public function testTypeLabelPlural(): void + { + //If no synonym is defined, the default label should be used + $this->assertSame('Parts', $this->service->typeLabelPlural(Part::class)); + $this->assertSame('Parts', $this->service->typeLabelPlural(new Part())); + $this->assertSame('Parts', $this->service->typeLabelPlural(ElementTypes::PART)); + $this->assertSame('Parts', $this->service->typeLabelPlural('part')); + + //Define a synonym for parts in english + $this->synonymSettings->setSynonymForType(ElementTypes::PART, 'en', 'Singular', 'Plurals'); + $this->assertSame('Plurals', $this->service->typeLabelPlural(Part::class)); + } } diff --git a/tests/Services/ElementTypesTest.php b/tests/Services/ElementTypesTest.php new file mode 100644 index 000000000..d4fa77ffc --- /dev/null +++ b/tests/Services/ElementTypesTest.php @@ -0,0 +1,79 @@ +. + */ + +namespace App\Tests\Services; + +use App\Entity\Parameters\CategoryParameter; +use App\Entity\Parts\Category; +use App\Exceptions\EntityNotSupportedException; +use App\Services\ElementTypes; +use PHPUnit\Framework\TestCase; + +class ElementTypesTest extends TestCase +{ + + public function testFromClass(): void + { + $this->assertSame(ElementTypes::CATEGORY, ElementTypes::fromClass(Category::class)); + $this->assertSame(ElementTypes::CATEGORY, ElementTypes::fromClass(new Category())); + + //Should also work with subclasses + $this->assertSame(ElementTypes::PARAMETER, ElementTypes::fromClass(CategoryParameter::class)); + $this->assertSame(ElementTypes::PARAMETER, ElementTypes::fromClass(new CategoryParameter())); + } + + public function testFromClassNotExisting(): void + { + $this->expectException(EntityNotSupportedException::class); + ElementTypes::fromClass(\LogicException::class); + } + + public function testFromValue(): void + { + //By enum value + $this->assertSame(ElementTypes::CATEGORY, ElementTypes::fromValue('category')); + $this->assertSame(ElementTypes::ATTACHMENT, ElementTypes::fromValue('attachment')); + + //From enum instance + $this->assertSame(ElementTypes::CATEGORY, ElementTypes::fromValue(ElementTypes::CATEGORY)); + + //From class string + $this->assertSame(ElementTypes::CATEGORY, ElementTypes::fromValue(Category::class)); + $this->assertSame(ElementTypes::PARAMETER, ElementTypes::fromValue(CategoryParameter::class)); + + //From class instance + $this->assertSame(ElementTypes::CATEGORY, ElementTypes::fromValue(new Category())); + $this->assertSame(ElementTypes::PARAMETER, ElementTypes::fromValue(new CategoryParameter())); + } + + public function testGetDefaultLabelKey(): void + { + $this->assertSame('category.label', ElementTypes::CATEGORY->getDefaultLabelKey()); + $this->assertSame('attachment.label', ElementTypes::ATTACHMENT->getDefaultLabelKey()); + } + + public function testGetDefaultPluralLabelKey(): void + { + $this->assertSame('category.labelp', ElementTypes::CATEGORY->getDefaultPluralLabelKey()); + $this->assertSame('attachment.labelp', ElementTypes::ATTACHMENT->getDefaultPluralLabelKey()); + } + + +} diff --git a/tests/Settings/SynonymSettingsTest.php b/tests/Settings/SynonymSettingsTest.php new file mode 100644 index 000000000..2d1407ac9 --- /dev/null +++ b/tests/Settings/SynonymSettingsTest.php @@ -0,0 +1,76 @@ +. + */ + +namespace App\Tests\Settings; + +use App\Services\ElementTypes; +use App\Settings\SynonymSettings; +use App\Tests\SettingsTestHelper; +use PHPUnit\Framework\TestCase; + +class SynonymSettingsTest extends TestCase +{ + + public function testGetSingularSynonymForType(): void + { + $settings = SettingsTestHelper::createSettingsDummy(SynonymSettings::class); + $settings->typeSynonyms['category'] = [ + 'en' => ['singular' => 'Category', 'plural' => 'Categories'], + 'de' => ['singular' => 'Kategorie', 'plural' => 'Kategorien'], + ]; + + $this->assertEquals('Category', $settings->getSingularSynonymForType(ElementTypes::CATEGORY, 'en')); + $this->assertEquals('Kategorie', $settings->getSingularSynonymForType(ElementTypes::CATEGORY, 'de')); + + //If no synonym is defined, it should return null + $this->assertNull($settings->getSingularSynonymForType(ElementTypes::MANUFACTURER, 'en')); + } + + public function testIsSynonymDefinedForType(): void + { + $settings = SettingsTestHelper::createSettingsDummy(SynonymSettings::class); + $settings->typeSynonyms['category'] = [ + 'en' => ['singular' => 'Category', 'plural' => 'Categories'], + 'de' => ['singular' => 'Kategorie', 'plural' => 'Kategorien'], + ]; + + $settings->typeSynonyms['supplier'] = []; + + $this->assertTrue($settings->isSynonymDefinedForType(ElementTypes::CATEGORY)); + $this->assertFalse($settings->isSynonymDefinedForType(ElementTypes::FOOTPRINT)); + $this->assertFalse($settings->isSynonymDefinedForType(ElementTypes::SUPPLIER)); + } + + public function testGetPluralSynonymForType(): void + { + $settings = SettingsTestHelper::createSettingsDummy(SynonymSettings::class); + $settings->typeSynonyms['category'] = [ + 'en' => ['singular' => 'Category', 'plural' => 'Categories'], + 'de' => ['singular' => 'Kategorie',], + ]; + + $this->assertEquals('Categories', $settings->getPluralSynonymForType(ElementTypes::CATEGORY, 'en')); + //Fallback to singular if no plural is defined + $this->assertEquals('Kategorie', $settings->getPluralSynonymForType(ElementTypes::CATEGORY, 'de')); + + //If no synonym is defined, it should return null + $this->assertNull($settings->getPluralSynonymForType(ElementTypes::MANUFACTURER, 'en')); + } +} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index acaee8699..db4370f45 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -14400,5 +14400,17 @@ Please note that this system is currently experimental, and the synonyms defined Parts + + + log.element_edited.changed_fields.part_ipn_prefix + IPN prefix + + + + + part.labelp + Parts + +