Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion config/packages/translation.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
4 changes: 2 additions & 2 deletions config/packages/twig.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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%'
Expand All @@ -20,4 +20,4 @@ twig:

when@test:
twig:
strict_variables: true
strict_variables: true
1 change: 0 additions & 1 deletion config/parameters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,13 @@ services:
$fontDirectory: '%kernel.project_dir%/var/dompdf/fonts/'
$tmpDirectory: '%kernel.project_dir%/var/dompdf/tmp/'

####################################################################################################################
# Twig Extensions
####################################################################################################################

App\Twig\DataSourceNameExtension:
tags: [ 'twig.extension' ]

####################################################################################################################
# Part info provider system
####################################################################################################################
Expand Down
2 changes: 0 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,8 +262,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.
Expand Down
2 changes: 1 addition & 1 deletion src/Controller/ToolsController.php
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ public function systemInfos(GitVersionInfo $versionInfo, DBInfoHelper $DBInfoHel
'default_timezone' => $settings->system->localization->timezone,
'default_currency' => $settings->system->localization->baseCurrency,
'default_theme' => $settings->system->customization->theme,
'enabled_locales' => $this->getParameter('partdb.locale_menu'),
'enabled_locales' => array_column($settings->system->localization->preferredLanguages, 'value'),
'demo_mode' => $this->getParameter('partdb.demo_mode'),
'use_gravatar' => $settings->system->privacy->useGravatar,
'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'),
Expand Down
103 changes: 103 additions & 0 deletions src/Form/Type/DataSourceJsonType.php
Original file line number Diff line number Diff line change
@@ -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;
}
}
6 changes: 3 additions & 3 deletions src/Form/Type/LocaleSelectType.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@

namespace App\Form\Type;

use Symfony\Component\DependencyInjection\Attribute\Autowire;
use App\Settings\SystemSettings\LocalizationSettings;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType;
use Symfony\Component\OptionsResolver\OptionsResolver;
Expand All @@ -35,7 +35,7 @@
class LocaleSelectType extends AbstractType
{

public function __construct(#[Autowire(param: 'partdb.locale_menu')] private readonly array $preferred_languages)
public function __construct(private LocalizationSettings $localizationSetting)
{

}
Expand All @@ -47,7 +47,7 @@ public function getParent(): string
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'preferred_choices' => $this->preferred_languages,
'preferred_choices' => array_column($this->localizationSetting->preferredLanguages, 'value'),
]);
}
}
45 changes: 36 additions & 9 deletions src/Services/Trees/ToolsTreeBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
use App\Entity\UserSystem\User;
use App\Helpers\Trees\TreeViewNode;
use App\Services\Cache\UserCacheKeyGenerator;
use App\Settings\BehaviorSettings\DataSourceSynonymsSettings;
use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Cache\ItemInterface;
Expand All @@ -49,8 +50,14 @@
*/
class ToolsTreeBuilder
{
public function __construct(protected TranslatorInterface $translator, protected UrlGeneratorInterface $urlGenerator, protected TagAwareCacheInterface $cache, protected UserCacheKeyGenerator $keyGenerator, protected Security $security)
{
public function __construct(
protected TranslatorInterface $translator,
protected UrlGeneratorInterface $urlGenerator,
protected TagAwareCacheInterface $cache,
protected UserCacheKeyGenerator $keyGenerator,
protected Security $security,
protected DataSourceSynonymsSettings $dataSourceSynonymsSettings,
) {
}

/**
Expand Down Expand Up @@ -138,7 +145,7 @@ protected function getToolsNode(): array
$this->translator->trans('info_providers.search.title'),
$this->urlGenerator->generate('info_providers_search')
))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down');

$nodes[] = (new TreeViewNode(
$this->translator->trans('info_providers.bulk_import.manage_jobs'),
$this->urlGenerator->generate('bulk_info_provider_manage')
Expand All @@ -165,37 +172,37 @@ protected function getEditNodes(): array
}
if ($this->security->isGranted('read', new Category())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.categories'),
$this->getTranslatedDataSourceOrSynonym('category', 'tree.tools.edit.categories', $this->translator->getLocale()),
$this->urlGenerator->generate('category_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-tags');
}
if ($this->security->isGranted('read', new Project())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.projects'),
$this->getTranslatedDataSourceOrSynonym('project', 'tree.tools.edit.projects', $this->translator->getLocale()),
$this->urlGenerator->generate('project_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-archive');
}
if ($this->security->isGranted('read', new Supplier())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.suppliers'),
$this->getTranslatedDataSourceOrSynonym('supplier', 'tree.tools.edit.suppliers', $this->translator->getLocale()),
$this->urlGenerator->generate('supplier_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-truck');
}
if ($this->security->isGranted('read', new Manufacturer())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.manufacturer'),
$this->getTranslatedDataSourceOrSynonym('manufacturer', 'tree.tools.edit.manufacturer', $this->translator->getLocale()),
$this->urlGenerator->generate('manufacturer_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-industry');
}
if ($this->security->isGranted('read', new StorageLocation())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.storelocation'),
$this->getTranslatedDataSourceOrSynonym('storagelocation', 'tree.tools.edit.storelocation', $this->translator->getLocale()),
$this->urlGenerator->generate('store_location_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-cube');
}
if ($this->security->isGranted('read', new Footprint())) {
$nodes[] = (new TreeViewNode(
$this->translator->trans('tree.tools.edit.footprint'),
$this->getTranslatedDataSourceOrSynonym('footprint', 'tree.tools.edit.footprint', $this->translator->getLocale()),
$this->urlGenerator->generate('footprint_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-microchip');
}
Expand Down Expand Up @@ -303,4 +310,24 @@ protected function getSystemNodes(): array

return $nodes;
}

protected function getTranslatedDataSourceOrSynonym(string $dataSource, string $translationKey, string $locale): string
{
$currentTranslation = $this->translator->trans($translationKey);

$synonyms = $this->dataSourceSynonymsSettings->getSynonymsAsArray();

// Call alternatives from DataSourcesynonyms (if available)
if (!empty($synonyms[$dataSource][$locale])) {
$alternativeTranslation = $synonyms[$dataSource][$locale];

// Use alternative translation when it deviates from the standard translation
if ($alternativeTranslation !== $currentTranslation) {
return $alternativeTranslation;
}
}

// Otherwise return the standard translation
return $currentTranslation;
}
}
36 changes: 30 additions & 6 deletions src/Services/Trees/TreeViewGenerator.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator;
use App\Services\EntityURLGenerator;
use App\Settings\BehaviorSettings\DataSourceSynonymsSettings;
use App\Settings\BehaviorSettings\SidebarSettings;
use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException;
Expand Down Expand Up @@ -67,6 +68,7 @@ public function __construct(
protected TranslatorInterface $translator,
private readonly UrlGeneratorInterface $router,
private readonly SidebarSettings $sidebarSettings,
protected DataSourceSynonymsSettings $dataSourceSynonymsSettings,
) {
$this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled;
$this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded;
Expand Down Expand Up @@ -212,13 +214,15 @@ protected function entityClassToRootNodeHref(string $class): ?string

protected function entityClassToRootNodeString(string $class): string
{
$locale = $this->translator->getLocale();

return match ($class) {
Category::class => $this->translator->trans('category.labelp'),
StorageLocation::class => $this->translator->trans('storelocation.labelp'),
Footprint::class => $this->translator->trans('footprint.labelp'),
Manufacturer::class => $this->translator->trans('manufacturer.labelp'),
Supplier::class => $this->translator->trans('supplier.labelp'),
Project::class => $this->translator->trans('project.labelp'),
Category::class => $this->getTranslatedOrSynonym('category', $locale),
StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale),
Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale),
Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale),
Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale),
Project::class => $this->getTranslatedOrSynonym('project', $locale),
default => $this->translator->trans('tree.root_node.text'),
};
}
Expand Down Expand Up @@ -274,4 +278,24 @@ public function getGenericTree(string $class, ?AbstractStructuralDBElement $pare
return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line
});
}

protected function getTranslatedOrSynonym(string $key, string $locale): string
{
$currentTranslation = $this->translator->trans($key . '.labelp');

$synonyms = $this->dataSourceSynonymsSettings->getSynonymsAsArray();

// Call alternatives from DataSourcesynonyms (if available)
if (!empty($synonyms[$key][$locale])) {
$alternativeTranslation = $synonyms[$key][$locale];

// Use alternative translation when it deviates from the standard translation
if ($alternativeTranslation !== $currentTranslation) {
return $alternativeTranslation;
}
}

// Otherwise return the standard translation
return $currentTranslation;
}
}
Loading