mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-11 05:29:30 +00:00
Entferne alten JSON-basierten Datenquellen-Synonym-Handler
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.
This commit is contained in:
parent
d80ec94227
commit
a8b3dce899
30 changed files with 802 additions and 277 deletions
|
|
@ -64,7 +64,7 @@ class SettingsController extends AbstractController
|
|||
$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'));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ class ToolsController extends AbstractController
|
|||
'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'),
|
||||
|
|
|
|||
|
|
@ -1,103 +0,0 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use App\Settings\BehaviorSettings\DataSourceSynonymsSettings;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextareaType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormEvent;
|
||||
use Symfony\Component\Form\FormEvents;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
|
||||
/**
|
||||
* A form type that generates multiple JSON input fields for different data sources.
|
||||
*/
|
||||
class DataSourceJsonType extends AbstractType
|
||||
{
|
||||
public function __construct(private DataSourceSynonymsSettings $settings)
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$dataSources = $options['data_sources'];
|
||||
$defaultValues = $options['default_values'];
|
||||
$existingData = $options['data'] ?? [];
|
||||
|
||||
if ($existingData === []) {
|
||||
$existingData = $this->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;
|
||||
}
|
||||
}
|
||||
131
src/Form/Type/DataSourceSynonymRowType.php
Normal file
131
src/Form/Type/DataSourceSynonymRowType.php
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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',
|
||||
]);
|
||||
}
|
||||
}
|
||||
180
src/Form/Type/DataSourceSynonymsCollectionType.php
Normal file
180
src/Form/Type/DataSourceSynonymsCollectionType.php
Normal file
|
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Form\Type;
|
||||
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\CallbackTransformer;
|
||||
use Symfony\Component\Form\Extension\Core\Type\CollectionType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormError;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Flat collection of translation rows.
|
||||
* 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
|
||||
{
|
||||
public function __construct(private readonly TranslatorInterface $translator)
|
||||
{
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->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';
|
||||
}
|
||||
}
|
||||
|
|
@ -23,19 +23,18 @@ 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\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 @@ class LocaleSelectType extends AbstractType
|
|||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'preferred_choices' => array_column($this->localizationSetting->preferredLanguages, 'value'),
|
||||
'preferred_choices' => $this->preferred_languages,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
71
src/Services/Misc/DataSourceSynonymResolver.php
Normal file
71
src/Services/Misc/DataSourceSynonymResolver.php
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2023 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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'] ?? ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ use App\Entity\UserSystem\Group;
|
|||
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 @@ class ToolsTreeBuilder
|
|||
protected TagAwareCacheInterface $cache,
|
||||
protected UserCacheKeyGenerator $keyGenerator,
|
||||
protected Security $security,
|
||||
protected DataSourceSynonymsSettings $dataSourceSynonymsSettings,
|
||||
protected readonly DataSourceSynonymResolver $synonymResolver,
|
||||
) {
|
||||
}
|
||||
|
||||
|
|
@ -173,37 +173,60 @@ class ToolsTreeBuilder
|
|||
}
|
||||
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 @@ class ToolsTreeBuilder
|
|||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,11 +34,10 @@ use App\Entity\ProjectSystem\Project;
|
|||
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 @@ class TreeViewGenerator
|
|||
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 @@ class TreeViewGenerator
|
|||
$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 @@ class TreeViewGenerator
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ declare(strict_types=1);
|
|||
|
||||
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<string, array<string, string>> The data source synonyms parsed from JSON to array.
|
||||
* @return array<string, array<string, array{singular: string, plural: string}>>
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,37 +0,0 @@
|
|||
<?php
|
||||
/*
|
||||
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
|
||||
*
|
||||
* Copyright (C) 2019 - 2024 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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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';
|
||||
}
|
||||
|
|
@ -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 getDataSourceName(string $dataSourceName, string $defaultKey): string
|
||||
public function getDataSourceNameSingular(string $dataSourceName, string $defaultKeySingular): string
|
||||
{
|
||||
$locale = $this->translator->getLocale();
|
||||
return $this->resolver->displayNameSingular($dataSourceName, $defaultKeySingular, $this->translator->getLocale());
|
||||
}
|
||||
|
||||
// Use alternative dataSource synonym (if available)
|
||||
if (isset($this->dataSourceSynonyms[$dataSourceName][$locale])) {
|
||||
return $this->dataSourceSynonyms[$dataSourceName][$locale];
|
||||
/**
|
||||
* 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,
|
||||
]);
|
||||
}
|
||||
|
||||
// Otherwise return the standard translation
|
||||
return $this->translator->trans($defaultKey);
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue