mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-12-06 11:09:29 +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
|
|
@ -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 <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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 || '';
|
||||
}
|
||||
}
|
||||
|
|
@ -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%'
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
// Use alternative dataSource synonym (if available)
|
||||
if (isset($this->dataSourceSynonyms[$dataSourceName][$locale])) {
|
||||
return $this->dataSourceSynonyms[$dataSourceName][$locale];
|
||||
return $this->resolver->displayNameSingular($dataSourceName, $defaultKeySingular, $this->translator->getLocale());
|
||||
}
|
||||
|
||||
// Otherwise return the standard translation
|
||||
return $this->translator->trans($defaultKey);
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
<i class="fas fa-tags fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
|
||||
<i class="fas fa-tags fa-fw"></i>
|
||||
{{ data_source_name_with_hint('category', 'category.label') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_pills %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
<i class="fas fa-microchip fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
|
||||
<i class="fas fa-microchip fa-fw"></i>
|
||||
{{ data_source_name_with_hint('footprint', 'footprint.labelp') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block master_picture_block %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
<i class="fas fa-industry fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
|
||||
<i class="fas fa-industry fa-fw"></i>
|
||||
{{ data_source_name_with_hint('manufacturer', 'manufacturer.caption') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block edit_title %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
<i class="fas fa-archive fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
|
||||
<i class="fas fa-archive fa-fw"></i>
|
||||
{{ data_source_name_with_hint('project', 'project.labelp') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block edit_title %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
<i class="fas fa-cube fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
|
||||
<i class="fas fa-cube fa-fw"></i>
|
||||
{{ data_source_name_with_hint('storagelocation', 'storelocation.labelp') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_controls %}
|
||||
|
|
|
|||
|
|
@ -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 %}
|
||||
<i class="fas fa-truck fa-fw"></i> {% if dataSourceName != translatedSource %}{{ 'datasource.synonym'|trans({'%name%': translatedSource, '%synonym%': dataSourceName}) }}{% else %}{{ translatedSource }}{% endif %}
|
||||
<i class="fas fa-truck fa-fw"></i>
|
||||
{{ data_source_name_with_hint('supplier', 'supplier.labelp') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block additional_panes %}
|
||||
|
|
|
|||
|
|
@ -20,9 +20,14 @@
|
|||
|
||||
{% for source in data_sources %}
|
||||
{% if source[3] %} {# show_condition #}
|
||||
<li><button class="tree-btns dropdown-item" data-mode="{{ source[0] }}" data-url="{{ source[1] }}" data-text="{{ get_data_source_name(source[4], source[2]) }}"
|
||||
<li>
|
||||
<button class="tree-btns dropdown-item"
|
||||
data-mode="{{ source[0] }}"
|
||||
data-url="{{ source[1] }}"
|
||||
data-text="{{ get_data_source_name_plural(source[4], source[2]) }}"
|
||||
{{ stimulus_action('elements/sidebar_tree', 'changeDataSource') }}
|
||||
>{{ get_data_source_name(source[4], source[2]) }}</button></li>
|
||||
>{{ get_data_source_name_plural(source[4], source[2]) }}</button>
|
||||
</li>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
{% endmacro %}
|
||||
|
|
|
|||
48
templates/form/datasource_synonyms_collection.html.twig
Normal file
48
templates/form/datasource_synonyms_collection.html.twig
Normal file
|
|
@ -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 %}
|
||||
<div class="tc-item mb-2 border rounded p-2">
|
||||
{{ form_widget(prototype) }}
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm tc-remove" data-action="elements--datasource-synonyms-collection#remove">
|
||||
<i class="fa fa-trash"></i> {{ 'settings.behavior.data_source_synonyms.collection.remove_entry'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endset %}
|
||||
{% set _proto_html = __proto|e('html_attr') %}
|
||||
{% set _proto_name = form.vars.prototype_name|default('__name__') %}
|
||||
{% set _index = form|length %}
|
||||
{% endif %}
|
||||
|
||||
<div
|
||||
{{ stimulus_controller('elements/datasource_synonyms_collection', {
|
||||
prototype: has_proto ? _proto_html : '',
|
||||
prototypeName: has_proto ? _proto_name : '__name__',
|
||||
index: has_proto ? _index : (form|length)
|
||||
}) }}
|
||||
{{ block('widget_container_attributes')|raw }}{% for k,v in _attrs %} {{ k }}="{{ v }}"{% endfor %}
|
||||
>
|
||||
<div class="tc-items" data-elements--datasource-synonyms-collection-target="items">
|
||||
{% for child in form %}
|
||||
<div class="tc-item mb-2 border rounded p-2">
|
||||
{{ form_widget(child) }}
|
||||
<div class="mt-2">
|
||||
<button type="button" class="btn btn-outline-danger btn-sm tc-remove" data-action="elements--datasource-synonyms-collection#remove">
|
||||
<i class="fa fa-trash"></i> {{ 'settings.behavior.data_source_synonyms.collection.remove_entry'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
<button type="button" class="btn btn-outline-primary btn-sm mt-2 tc-add" data-action="elements--datasource-synonyms-collection#add">
|
||||
<i class="fa fa-plus"></i> {{ 'settings.behavior.data_source_synonyms.collection.add_entry'|trans }}
|
||||
</button>
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
@ -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 }}
|
||||
|
|
|
|||
|
|
@ -12828,7 +12828,7 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
|
|||
<unit id="lEcz6N1" name="settings.system.data_source_synonyms.configuration.help">
|
||||
<segment state="translated">
|
||||
<source>settings.system.data_source_synonyms.configuration.help</source>
|
||||
<target>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%.</target>
|
||||
<target>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é.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kd8nBt4" name="settings.behavior.data_source_synonyms.category">
|
||||
|
|
@ -12867,6 +12867,42 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
|
|||
<target>Projekt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="dGuR4cl" name="settings.behavior.data_source_synonyms.collection.add_entry">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.collection.add_entry</source>
|
||||
<target>Přidat položku</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="nctZn5c" name="settings.behavior.data_source_synonyms.collection.remove_entry">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.collection.remove_entry</source>
|
||||
<target>Odebrat položku</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kdoci8c" name="settings.behavior.data_source_synonyms.row_type.form.datasource">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.datasource</source>
|
||||
<target>Zdroj dat</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cjcUeb3" name="settings.behavior.data_source_synonyms.row_type.form.locale">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.locale</source>
|
||||
<target>Místní nastavení</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jcueBn3" name="settings.behavior.data_source_synonyms.row_type.form.translation_singular">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.translation_singular</source>
|
||||
<target>Překlad (jednotné číslo)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ncud7Rc" name="settings.behavior.data_source_synonyms.row_type.form.translation_plural">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.translation_plural</source>
|
||||
<target>Překlad (množné číslo)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cvpTUeY" name="settings.system.privacy">
|
||||
<segment state="translated">
|
||||
<source>settings.system.privacy</source>
|
||||
|
|
|
|||
|
|
@ -12896,7 +12896,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
|||
<unit id="lEcz6N1" name="settings.system.data_source_synonyms.configuration.help">
|
||||
<segment state="translated">
|
||||
<source>settings.system.data_source_synonyms.configuration.help</source>
|
||||
<target>Definieren Sie Ihre eigenen Synonyme für die angegebenen Datenquellen. Erwartet wird ein JSON-Format mit Ihren bevorzugten Sprache-ISO-Codes. Beispiel: %format%.</target>
|
||||
<target>Definieren Sie Ihre eigenen Synonyme für die angegebenen Datenquellen. Datenquelle, Sprache und Übersetzungen frei hinzufügen; Nicht verwendete Sprachen bleiben leer.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kd8nBt4" name="settings.behavior.data_source_synonyms.category">
|
||||
|
|
@ -12935,6 +12935,42 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
|
|||
<target>Projekt</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="dGuR4cl" name="settings.behavior.data_source_synonyms.collection.add_entry">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.collection.add_entry</source>
|
||||
<target>Eintrag hinzufügen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="nctZn5c" name="settings.behavior.data_source_synonyms.collection.remove_entry">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.collection.remove_entry</source>
|
||||
<target>Eintrag entfernen</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kdoci8c" name="settings.behavior.data_source_synonyms.row_type.form.datasource">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.datasource</source>
|
||||
<target>Datenquelle</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cjcUeb3" name="settings.behavior.data_source_synonyms.row_type.form.locale">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.locale</source>
|
||||
<target>Sprache</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jcueBn3" name="settings.behavior.data_source_synonyms.row_type.form.translation_singular">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.translation_singular</source>
|
||||
<target>Übersetzung (Singular)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ncud7Rc" name="settings.behavior.data_source_synonyms.row_type.form.translation_plural">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.translation_plural</source>
|
||||
<target>Übersetzung (Plural)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cvpTUeY" name="settings.system.privacy">
|
||||
<segment state="translated">
|
||||
<source>settings.system.privacy</source>
|
||||
|
|
|
|||
|
|
@ -12897,7 +12897,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<unit id="lEcz6N1" name="settings.system.data_source_synonyms.configuration.help">
|
||||
<segment state="translated">
|
||||
<source>settings.system.data_source_synonyms.configuration.help</source>
|
||||
<target>Define your own synonyms for the given data sources. Expected in JSON-format with your preferred language iso-codes. Example: %format%.</target>
|
||||
<target>Define your own synonyms for the given data sources. Add data source, language and translations freely; unused languages remain empty.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kd8nBt4" name="settings.behavior.data_source_synonyms.category">
|
||||
|
|
@ -12936,6 +12936,42 @@ Please note, that you can not impersonate a disabled user. If you try you will g
|
|||
<target>Project</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="dGuR4cl" name="settings.behavior.data_source_synonyms.collection.add_entry">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.collection.add_entry</source>
|
||||
<target>Add entry</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="nctZn5c" name="settings.behavior.data_source_synonyms.collection.remove_entry">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.collection.remove_entry</source>
|
||||
<target>Remove entry</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kdoci8c" name="settings.behavior.data_source_synonyms.row_type.form.datasource">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.datasource</source>
|
||||
<target>Data source</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cjcUeb3" name="settings.behavior.data_source_synonyms.row_type.form.locale">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.locale</source>
|
||||
<target>Locale</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="jcueBn3" name="settings.behavior.data_source_synonyms.row_type.form.translation_singular">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.translation_singular</source>
|
||||
<target>Translation singular</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="ncud7Rc" name="settings.behavior.data_source_synonyms.row_type.form.translation_plural">
|
||||
<segment state="translated">
|
||||
<source>settings.behavior.data_source_synonyms.row_type.form.translation_plural</source>
|
||||
<target>Translation plural</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="cvpTUeY" name="settings.system.privacy">
|
||||
<segment state="translated">
|
||||
<source>settings.system.privacy</source>
|
||||
|
|
|
|||
|
|
@ -365,5 +365,17 @@
|
|||
<target>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í.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kduT6bD" name="settings.system.data_source_synonyms.row_type.value_not_blank">
|
||||
<segment state="translated">
|
||||
<source>settings.system.data_source_synonyms.row_type.value_not_blank</source>
|
||||
<target>Tato hodnota nemůže být prázdná.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="djd7Z5r" name="settings.system.data_source_synonyms.collection_type.duplicate">
|
||||
<segment state="translated">
|
||||
<source>settings.system.data_source_synonyms.collection_type.duplicate</source>
|
||||
<target>Dvojitá kombinace zdroje dat a jazyka.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
|||
|
|
@ -365,5 +365,17 @@
|
|||
<target>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.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kduT6bD" name="settings.system.data_source_synonyms.row_type.value_not_blank">
|
||||
<segment state="translated">
|
||||
<source>settings.system.data_source_synonyms.row_type.value_not_blank</source>
|
||||
<target>Dieser Wert darf nicht leer sein.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="djd7Z5r" name="settings.system.data_source_synonyms.collection_type.duplicate">
|
||||
<segment state="translated">
|
||||
<source>settings.system.data_source_synonyms.collection_type.duplicate</source>
|
||||
<target>Doppelte Kombination aus Datenquelle und Sprache.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
|||
|
|
@ -365,5 +365,17 @@
|
|||
<target>Invalid code. Check that your authenticator app is set up correctly and that both the server and authentication device has the time set correctly.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="kduT6bD" name="settings.system.data_source_synonyms.row_type.value_not_blank">
|
||||
<segment state="translated">
|
||||
<source>settings.system.data_source_synonyms.row_type.value_not_blank</source>
|
||||
<target>This value should not be blank.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="djd7Z5r" name="settings.system.data_source_synonyms.collection_type.duplicate">
|
||||
<segment state="translated">
|
||||
<source>settings.system.data_source_synonyms.collection_type.duplicate</source>
|
||||
<target>Duplicate combination of data source and locale.</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue