Implementiere bevorzugte Sprachauswahl und Datenquellen-Synonyme

Die Spracheinstellungen/System-Settings wurden um die Möglichkeit ergänzt, bevorzugte Sprachen für die Dropdown-Menüs festzulegen. Zudem wurde ein Datenquellen-Synonymsystem implementiert, um benutzerfreundlichere Bezeichnungen anzuzeigen und zu personalisieren.
This commit is contained in:
Marcel Diegelmann 2025-10-15 12:33:05 +02:00
parent e1418dfdc1
commit 61b0a58a47
37 changed files with 697 additions and 53 deletions

View file

@ -1,7 +1,7 @@
framework: framework:
default_locale: 'en' default_locale: 'en'
# Just enable the locales we need for performance reasons. # 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: translator:
default_path: '%kernel.project_dir%/translations' default_path: '%kernel.project_dir%/translations'
fallbacks: fallbacks:

View file

@ -7,7 +7,7 @@ twig:
globals: globals:
allow_email_pw_reset: '%partdb.users.email_pw_reset%' 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' attachment_manager: '@App\Services\Attachments\AttachmentManager'
label_profile_dropdown_helper: '@App\Services\LabelSystem\LabelProfileDropdownHelper' label_profile_dropdown_helper: '@App\Services\LabelSystem\LabelProfileDropdownHelper'
error_page_admin_email: '%partdb.error_pages.admin_email%' error_page_admin_email: '%partdb.error_pages.admin_email%'
@ -20,4 +20,4 @@ twig:
when@test: when@test:
twig: 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) # 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.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 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

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

View file

@ -262,8 +262,6 @@ command `bin/console cache:clear`.
The following options are available: 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 * `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 anonymized, by removing the last byte of the IP. This is required by the GDPR (General Data Protection Regulation) in
the EU. the EU.

View file

@ -61,7 +61,7 @@ class ToolsController extends AbstractController
'default_timezone' => $settings->system->localization->timezone, 'default_timezone' => $settings->system->localization->timezone,
'default_currency' => $settings->system->localization->baseCurrency, 'default_currency' => $settings->system->localization->baseCurrency,
'default_theme' => $settings->system->customization->theme, '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'), 'demo_mode' => $this->getParameter('partdb.demo_mode'),
'use_gravatar' => $settings->system->privacy->useGravatar, 'use_gravatar' => $settings->system->privacy->useGravatar,
'gdpr_compliance' => $this->getParameter('partdb.gdpr_compliance'), '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
*/
private 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; namespace App\Form\Type;
use Symfony\Component\DependencyInjection\Attribute\Autowire; use App\Settings\SystemSettings\LocalizationSettings;
use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\LocaleType; use Symfony\Component\Form\Extension\Core\Type\LocaleType;
use Symfony\Component\OptionsResolver\OptionsResolver; use Symfony\Component\OptionsResolver\OptionsResolver;
@ -35,7 +35,7 @@ use Symfony\Component\OptionsResolver\OptionsResolver;
class LocaleSelectType extends AbstractType 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 public function configureOptions(OptionsResolver $resolver): void
{ {
$resolver->setDefaults([ $resolver->setDefaults([
'preferred_choices' => $this->preferred_languages, 'preferred_choices' => array_column($this->localizationSetting->preferredLanguages, 'value'),
]); ]);
} }
} }

View file

@ -37,6 +37,7 @@ use App\Entity\UserSystem\Group;
use App\Entity\UserSystem\User; use App\Entity\UserSystem\User;
use App\Helpers\Trees\TreeViewNode; use App\Helpers\Trees\TreeViewNode;
use App\Services\Cache\UserCacheKeyGenerator; use App\Services\Cache\UserCacheKeyGenerator;
use App\Settings\BehaviorSettings\DataSourceSynonymsSettings;
use Symfony\Bundle\SecurityBundle\Security; use Symfony\Bundle\SecurityBundle\Security;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
use Symfony\Contracts\Cache\ItemInterface; use Symfony\Contracts\Cache\ItemInterface;
@ -49,8 +50,14 @@ use Symfony\Contracts\Translation\TranslatorInterface;
*/ */
class ToolsTreeBuilder 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,
) {
} }
/** /**
@ -138,7 +145,7 @@ class ToolsTreeBuilder
$this->translator->trans('info_providers.search.title'), $this->translator->trans('info_providers.search.title'),
$this->urlGenerator->generate('info_providers_search') $this->urlGenerator->generate('info_providers_search')
))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down'); ))->setIcon('fa-treeview fa-fw fa-solid fa-cloud-arrow-down');
$nodes[] = (new TreeViewNode( $nodes[] = (new TreeViewNode(
$this->translator->trans('info_providers.bulk_import.manage_jobs'), $this->translator->trans('info_providers.bulk_import.manage_jobs'),
$this->urlGenerator->generate('bulk_info_provider_manage') $this->urlGenerator->generate('bulk_info_provider_manage')
@ -165,37 +172,37 @@ class ToolsTreeBuilder
} }
if ($this->security->isGranted('read', new Category())) { if ($this->security->isGranted('read', new Category())) {
$nodes[] = (new TreeViewNode( $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') $this->urlGenerator->generate('category_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-tags'); ))->setIcon('fa-fw fa-treeview fa-solid fa-tags');
} }
if ($this->security->isGranted('read', new Project())) { if ($this->security->isGranted('read', new Project())) {
$nodes[] = (new TreeViewNode( $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') $this->urlGenerator->generate('project_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-archive'); ))->setIcon('fa-fw fa-treeview fa-solid fa-archive');
} }
if ($this->security->isGranted('read', new Supplier())) { if ($this->security->isGranted('read', new Supplier())) {
$nodes[] = (new TreeViewNode( $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') $this->urlGenerator->generate('supplier_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-truck'); ))->setIcon('fa-fw fa-treeview fa-solid fa-truck');
} }
if ($this->security->isGranted('read', new Manufacturer())) { if ($this->security->isGranted('read', new Manufacturer())) {
$nodes[] = (new TreeViewNode( $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') $this->urlGenerator->generate('manufacturer_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-industry'); ))->setIcon('fa-fw fa-treeview fa-solid fa-industry');
} }
if ($this->security->isGranted('read', new StorageLocation())) { if ($this->security->isGranted('read', new StorageLocation())) {
$nodes[] = (new TreeViewNode( $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') $this->urlGenerator->generate('store_location_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-cube'); ))->setIcon('fa-fw fa-treeview fa-solid fa-cube');
} }
if ($this->security->isGranted('read', new Footprint())) { if ($this->security->isGranted('read', new Footprint())) {
$nodes[] = (new TreeViewNode( $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') $this->urlGenerator->generate('footprint_new')
))->setIcon('fa-fw fa-treeview fa-solid fa-microchip'); ))->setIcon('fa-fw fa-treeview fa-solid fa-microchip');
} }
@ -303,4 +310,24 @@ class ToolsTreeBuilder
return $nodes; 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

@ -38,6 +38,7 @@ use App\Repository\StructuralDBElementRepository;
use App\Services\Cache\ElementCacheTagGenerator; use App\Services\Cache\ElementCacheTagGenerator;
use App\Services\Cache\UserCacheKeyGenerator; use App\Services\Cache\UserCacheKeyGenerator;
use App\Services\EntityURLGenerator; use App\Services\EntityURLGenerator;
use App\Settings\BehaviorSettings\DataSourceSynonymsSettings;
use App\Settings\BehaviorSettings\SidebarSettings; use App\Settings\BehaviorSettings\SidebarSettings;
use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\EntityManagerInterface;
use InvalidArgumentException; use InvalidArgumentException;
@ -67,6 +68,7 @@ class TreeViewGenerator
protected TranslatorInterface $translator, protected TranslatorInterface $translator,
private readonly UrlGeneratorInterface $router, private readonly UrlGeneratorInterface $router,
private readonly SidebarSettings $sidebarSettings, private readonly SidebarSettings $sidebarSettings,
protected DataSourceSynonymsSettings $dataSourceSynonymsSettings,
) { ) {
$this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled; $this->rootNodeEnabled = $this->sidebarSettings->rootNodeEnabled;
$this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded; $this->rootNodeExpandedByDefault = $this->sidebarSettings->rootNodeExpanded;
@ -212,13 +214,15 @@ class TreeViewGenerator
protected function entityClassToRootNodeString(string $class): string protected function entityClassToRootNodeString(string $class): string
{ {
$locale = $this->translator->getLocale();
return match ($class) { return match ($class) {
Category::class => $this->translator->trans('category.labelp'), Category::class => $this->getTranslatedOrSynonym('category', $locale),
StorageLocation::class => $this->translator->trans('storelocation.labelp'), StorageLocation::class => $this->getTranslatedOrSynonym('storelocation', $locale),
Footprint::class => $this->translator->trans('footprint.labelp'), Footprint::class => $this->getTranslatedOrSynonym('footprint', $locale),
Manufacturer::class => $this->translator->trans('manufacturer.labelp'), Manufacturer::class => $this->getTranslatedOrSynonym('manufacturer', $locale),
Supplier::class => $this->translator->trans('supplier.labelp'), Supplier::class => $this->getTranslatedOrSynonym('supplier', $locale),
Project::class => $this->translator->trans('project.labelp'), Project::class => $this->getTranslatedOrSynonym('project', $locale),
default => $this->translator->trans('tree.root_node.text'), default => $this->translator->trans('tree.root_node.text'),
}; };
} }
@ -274,4 +278,24 @@ class TreeViewGenerator
return $repo->getGenericNodeTree($parent); //@phpstan-ignore-line 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; namespace App\Settings;
use App\Settings\BehaviorSettings\DataSourceSynonymsSettings;
use App\Settings\SystemSettings\AttachmentsSettings; use App\Settings\SystemSettings\AttachmentsSettings;
use App\Settings\SystemSettings\CustomizationSettings; use App\Settings\SystemSettings\CustomizationSettings;
use App\Settings\SystemSettings\HistorySettings; use App\Settings\SystemSettings\HistorySettings;
@ -37,6 +38,9 @@ class SystemSettings
#[EmbeddedSettings()] #[EmbeddedSettings()]
public ?LocalizationSettings $localization = null; public ?LocalizationSettings $localization = null;
#[EmbeddedSettings]
public ?DataSourceSynonymsSettings $dataSourceSynonyms = null;
#[EmbeddedSettings()] #[EmbeddedSettings()]
public ?CustomizationSettings $customization = null; public ?CustomizationSettings $customization = null;
@ -48,4 +52,4 @@ class SystemSettings
#[EmbeddedSettings()] #[EmbeddedSettings()]
public ?HistorySettings $history = null; public ?HistorySettings $history = null;
} }

View file

@ -26,6 +26,8 @@ namespace App\Settings\SystemSettings;
use App\Form\Type\LocaleSelectType; use App\Form\Type\LocaleSelectType;
use App\Settings\SettingsIcon; use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Metadata\EnvVarMode; use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\ParameterTypes\ArrayType;
use Jbtronics\SettingsBundle\ParameterTypes\EnumType;
use Jbtronics\SettingsBundle\Settings\Settings; use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter; use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait; use Jbtronics\SettingsBundle\Settings\SettingsTrait;
@ -60,4 +62,19 @@ class LocalizationSettings
envVar: "string:BASE_CURRENCY", envVarMode: EnvVarMode::OVERWRITE envVar: "string:BASE_CURRENCY", envVarMode: EnvVarMode::OVERWRITE
)] )]
public string $baseCurrency = 'EUR'; 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" data-title="{{ current_page_title|trim|raw }}" {{ stimulus_controller('turbo/title') }}></div>
<div class="d-none" {{ stimulus_controller('turbo/locale_menu') }}> <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'), <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})) }}"> app.request.query.all|merge(app.request.attributes.get('_route_params'))|merge({'_locale': locale.value})) }}">
{{ locale|language_name }} ({{ locale|upper }})</a> {{ locale.value|language_name }} ({{ locale.value|upper }})</a>
{% endfor %} {% endfor %}
</div> </div>

View file

@ -1,7 +1,9 @@
{% extends "admin/base_admin.html.twig" %} {% extends "admin/base_admin.html.twig" %}
{% block card_title %} {% 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 %} {% endblock %}
{% block additional_pills %} {% block additional_pills %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_admin.html.twig" %} {% extends "admin/base_admin.html.twig" %}
{% block card_title %} {% 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 %} {% endblock %}
{% block master_picture_block %} {% block master_picture_block %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_company_admin.html.twig" %} {% extends "admin/base_company_admin.html.twig" %}
{% block card_title %} {% 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 %} {% endblock %}
{% block edit_title %} {% block edit_title %}

View file

@ -3,7 +3,9 @@
{# @var entity App\Entity\ProjectSystem\Project #} {# @var entity App\Entity\ProjectSystem\Project #}
{% block card_title %} {% 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 %} {% endblock %}
{% block edit_title %} {% block edit_title %}

View file

@ -2,7 +2,9 @@
{% import "label_system/dropdown_macro.html.twig" as dropdown %} {% import "label_system/dropdown_macro.html.twig" as dropdown %}
{% block card_title %} {% 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 %} {% endblock %}
{% block additional_controls %} {% block additional_controls %}

View file

@ -1,7 +1,9 @@
{% extends "admin/base_company_admin.html.twig" %} {% extends "admin/base_company_admin.html.twig" %}
{% block card_title %} {% 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 %} {% endblock %}
{% block additional_panes %} {% block additional_panes %}

View file

@ -1,13 +1,15 @@
{% macro sidebar_dropdown() %} {% macro sidebar_dropdown() %}
{% set currentLocale = app.request.locale %}
{# Format is [mode, route, label, show_condition] #} {# Format is [mode, route, label, show_condition] #}
{% set data_sources = [ {% set data_sources = [
['categories', path('tree_category_root'), 'category.labelp', is_granted('@categories.read') and is_granted('@parts.read')], ['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')], ['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')], ['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')], ['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')], ['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')], ['projects', path('tree_device_root'), 'project.labelp', is_granted('@projects.read'), 'project'],
['tools', path('tree_tools'), 'tools.label', true], ['tools', path('tree_tools'), 'tools.label', true, 'tool'],
] %} ] %}
<li class="dropdown-header">{% trans %}actions{% endtrans %}</li> <li class="dropdown-header">{% trans %}actions{% endtrans %}</li>
@ -18,9 +20,9 @@
{% for source in data_sources %} {% for source in data_sources %}
{% if source[3] %} {# show_condition #} {% 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') }} {{ stimulus_action('elements/sidebar_tree', 'changeDataSource') }}
>{{ source[2] | trans }}</button></li> >{{ get_data_source_name(source[4], source[2]) }}</button></li>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% endmacro %} {% endmacro %}
@ -61,4 +63,4 @@
<div class="treeview-sm mt-2" {{ stimulus_target('elements/tree', 'tree') }}></div> <div class="treeview-sm mt-2" {{ stimulus_target('elements/tree', 'tree') }}></div>
</div> </div>
{% endmacro %} {% endmacro %}

View file

@ -6,12 +6,34 @@
<div class="form-check"> <div class="form-check">
<input type="checkbox" class="form-check-input tristate permission-checkbox permission_multicheckbox" id="mulit_check_{{ form.vars.label }}"> <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 }}"> <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> </label>
</div> </div>
{% else %} {% else %}
<b>{{ form.vars.label | trans }}</b> <b>def{{ form.vars.label | trans }}</b>
{% endif %} {% endif %}
@ -110,4 +132,4 @@
{% endfor %} {% endfor %}
</div> </div>
{% endblock %} {% endblock %}

View file

@ -12669,6 +12669,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> &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> </segment>
</unit> </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"> <unit id="cvpTUeY" name="settings.system.privacy">
<segment state="translated"> <segment state="translated">
<source>settings.system.privacy</source> <source>settings.system.privacy</source>
@ -13479,5 +13545,11 @@ Vezměte prosím na vědomí, že se nemůžete vydávat za uživatele se zakáz
<target>Minimální šířka náhledu (px)</target> <target>Minimální šířka náhledu (px)</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Váš synonymum: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -12196,5 +12196,11 @@ Bemærk venligst, at du ikke kan kopiere fra deaktiveret bruger. Hvis du prøver
<target>Du forsøgte at fjerne/tilføje en mængde sat til nul! Der blev ikke foretaget nogen handling.</target> <target>Du forsøgte at fjerne/tilføje en mængde sat til nul! Der blev ikke foretaget nogen handling.</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Dit synonym: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -12749,6 +12749,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> &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> </segment>
</unit> </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"> <unit id="cvpTUeY" name="settings.system.privacy">
<segment state="translated"> <segment state="translated">
<source>settings.system.privacy</source> <source>settings.system.privacy</source>
@ -14189,5 +14255,11 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<target>Maximale Anzahl von Zuordnungen erreicht</target> <target>Maximale Anzahl von Zuordnungen erreicht</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Ihr Synonym: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

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

View file

@ -12750,6 +12750,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> &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> </segment>
</unit> </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"> <unit id="cvpTUeY" name="settings.system.privacy">
<segment state="translated"> <segment state="translated">
<source>settings.system.privacy</source> <source>settings.system.privacy</source>
@ -14190,5 +14256,11 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<target>Maximum number of mappings reached</target> <target>Maximum number of mappings reached</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Your synonym: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -12368,5 +12368,11 @@ Por favor ten en cuenta que no puedes personificar a un usuario deshabilitado. S
<target>Este componente contiene más de un stock. Cambie la ubicación manualmente para seleccionar el stock deseado.</target> <target>Este componente contiene más de un stock. Cambie la ubicación manualmente para seleccionar el stock deseado.</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Tu sinónimo: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -6947,7 +6947,7 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>company.edit.address.placeholder</source> <source>company.edit.address.placeholder</source>
<target>Ex. 99 exemple de rue <target>Ex. 99 exemple de rue
exemple de ville</target> exemple de ville</target>
</segment> </segment>
</unit> </unit>
@ -9097,5 +9097,11 @@ exemple de ville</target>
<target>Si vous avez des questions à propos de Part-DB , rendez vous sur &lt;a href="%href%" class="link-external" target="_blank"&gt;Github&lt;/a&gt;</target> <target>Si vous avez des questions à propos de Part-DB , rendez vous sur &lt;a href="%href%" class="link-external" target="_blank"&gt;Github&lt;/a&gt;</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Votre synonyme : %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -12370,5 +12370,11 @@ Notare che non è possibile impersonare un utente disattivato. Quando si prova a
<target>Questo componente contiene più di uno stock. Cambia manualmente la posizione per selezionare quale stock scegliere.</target> <target>Questo componente contiene più di uno stock. Cambia manualmente la posizione per selezionare quale stock scegliere.</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Il tuo sinonimo: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -8834,5 +8834,11 @@ Exampletown</target>
<target>Part-DBについての質問は、&lt;a href="%href%" class="link-external" target="_blank"&gt;GitHub&lt;/a&gt; にスレッドがあります。</target> <target>Part-DBについての質問は、&lt;a href="%href%" class="link-external" target="_blank"&gt;GitHub&lt;/a&gt; にスレッドがあります。</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (あなたの同義語: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -724,5 +724,11 @@
<target>Weet u zeker dat u wilt doorgaan?</target> <target>Weet u zeker dat u wilt doorgaan?</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Uw synoniem: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

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

View file

@ -731,7 +731,7 @@
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>user.edit.tfa.disable_tfa_message</source> <source>user.edit.tfa.disable_tfa_message</source>
<target>Это выключит &lt;b&gt;все активные двухфакторной способы аутентификации пользователя&lt;/b&gt;и удалит &lt;b&gt;резервные коды&lt;/b&gt;! <target>Это выключит &lt;b&gt;все активные двухфакторной способы аутентификации пользователя&lt;/b&gt;и удалит &lt;b&gt;резервные коды&lt;/b&gt;!
&lt;br&gt; &lt;br&gt;
Пользователь должен будет снова настроить все методы двухфакторной аутентификации и распечатать новые резервные коды! &lt;br&gt;&lt;br&gt; Пользователь должен будет снова настроить все методы двухфакторной аутентификации и распечатать новые резервные коды! &lt;br&gt;&lt;br&gt;
&lt;b&gt;Делайте это только в том случае, если вы абсолютно уверены в личности пользователя (обращающегося за помощью), в противном случае учетная запись может быть взломана злоумышленником!&lt;/b&gt;</target> &lt;b&gt;Делайте это только в том случае, если вы абсолютно уверены в личности пользователя (обращающегося за помощью), в противном случае учетная запись может быть взломана злоумышленником!&lt;/b&gt;</target>
@ -3740,7 +3740,7 @@
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>tfa_backup.reset_codes.confirm_message</source> <source>tfa_backup.reset_codes.confirm_message</source>
<target>Это удалит все предыдущие коды и создаст набор новых. Это не может быть отменено. <target>Это удалит все предыдущие коды и создаст набор новых. Это не может быть отменено.
Не забудьте распечатать новы кода и хранить их в безопасном месте!</target> Не забудьте распечатать новы кода и хранить их в безопасном месте!</target>
</segment> </segment>
</unit> </unit>
@ -12323,5 +12323,11 @@
<target>Профиль сохранен!</target> <target>Профиль сохранен!</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (Ваш синоним: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>

View file

@ -12208,5 +12208,11 @@ Element 3</target>
<target>成功创建 %COUNT% 个元素。</target> <target>成功创建 %COUNT% 个元素。</target>
</segment> </segment>
</unit> </unit>
<unit id="a7uOieC" name="datasource.synonym">
<segment state="translated">
<source>datasource.synonym</source>
<target>%name% (您的同义词: %synonym%)</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>