Merge remote-tracking branch 'origin/feature/custom-data-source-label' into feature/all-features

# Conflicts:
#	src/Services/Trees/TreeViewGenerator.php
#	templates/components/tree_macros.html.twig
#	translations/messages.cs.xlf
#	translations/messages.da.xlf
#	translations/messages.de.xlf
#	translations/messages.el.xlf
#	translations/messages.en.xlf
#	translations/messages.es.xlf
#	translations/messages.fr.xlf
#	translations/messages.it.xlf
#	translations/messages.ja.xlf
#	translations/messages.nl.xlf
#	translations/messages.pl.xlf
#	translations/messages.ru.xlf
#	translations/messages.zh.xlf
This commit is contained in:
Marcel Diegelmann 2025-10-15 15:17:21 +02:00
commit 3dabf57a67
37 changed files with 691 additions and 49 deletions

View file

@ -1,7 +1,7 @@
framework:
default_locale: 'en'
# Just enable the locales we need for performance reasons.
enabled_locale: '%partdb.locale_menu%'
enabled_locale: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl']
translator:
default_path: '%kernel.project_dir%/translations'
fallbacks:

View file

@ -7,7 +7,7 @@ twig:
globals:
allow_email_pw_reset: '%partdb.users.email_pw_reset%'
locale_menu: '%partdb.locale_menu%'
location_settings: '@App\Settings\SystemSettings\LocalizationSettings'
attachment_manager: '@App\Services\Attachments\AttachmentManager'
label_profile_dropdown_helper: '@App\Services\LabelSystem\LabelProfileDropdownHelper'
error_page_admin_email: '%partdb.error_pages.admin_email%'
@ -20,4 +20,4 @@ twig:
when@test:
twig:
strict_variables: true
strict_variables: true

View file

@ -8,7 +8,6 @@ parameters:
# This is used as workaround for places where we can not access the settings directly (like the 2FA application names)
partdb.title: '%env(string:settings:customization:instanceName)%' # The title shown inside of Part-DB (e.g. in the navbar and on homepage)
partdb.locale_menu: ['en', 'de', 'it', 'fr', 'ru', 'ja', 'cs', 'da', 'zh', 'pl'] # The languages that are shown in user drop down menu
partdb.default_uri: '%env(string:DEFAULT_URI)%' # The default URI to use for the Part-DB instance (e.g. https://part-db.example.com/). This is used for generating links in emails

View file

@ -190,6 +190,13 @@ services:
$fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/'
$tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/'
####################################################################################################################
# Twig Extensions
####################################################################################################################
App\Twig\DataSourceNameExtension:
tags: [ 'twig.extension' ]
####################################################################################################################
# Part info provider system
####################################################################################################################

View file

@ -281,8 +281,6 @@ command `bin/console cache:clear`.
The following options are available:
* `partdb.locale_menu`: The codes of the languages, which should be shown in the language chooser menu (the one with the
user icon in the navbar). The first language in the list will be the default language.
* `partdb.gdpr_compliance`: When set to true (default value), IP addresses which are saved in the database will be
anonymized, by removing the last byte of the IP. This is required by the GDPR (General Data Protection Regulation) in
the EU.

View file

@ -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' => $this->getParameter('partdb.locale_menu'),
'enabled_locales' => array_column($settings->system->localization->preferredLanguages, 'value'),
'demo_mode' => $this->getParameter('partdb.demo_mode'),
'use_gravatar' => $settings->system->privacy->useGravatar,
'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'),

View file

@ -0,0 +1,103 @@
<?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;
}
}

View file

@ -23,7 +23,7 @@ declare(strict_types=1);
namespace App\Form\Type;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use App\Settings\SystemSettings\LocalizationSettings;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
use Symfony\Component\OptionsResolver\OptionsResolver;
@ -35,7 +35,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class LocaleSelectType extends AbstractType
{
public function __construct(#[Autowire(param: 'partdb.locale_menu')] private readonly array $preferred_languages)
public function __construct(private LocalizationSettings $localizationSetting)
{
}
@ -47,7 +47,7 @@ class LocaleSelectType extends AbstractType
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'preferred_choices' => $this->preferred_languages,
'preferred_choices' => array_column($this->localizationSetting->preferredLanguages, 'value'),
]);
}
}

View file

@ -39,6 +39,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 Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Cache\ItemInterface;
@ -51,8 +52,14 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/
class ToolsTreeBuilder
{
public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security)
{
public function __construct(
protected TranslatorInterface $translator,
protected UrlGeneratorInterface $urlGenerator,
protected TagAwareCacheInterface $cache,
protected UserCacheKeyGenerator $keyGenerator,
protected Security $security,
protected DataSourceSynonymsSettings $dataSourceSynonymsSettings,
) {
}
/**
@ -167,13 +174,13 @@ class ToolsTreeBuilder
}
if ($this->security->isGranted('read', new Category())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.categories'),
$this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()),
$this->urlGenerator->generate('category_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-tags');
}
if ($this->security->isGranted('read', new Project())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.projects'),
$this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()),
$this->urlGenerator->generate('project_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-archive');
}
@ -185,25 +192,25 @@ class ToolsTreeBuilder
}
if ($this->security->isGranted('read', new Supplier())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.suppliers'),
$this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()),
$this->urlGenerator->generate('supplier_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-truck');
}
if ($this->security->isGranted('read', new Manufacturer())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.manufacturer'),
$this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()),
$this->urlGenerator->generate('manufacturer_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-industry');
}
if ($this->security->isGranted('read', new StorageLocation())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.storelocation'),
$this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()),
$this->urlGenerator->generate('store_location_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-cube');
}
if ($this->security->isGranted('read', new Footprint())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.footprint'),
$this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()),
$this->urlGenerator->generate('footprint_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-microchip');
}
@ -317,4 +324,24 @@ 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;
}
}

View file

@ -39,6 +39,7 @@ 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\Settings\BehaviorSettings\SidebarSettings;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
@ -68,6 +69,7 @@ class TreeViewGenerator
protected TranslatorInterface $translator,
private readonly UrlGeneratorInterface $router,
private readonly SidebarSettings $sidebarSettings,
protected DataSourceSynonymsSettings $dataSourceSynonymsSettings,
) {
$this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled;
$this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded;
@ -226,14 +228,15 @@ class TreeViewGenerator
protected function entityClassToRootNodeString(string $class): string
{
$locale = $this->translator->getLocale();
return match ($class) {
Category::class => $this->translator->trans('category.labelp'),
StorageLocation::class => $this->translator->trans('storelocation.labelp'),
Footprint::class => $this->translator->trans('footprint.labelp'),
Manufacturer::class => $this->translator->trans('manufacturer.labelp'),
Supplier::class => $this->translator->trans('supplier.labelp'),
Project::class => $this->translator->trans('project.labelp'),
Assembly::class => $this->translator->trans('assembly.labelp'),
Category::class => $this->getTranslatedOrSynonym('category', $locale),
StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale),
Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale),
Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale),
Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale),
Project::class => $this->getTranslatedOrSynonym('project', $locale),
default => $this->translator->trans('tree.root_node.text'),
};
}
@ -290,4 +293,24 @@ 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;
}
}

View file

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace App\Settings\BehaviorSettings;
use App\Form\Type\DataSourceJsonType;
use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\ParameterTypes\ArrayType;
use Jbtronics\SettingsBundle\ParameterTypes\StringType;
use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Translation\TranslatableMessage as TM;
#[Settings(label: new TM("settings.system.data_source_synonyms"))]
#[SettingsIcon("fa-language")]
class DataSourceSynonymsSettings
{
use SettingsTrait;
#[SettingsParameter(ArrayType::class,
label: new TM("settings.system.data_source_synonyms.configuration"),
description: new TM("settings.system.data_source_synonyms.configuration.help", ['%format%' => '{"en":"", "de":""}']),
options: ['type' => StringType::class],
formType: DataSourceJsonType::class,
formOptions: [
'required' => false,
'data_sources' => [
'category' => new TM("settings.behavior.data_source_synonyms.category"),
'storagelocation' => new TM("settings.behavior.data_source_synonyms.storagelocation"),
'footprint' => new TM("settings.behavior.data_source_synonyms.footprint"),
'manufacturer' => new TM("settings.behavior.data_source_synonyms.manufacturer"),
'supplier' => new TM("settings.behavior.data_source_synonyms.supplier"),
'project' => new TM("settings.behavior.data_source_synonyms.project"),
],
'default_values' => [
'category' => '{"en":"Categories", "de":"Kategorien"}',
'storagelocation' => '{"en":"Storage locations", "de":"Lagerorte"}',
'footprint' => '{"en":"Footprints", "de":"Footprints"}',
'manufacturer' => '{"en":"Manufacturers", "de":"Hersteller"}',
'supplier' => '{"en":"Suppliers", "de":"Lieferanten"}',
'project' => '{"en":"Projects", "de":"Projekte"}',
],
],
)]
#[Assert\Type('array')]
public array $dataSourceSynonyms = [
'category' => '{"en":"Categories", "de":"Kategorien"}',
'storagelocation' => '{"en":"Storage locations", "de":"Lagerorte"}',
'footprint' => '{"en":"Footprints", "de":"Footprints"}',
'manufacturer' => '{"en":"Manufacturers", "de":"Hersteller"}',
'supplier' => '{"en":"Suppliers", "de":"Lieferanten"}',
'project' => '{"en":"Projects", "de":"Projekte"}',
];
/**
* Get the synonyms data as a structured array.
*
* @return array<string, array<string, string>> The data source synonyms parsed from JSON to array.
*/
public function getSynonymsAsArray(): array
{
$result = [];
foreach ($this->dataSourceSynonyms as $key => $jsonString) {
$result[$key] = json_decode($jsonString, true, 512, JSON_THROW_ON_ERROR) ?? [];
}
return $result;
}
}

View file

@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\Settings;
use App\Settings\BehaviorSettings\DataSourceSynonymsSettings;
use App\Settings\SystemSettings\AttachmentsSettings;
use App\Settings\SystemSettings\CustomizationSettings;
use App\Settings\SystemSettings\HistorySettings;
@ -37,6 +38,9 @@ class SystemSettings
#[EmbeddedSettings()]
public ?LocalizationSettings $localization = null;
#[EmbeddedSettings]
public ?DataSourceSynonymsSettings $dataSourceSynonyms = null;
#[EmbeddedSettings()]
public ?CustomizationSettings $customization = null;
@ -48,4 +52,4 @@ class SystemSettings
#[EmbeddedSettings()]
public ?HistorySettings $history = null;
}
}

View file

@ -26,6 +26,8 @@ namespace App\Settings\SystemSettings;
use App\Form\Type\LocaleSelectType;
use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\ParameterTypes\ArrayType;
use Jbtronics\SettingsBundle\ParameterTypes\EnumType;
use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
@ -60,4 +62,19 @@ class LocalizationSettings
envVar: "string:BASE_CURRENCY", envVarMode: EnvVarMode::OVERWRITE
)]
public string $baseCurrency = 'EUR';
}
/** @var PreferredLocales[] */
#[SettingsParameter(ArrayType::class,
label: new TM("settings.system.localization.preferred_languages"),
description: new TM("settings.system.localization.preferred_languages.help"),
options: ['type' => EnumType::class, 'options' => ['class' => PreferredLocales::class]],
formType: \Symfony\Component\Form\Extension\Core\Type\EnumType::class,
formOptions: ['class' => PreferredLocales::class, 'multiple' => true, 'ordered' => true]
)]
#[Assert\NotBlank()]
#[Assert\Unique()]
#[Assert\All([new Assert\Type(PreferredLocales::class)])]
public array $preferredLanguages = [PreferredLocales::EN, PreferredLocales::DE,
PreferredLocales::IT, PreferredLocales::FR, PreferredLocales::RU, PreferredLocales::JA,
PreferredLocales::CS, PreferredLocales::DA, PreferredLocales::ZH, PreferredLocales::PL];
}

View file

@ -0,0 +1,37 @@
<?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';
}

View file

@ -0,0 +1,43 @@
<?php
namespace App\Twig;
use App\Settings\BehaviorSettings\DataSourceSynonymsSettings;
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 getFunctions(): array
{
return [
new TwigFunction('get_data_source_name', [$this, 'getDataSourceName']),
];
}
/**
* Based on the locale and data source names, gives the right synonym value back or the default translator value.
*/
public function getDataSourceName(string $dataSourceName, string $defaultKey): string
{
$locale = $this->translator->getLocale();
// Use alternative dataSource synonym (if available)
if (isset($this->dataSourceSynonyms[$dataSourceName][$locale])) {
return $this->dataSourceSynonyms[$dataSourceName][$locale];
}
// Otherwise return the standard translation
return $this->translator->trans($defaultKey);
}
}

View file

@ -22,9 +22,9 @@
<div class="d-none" data-title="{{ current_page_title|trim|raw }}" {{ stimulus_controller('turbo/title') }}></div>
<div class="d-none" {{ stimulus_controller('turbo/locale_menu') }}>
{% for locale in locale_menu %}
{% for locale in location_settings.preferredLanguages %}
<a class="dropdown-item" data-turbo="false" data-turbo-frame="_top" href="{{ path(app.request.attributes.get('_route'),
app.request.query.all|merge(app.request.attributes.get('_route_params'))|merge({'_locale': locale})) }}">
{{ locale|language_name }} ({{ locale|upper }})</a>
app.request.query.all|merge(app.request.attributes.get('_route_params'))|merge({'_locale': locale.value})) }}">
{{ locale.value|language_name }} ({{ locale.value|upper }})</a>
{% endfor %}
</div>
</div>

View file

@ -1,7 +1,9 @@
{% extends "admin/base_admin.html.twig" %}
{% block card_title %}
<i class="fas fa-tags fa-fw"></i> {% trans %}category.labelp{% endtrans %}
{% 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 %}
{% endblock %}
{% block additional_pills %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_admin.html.twig" %}
{% block card_title %}
<i class="fas fa-microchip fa-fw"></i> {% trans %}footprint.labelp{% endtrans %}
{% 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 %}
{% endblock %}
{% block master_picture_block %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_company_admin.html.twig" %}
{% block card_title %}
<i class="fas fa-industry fa-fw"></i> {% trans %}manufacturer.caption{% endtrans %}
{% 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 %}
{% endblock %}
{% block edit_title %}

View file

@ -3,7 +3,9 @@
{# @var entity App\Entity\ProjectSystem\Project #}
{% block card_title %}
<i class="fas fa-archive fa-fw"></i> {% trans %}project.caption{% endtrans %}
{% 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 %}
{% endblock %}
{% block edit_title %}

View file

@ -2,7 +2,9 @@
{% import "label_system/dropdown_macro.html.twig" as dropdown %}
{% block card_title %}
<i class="fas fa-cube fa-fw"></i> {% trans %}storelocation.labelp{% endtrans %}
{% 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 %}
{% endblock %}
{% block additional_controls %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_company_admin.html.twig" %}
{% block card_title %}
<i class="fas fa-truck fa-fw"></i> {% trans %}supplier.caption{% endtrans %}
{% 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 %}
{% endblock %}
{% block additional_panes %}

View file

@ -1,14 +1,15 @@
{% macro sidebar_dropdown() %}
{% set currentLocale = app.request.locale %}
{# Format is [mode, route, label, show_condition] #}
{% set data_sources = [
['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read')],
['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read')],
['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read')],
['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read')],
['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read')],
['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read')],
['assembly', path('tree_assembly_root'), 'assembly.labelp', is_granted('@assemblies.read')],
['tools', path('tree_tools'), 'tools.label', true],
['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read'), 'category'],
['locations', path('tree_location_root'), 'storelocation.labelp', is_granted('@storelocations.read') and is_granted('@parts.read'), 'storagelocation'],
['footprints', path('tree_footprint_root'), 'footprint.labelp', is_granted('@footprints.read') and is_granted('@parts.read'), 'footprint'],
['manufacturers', path('tree_manufacturer_root'), 'manufacturer.labelp', is_granted('@manufacturers.read') and is_granted('@parts.read'), 'manufacturer'],
['suppliers', path('tree_supplier_root'), 'supplier.labelp', is_granted('@suppliers.read') and is_granted('@parts.read'), 'supplier'],
['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read'), 'project'],
['tools', path('tree_tools'), 'tools.label', true, 'tool'],
] %}
<li class="dropdown-header">{% trans %}actions{% endtrans %}</li>
@ -19,9 +20,9 @@
{% 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="{{ source[2] | trans }}"
<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]) }}"
{{ stimulus_action('elements/sidebar_tree', 'changeDataSource') }}
>{{ source[2] | trans }}</button></li>
>{{ get_data_source_name(source[4], source[2]) }}</button></li>
{% endif %}
{% endfor %}
{% endmacro %}

View file

@ -6,12 +6,34 @@
<div class="form-check">
<input type="checkbox" class="form-check-input tristate permission-checkbox permission_multicheckbox" id="mulit_check_{{ form.vars.label }}">
<label class="form-check-label" for="mulit_check_{{ form.vars.label }}">
<b>{{ form.vars.label | trans }}</b>
{% set dataSource = '' %}
{% if (form.vars.label == 'perm.part.categories') %}
{% set dataSource = 'category' %}
{% elseif (form.vars.label == 'perm.storelocations') %}
{% set dataSource = 'storagelocation' %}
{% elseif (form.vars.label == 'perm.part.footprints') %}
{% set dataSource = 'footprint' %}
{% elseif (form.vars.label == 'perm.part.manufacturers') %}
{% set dataSource = 'manufacturer' %}
{% elseif (form.vars.label == 'perm.part.supplier') %}
{% set dataSource = 'supplier' %}
{% elseif (form.vars.label == 'perm.projects') %}
{% set dataSource = 'project' %}
{% endif %}
{% set dataSourceName = get_data_source_name(dataSource, form.vars.label) %}
{% set translatedSource = form.vars.label|trans %}
{% if dataSourceName != translatedSource %}
{{ translatedSource }}
<i class="fas fa-fw fa-info" title="{{ 'datasource.synonym'|trans({'%name%': '', '%synonym%': dataSourceName })|raw }}"></i>
{% else %}
{{ translatedSource }}
{% endif %}
</label>
</div>
{% else %}
<b>{{ form.vars.label | trans }}</b>
<b>def{{ form.vars.label | trans }}</b>
{% endif %}

View file

@ -13186,6 +13186,72 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
&lt;b&gt;Upozorňujeme, že při změně této hodnoty nedochází k převodu měn. Změna výchozí měny po přidání informací o cenách tedy povede k nesprávným cenám!&lt;/b&gt;</target>
</segment>
</unit>
<unit id="ferwk9E" name="settings.system.localization.preferred_languages">
<segment state="translated">
<source>settings.system.localization.preferred_languages</source>
<target>Preferované jazyky</target>
</segment>
</unit>
<unit id="idme4VU" name="settings.system.localization.preferred_languages.help">
<segment state="translated">
<source>settings.system.localization.preferred_languages.help</source>
<target>Jazyky, které se zobrazují v uživatelské rozbalovací nabídce</target>
</segment>
</unit>
<unit id="o6zcqUt" name="settings.system.data_source_synonyms">
<segment state="translated">
<source>settings.system.data_source_synonyms</source>
<target>Synonyma zdrojů dat</target>
</segment>
</unit>
<unit id="kue6Tga" name="settings.system.data_source_synonyms.configuration">
<segment state="translated">
<source>settings.system.data_source_synonyms.configuration</source>
<target>Zdroj</target>
</segment>
</unit>
<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>
</segment>
</unit>
<unit id="kd8nBt4" name="settings.behavior.data_source_synonyms.category">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.category</source>
<target>Kategorie</target>
</segment>
</unit>
<unit id="id3hnbC" name="settings.behavior.data_source_synonyms.storagelocation">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.storagelocation</source>
<target>Skladové umístění</target>
</segment>
</unit>
<unit id="kdz5RcV" name="settings.behavior.data_source_synonyms.footprint">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.footprint</source>
<target>Pouzdro</target>
</segment>
</unit>
<unit id="uBeDc7l" name="settings.behavior.data_source_synonyms.manufacturer">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.manufacturer</source>
<target>Výrobce</target>
</segment>
</unit>
<unit id="OewynB4" name="settings.behavior.data_source_synonyms.supplier">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.supplier</source>
<target>Dodavatel</target>
</segment>
</unit>
<unit id="n7ze4lk" name="settings.behavior.data_source_synonyms.project">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.project</source>
<target>Projekt</target>
</segment>
</unit>
<unit id="cvpTUeY" name="settings.system.privacy">
<segment state="translated">
<source>settings.system.privacy</source>
@ -15135,5 +15201,11 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
<target>Upravit</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Váš synonymum: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13732,5 +13732,11 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
<target>Rediger</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Dit synonym: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13284,6 +13284,72 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
&lt;b&gt;Bitte beachten Sie, dass die Währungen bei einer Änderung dieses Wertes nicht umgerechnet werden. Wenn Sie also die Basiswährung ändern, nachdem Sie bereits Preisinformationen hinzugefügt haben, führt dies zu falschen Preisen!&lt;/b&gt;</target>
</segment>
</unit>
<unit id="ferwk9E" name="settings.system.localization.preferred_languages">
<segment state="translated">
<source>settings.system.localization.preferred_languages</source>
<target>Bevorzugte Sprachen</target>
</segment>
</unit>
<unit id="idme4VU" name="settings.system.localization.preferred_languages.help">
<segment state="translated">
<source>settings.system.localization.preferred_languages.help</source>
<target>Die Sprachen, die im Dropdown-Menü der Nutzer angezeigt werden</target>
</segment>
</unit>
<unit id="o6zcqUt" name="settings.system.data_source_synonyms">
<segment state="translated">
<source>settings.system.data_source_synonyms</source>
<target>Datenquellen-Synonyme</target>
</segment>
</unit>
<unit id="kue6Tga" name="settings.system.data_source_synonyms.configuration">
<segment state="translated">
<source>settings.system.data_source_synonyms.configuration</source>
<target>Quelle</target>
</segment>
</unit>
<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>
</segment>
</unit>
<unit id="kd8nBt4" name="settings.behavior.data_source_synonyms.category">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.category</source>
<target>Kategorie</target>
</segment>
</unit>
<unit id="id3hnbC" name="settings.behavior.data_source_synonyms.storagelocation">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.storagelocation</source>
<target>Lagerort</target>
</segment>
</unit>
<unit id="kdz5RcV" name="settings.behavior.data_source_synonyms.footprint">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.footprint</source>
<target>Footprint</target>
</segment>
</unit>
<unit id="uBeDc7l" name="settings.behavior.data_source_synonyms.manufacturer">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.manufacturer</source>
<target>Hersteller</target>
</segment>
</unit>
<unit id="OewynB4" name="settings.behavior.data_source_synonyms.supplier">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.supplier</source>
<target>Lieferant</target>
</segment>
</unit>
<unit id="n7ze4lk" name="settings.behavior.data_source_synonyms.project">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.project</source>
<target>Projekt</target>
</segment>
</unit>
<unit id="cvpTUeY" name="settings.system.privacy">
<segment state="translated">
<source>settings.system.privacy</source>
@ -15809,5 +15875,11 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<target>Bearbeiten</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Ihr Synonym: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -3645,5 +3645,11 @@
<target>Επεξεργασία</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Το συνώνυμό σας: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13285,6 +13285,72 @@ Please note, that you can not impersonate a disabled user. If you try you will g
&lt;b&gt;Please note that the currencies are not converted, when changing this value. So changing the default currency after you already added price information, will result in wrong prices!&lt;/b&gt;</target>
</segment>
</unit>
<unit id="ferwk9E" name="settings.system.localization.preferred_languages">
<segment state="translated">
<source>settings.system.localization.preferred_languages</source>
<target>Preferred languages</target>
</segment>
</unit>
<unit id="idme4VU" name="settings.system.localization.preferred_languages.help">
<segment state="translated">
<source>settings.system.localization.preferred_languages.help</source>
<target>The languages that are shown in user drop down menu</target>
</segment>
</unit>
<unit id="o6zcqUt" name="settings.system.data_source_synonyms">
<segment state="translated">
<source>settings.system.data_source_synonyms</source>
<target>Data source synonyms</target>
</segment>
</unit>
<unit id="kue6Tga" name="settings.system.data_source_synonyms.configuration">
<segment state="translated">
<source>settings.system.data_source_synonyms.configuration</source>
<target>Source</target>
</segment>
</unit>
<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>
</segment>
</unit>
<unit id="kd8nBt4" name="settings.behavior.data_source_synonyms.category">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.category</source>
<target>Category</target>
</segment>
</unit>
<unit id="id3hnbC" name="settings.behavior.data_source_synonyms.storagelocation">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.storagelocation</source>
<target>Storage location</target>
</segment>
</unit>
<unit id="kdz5RcV" name="settings.behavior.data_source_synonyms.footprint">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.footprint</source>
<target>Footprint</target>
</segment>
</unit>
<unit id="uBeDc7l" name="settings.behavior.data_source_synonyms.manufacturer">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.manufacturer</source>
<target>Manufacturer</target>
</segment>
</unit>
<unit id="OewynB4" name="settings.behavior.data_source_synonyms.supplier">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.supplier</source>
<target>Supplier</target>
</segment>
</unit>
<unit id="n7ze4lk" name="settings.behavior.data_source_synonyms.project">
<segment state="translated">
<source>settings.behavior.data_source_synonyms.project</source>
<target>Project</target>
</segment>
</unit>
<unit id="cvpTUeY" name="settings.system.privacy">
<segment state="translated">
<source>settings.system.privacy</source>
@ -15810,5 +15876,11 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<target>Edit</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Your synonym: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13904,5 +13904,11 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S
<target>Editar</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Tu sinónimo: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -10314,5 +10314,11 @@ exemple de ville</target>
<target>État personnalisé de la pièce</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Votre synonyme : %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13906,5 +13906,11 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a
<target>Modifica</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Il tuo sinonimo: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -10039,5 +10039,11 @@ Exampletown</target>
<target>部品のカスタム状態</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (あなたの同義語: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -1959,5 +1959,11 @@
<target>Maak eerst een component en wijs het toe aan een categorie: met de bestaande categorieën en hun eigen IPN-prefixen kan de IPN voor het component automatisch worden voorgesteld</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Uw synoniem: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13759,5 +13759,11 @@ Należy pamiętać, że nie możesz udawać nieaktywnych użytkowników. Jeśli
<target>Edytuj</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Twój synonim: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13859,5 +13859,11 @@
<target>Редактировать</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Ваш синоним: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>

View file

@ -13744,5 +13744,11 @@ Element 3</target>
<target>编辑</target>
</segment>
</unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (您的同义词: %synonym%)</target>
</segment>
</unit>
</file>
</xliff>