Moved pollin and reichelt info provider settings to new settings system
Some checks failed
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, sqlite) (push) Has been cancelled
Build assets artifact / Build assets artifact (push) Has been cancelled
Docker Image Build / docker (push) Has been cancelled
Docker Image Build (FrankenPHP) / docker (push) Has been cancelled
Static analysis / Static analysis (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.1, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, mysql) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.1, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.4, postgres) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.1, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.2, sqlite) (push) Has been cancelled
PHPUnit Tests / PHPUnit and coverage Test (PHP 8.3, sqlite) (push) Has been cancelled

This commit is contained in:
Jan Böhmer 2025-06-22 20:08:27 +02:00
parent beb079b825
commit b2946aee0d
8 changed files with 182 additions and 86 deletions

31
.env
View file

@ -67,16 +67,6 @@ ERROR_PAGE_ADMIN_EMAIL=''
# If this is set to true, solutions to common problems are shown on error pages. Disable this, if you do not want your users to see them...
ERROR_PAGE_SHOW_HELP=1
##################################################################################
# Part table settings
##################################################################################
# The default page size for the part table (set to -1 to show all parts on one page)
#TABLE_DEFAULT_PAGE_SIZE=50
# Configure which columns will be visible by default in the parts table (and in which order).
# This is a comma separated list of column names. See documentation for available values.
#TABLE_PARTS_DEFAULT_COLUMNS=name,description,category,footprint,manufacturer,storage_location,amount
##################################################################################
# Info provider settings
##################################################################################
@ -106,27 +96,6 @@ PROVIDER_OCTOPART_SEARCH_LIMIT=10
# Set to false to include non authorized offers in the results
PROVIDER_OCTOPART_ONLY_AUTHORIZED_SELLERS=1
# Reichelt provider:
# Reichelt.com offers no official API, so this info provider webscrapes the website to extract info
# It could break at any time, use it at your own risk
# We dont require an API key for Reichelt, just set this to 1 to enable Reichelt support
PROVIDER_REICHELT_ENABLED=0
# The country to get prices for
PROVIDER_REICHELT_COUNTRY=DE
# The language to get results in (en, de, fr, nl, pl, it, es)
PROVIDER_REICHELT_LANGUAGE=en
# Include VAT in prices (set to 1 to include VAT, 0 to exclude VAT)
PROVIDER_REICHELT_INCLUDE_VAT=1
# The currency to get prices in (only for countries with countries other than EUR)
PROVIDER_REICHELT_CURRENCY=EUR
# Pollin provider:
# Pollin.de offers no official API, so this info provider webscrapes the website to extract info
# It could break at any time, use it at your own risk
# We dont require an API key for Pollin, just set this to 1 to enable Pollin support
PROVIDER_POLLIN_ENABLED=0
##################################################################################
# EDA integration related settings
##################################################################################

View file

@ -31,6 +31,7 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\PollinSettings;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -39,8 +40,7 @@ class PollinProvider implements InfoProviderInterface
{
public function __construct(private readonly HttpClientInterface $client,
#[Autowire(env: 'bool:PROVIDER_POLLIN_ENABLED')]
private readonly bool $enabled = true,
private readonly PollinSettings $settings,
)
{
}
@ -62,7 +62,7 @@ class PollinProvider implements InfoProviderInterface
public function isActive(): bool
{
return $this->enabled;
return $this->settings->enabled;
}
public function searchByKeyword(string $keyword): array

View file

@ -29,6 +29,7 @@ use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\PriceDTO;
use App\Services\InfoProviderSystem\DTOs\PurchaseInfoDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\ReicheltSettings;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DomCrawler\Crawler;
use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -39,16 +40,7 @@ class ReicheltProvider implements InfoProviderInterface
public const DISTRIBUTOR_NAME = "Reichelt";
public function __construct(private readonly HttpClientInterface $client,
#[Autowire(env: "bool:PROVIDER_REICHELT_ENABLED")]
private readonly bool $enabled = true,
#[Autowire(env: "PROVIDER_REICHELT_LANGUAGE")]
private readonly string $language = "en",
#[Autowire(env: "PROVIDER_REICHELT_COUNTRY")]
private readonly string $country = "DE",
#[Autowire(env: "PROVIDER_REICHELT_INCLUDE_VAT")]
private readonly bool $includeVAT = false,
#[Autowire(env: "PROVIDER_REICHELT_CURRENCY")]
private readonly string $currency = "EUR",
private readonly ReicheltSettings $settings,
)
{
}
@ -70,7 +62,7 @@ class ReicheltProvider implements InfoProviderInterface
public function isActive(): bool
{
return $this->enabled;
return $this->settings->enabled;
}
public function searchByKeyword(string $keyword): array
@ -121,8 +113,8 @@ class ReicheltProvider implements InfoProviderInterface
sprintf(
'https://www.reichelt.com/?ACTION=514&id=74&article=%s&LANGUAGE=%s&CCOUNTRY=%s',
$id,
strtoupper($this->language),
strtoupper($this->country)
strtoupper($this->settings->language),
strtoupper($this->settings->country)
)
);
$json = $response->toArray();
@ -133,8 +125,8 @@ class ReicheltProvider implements InfoProviderInterface
$response = $this->client->request('GET', $productPage, [
'query' => [
'CCTYPE' => $this->includeVAT ? 'private' : 'business',
'currency' => $this->currency,
'CCTYPE' => $this->settings->includeVAT ? 'private' : 'business',
'currency' => $this->settings->currency,
],
]);
$html = $response->getContent();
@ -158,7 +150,7 @@ class ReicheltProvider implements InfoProviderInterface
distributor_name: self::DISTRIBUTOR_NAME,
order_number: $json[0]['article_artnr'],
prices: array_merge(
[new PriceDTO(1.0, $priceString, $currency, $this->includeVAT)]
[new PriceDTO(1.0, $priceString, $currency, $this->settings->includeVAT)]
, $this->parseBatchPrices($dom, $currency)),
product_url: $productPage
);
@ -218,7 +210,7 @@ class ReicheltProvider implements InfoProviderInterface
//Strip any non-numeric characters
$priceString = preg_replace('/[^0-9.]/', '', $priceString);
$prices[] = new PriceDTO($minAmount, $priceString, $currency, $this->includeVAT);
$prices[] = new PriceDTO($minAmount, $priceString, $currency, $this->settings->includeVAT);
});
return $prices;
@ -270,7 +262,7 @@ class ReicheltProvider implements InfoProviderInterface
private function getBaseURL(): string
{
//Without the trailing slash
return 'https://www.reichelt.com/' . strtolower($this->country) . '/' . strtolower($this->language);
return 'https://www.reichelt.com/' . strtolower($this->settings->country) . '/' . strtolower($this->settings->language);
}
public function getCapabilities(): array

View file

@ -46,4 +46,10 @@ class InfoProviderSettings
#[EmbeddedSettings]
public ?OEMSecretsSettings $oemsecrets = null;
#[EmbeddedSettings]
public ?ReicheltSettings $reichelt = null;
#[EmbeddedSettings]
public ?PollinSettings $pollin = null;
}

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 - 2025 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\InfoProviderSystem;
use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Symfony\Component\Translation\TranslatableMessage as TM;
#[Settings(label: new TM("settings.ips.pollin"), description: new TM("settings.ips.pollin.help"))]
#[SettingsIcon("fa-plug")]
class PollinSettings
{
#[SettingsParameter(label: new TM("settings.ips.lcsc.enabled"), envVar: "bool:PROVIDER_POLLIN_ENABLED")]
public bool $enabled = false;
}

View file

@ -0,0 +1,62 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2025 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\InfoProviderSystem;
use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Settings\Settings;
use Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Jbtronics\SettingsBundle\Settings\SettingsTrait;
use Symfony\Component\Form\Extension\Core\Type\CountryType;
use Symfony\Component\Form\Extension\Core\Type\CurrencyType;
use Symfony\Component\Form\Extension\Core\Type\LanguageType;
use Symfony\Component\Translation\TranslatableMessage as TM;
use Symfony\Component\Validator\Constraints as Assert;
#[Settings(label: new TM("settings.ips.reichelt"), description: new TM("settings.ips.reichelt.help"))]
#[SettingsIcon("fa-plug")]
class ReicheltSettings
{
use SettingsTrait;
public const SUPPORTED_LANGUAGE = ["en", "de", "fr", "nl", "pl", "it", "es"];
#[SettingsParameter(label: new TM("settings.ips.lcsc.enabled"), envVar: "bool:PROVIDER_REICHELT_ENABLED")]
public bool $enabled = false;
#[SettingsParameter(label: new TM("settings.ips.tme.currency"), formType: CurrencyType::class, formOptions: ["preferred_choices" => ["EUR"]], envVar: "PROVIDER_REICHELT_CURRENCY")]
public string $currency = "EUR";
#[SettingsParameter(label: new TM("settings.ips.tme.language"), formType: LanguageType::class, formOptions: ["preferred_choices" => self::SUPPORTED_LANGUAGE], envVar: "PROVIDER_REICHELT_LANGUAGE")]
#[Assert\Language()]
#[Assert\Choice(choices: self::SUPPORTED_LANGUAGE)]
public string $language = "en";
#[SettingsParameter(label: new TM("settings.ips.tme.country"), envVar: "PROVIDER_REICHELT_COUNTRY", formType: CountryType::class, formOptions: ["preferred_choices" => ["DE", "PL", "GB", "FR"]])]
#[Assert\Country]
public string $country = "DE";
#[SettingsParameter(label: new TM("settings.ips.reichelt.include_vat"), envVar: "bool:PROVIDER_REICHELT_INCLUDE_VAT")]
public bool $includeVAT = true;
}

View file

@ -56,7 +56,7 @@ class TMESettings
#[Assert\Language]
public string $language = "en";
#[SettingsParameter(label: new TM("settings.ips.tme.country"), envVar: "PROVIDER_TME_COUNTRY", formType: CountryType::class, formOptions: ["preferred_choices" => ["DE", "PL", "GB", "FR"]])]
#[SettingsParameter(label: new TM("settings.ips.tme.country"), formType: CountryType::class, formOptions: ["preferred_choices" => ["DE", "PL", "GB", "FR"]], envVar: "PROVIDER_TME_COUNTRY")]
#[Assert\Country]
public string $country = "DE";

View file

@ -242,7 +242,7 @@
</notes>
<segment state="final">
<source>part.info.timetravel_hint</source>
<target>This is how the part appeared before %timestamp%. &lt;i&gt;Please note that this feature is experimental, so the info may not be correct.&lt;/i&gt;</target>
<target><![CDATA[This is how the part appeared before %timestamp%. <i>Please note that this feature is experimental, so the info may not be correct.</i>]]></target>
</segment>
</unit>
<unit id="3exvSpl" name="standard.label">
@ -731,10 +731,10 @@
</notes>
<segment state="translated">
<source>user.edit.tfa.disable_tfa_message</source>
<target>This will disable &lt;b&gt;all active two-factor authentication methods of the user&lt;/b&gt; and delete the &lt;b&gt;backup codes&lt;/b&gt;!
&lt;br&gt;
The user will have to set up all two-factor authentication methods again and print new backup codes! &lt;br&gt;&lt;br&gt;
&lt;b&gt;Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!&lt;/b&gt;</target>
<target><![CDATA[This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>!
<br>
The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br>
<b>Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!</b>]]></target>
</segment>
</unit>
<unit id="APsHYu0" name="user.edit.tfa.disable_tfa.btn">
@ -885,9 +885,9 @@ The user will have to set up all two-factor authentication methods again and pri
</notes>
<segment state="translated">
<source>entity.delete.message</source>
<target>This can not be undone!
&lt;br&gt;
Sub elements will be moved upwards.</target>
<target><![CDATA[This can not be undone!
<br>
Sub elements will be moved upwards.]]></target>
</segment>
</unit>
<unit id="2tKAqHw" name="entity.delete">
@ -1441,7 +1441,7 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="final">
<source>homepage.github.text</source>
<target>Source, downloads, bug reports, to-do-list etc. can be found on &lt;a href="%href%" class="link-external" target="_blank"&gt;GitHub project page&lt;/a&gt;</target>
<target><![CDATA[Source, downloads, bug reports, to-do-list etc. can be found on <a href="%href%" class="link-external" target="_blank">GitHub project page</a>]]></target>
</segment>
</unit>
<unit id="D5OKsgU" name="homepage.help.caption">
@ -1463,7 +1463,7 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="translated">
<source>homepage.help.text</source>
<target>Help and tips can be found in Wiki the &lt;a href="%href%" class="link-external" target="_blank"&gt;GitHub page&lt;/a&gt;</target>
<target><![CDATA[Help and tips can be found in Wiki the <a href="%href%" class="link-external" target="_blank">GitHub page</a>]]></target>
</segment>
</unit>
<unit id="dnirx4v" name="homepage.forum.caption">
@ -1705,7 +1705,7 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="translated">
<source>email.pw_reset.fallback</source>
<target>If this does not work for you, go to &lt;a href="%url%"&gt;%url%&lt;/a&gt; and enter the following info</target>
<target><![CDATA[If this does not work for you, go to <a href="%url%">%url%</a> and enter the following info]]></target>
</segment>
</unit>
<unit id="DduL9Hu" name="email.pw_reset.username">
@ -1735,7 +1735,7 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="translated">
<source>email.pw_reset.valid_unit %date%</source>
<target>The reset token will be valid until &lt;i&gt;%date%&lt;/i&gt;.</target>
<target><![CDATA[The reset token will be valid until <i>%date%</i>.]]></target>
</segment>
</unit>
<unit id="8sBnjRy" name="orderdetail.delete">
@ -3578,8 +3578,8 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="translated">
<source>tfa_google.disable.confirm_message</source>
<target>If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.&lt;br&gt;
Also note that without two-factor authentication, your account is no longer as well protected against attackers!</target>
<target><![CDATA[If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.<br>
Also note that without two-factor authentication, your account is no longer as well protected against attackers!]]></target>
</segment>
</unit>
<unit id="yu9MSt5" name="tfa_google.disabled_message">
@ -3599,7 +3599,7 @@ Also note that without two-factor authentication, your account is no longer as w
</notes>
<segment state="translated">
<source>tfa_google.step.download</source>
<target>Download an authenticator app (e.g. &lt;a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"&gt;Google Authenticator&lt;/a&gt; oder &lt;a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp"&gt;FreeOTP Authenticator&lt;/a&gt;)</target>
<target><![CDATA[Download an authenticator app (e.g. <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a> oder <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a>)]]></target>
</segment>
</unit>
<unit id="eriwJoR" name="tfa_google.step.scan">
@ -3841,8 +3841,8 @@ Also note that without two-factor authentication, your account is no longer as w
</notes>
<segment state="translated">
<source>tfa_trustedDevices.explanation</source>
<target>When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed.
If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of &lt;i&gt;all &lt;/i&gt;computers here.</target>
<target><![CDATA[When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed.
If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of <i>all </i>computers here.]]></target>
</segment>
</unit>
<unit id="FZINq8z" name="tfa_trustedDevices.invalidate.confirm_title">
@ -5313,7 +5313,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
</notes>
<segment state="translated">
<source>label_options.lines_mode.help</source>
<target>If you select Twig here, the content field is interpreted as Twig template. See &lt;a href="https://twig.symfony.com/doc/3.x/templates.html"&gt;Twig documentation&lt;/a&gt; and &lt;a href="https://docs.part-db.de/usage/labels.html#twig-mode"&gt;Wiki&lt;/a&gt; for more information.</target>
<target><![CDATA[If you select Twig here, the content field is interpreted as Twig template. See <a href="https://twig.symfony.com/doc/3.x/templates.html">Twig documentation</a> and <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a> for more information.]]></target>
</segment>
</unit>
<unit id="isvxbiX" name="label_options.page_size.label">
@ -9388,25 +9388,25 @@ Element 3</target>
<unit id="r4vDLAt" name="filter.parameter_value_constraint.operator.&lt;">
<segment state="translated">
<source>filter.parameter_value_constraint.operator.&lt;</source>
<target>Typ. Value &lt;</target>
<target><![CDATA[Typ. Value <]]></target>
</segment>
</unit>
<unit id="X9SA3UP" name="filter.parameter_value_constraint.operator.&gt;">
<segment state="translated">
<source>filter.parameter_value_constraint.operator.&gt;</source>
<target>Typ. Value &gt;</target>
<target><![CDATA[Typ. Value >]]></target>
</segment>
</unit>
<unit id="BQGaoQS" name="filter.parameter_value_constraint.operator.&lt;=">
<segment state="translated">
<source>filter.parameter_value_constraint.operator.&lt;=</source>
<target>Typ. Value &lt;=</target>
<target><![CDATA[Typ. Value <=]]></target>
</segment>
</unit>
<unit id="2ha3P6g" name="filter.parameter_value_constraint.operator.&gt;=">
<segment state="translated">
<source>filter.parameter_value_constraint.operator.&gt;=</source>
<target>Typ. Value &gt;=</target>
<target><![CDATA[Typ. Value >=]]></target>
</segment>
</unit>
<unit id="4DaBace" name="filter.parameter_value_constraint.operator.BETWEEN">
@ -9514,7 +9514,7 @@ Element 3</target>
<unit id="4tHhDtU" name="parts_list.search.searching_for">
<segment state="translated">
<source>parts_list.search.searching_for</source>
<target>Searching parts with keyword &lt;b&gt;%keyword%&lt;/b&gt;</target>
<target><![CDATA[Searching parts with keyword <b>%keyword%</b>]]></target>
</segment>
</unit>
<unit id="4vomKLa" name="parts_list.search_options.caption">
@ -10174,13 +10174,13 @@ Element 3</target>
<unit id="NdZ1t7a" name="project.builds.number_of_builds_possible">
<segment state="translated">
<source>project.builds.number_of_builds_possible</source>
<target>You have enough stocked to build &lt;b&gt;%max_builds%&lt;/b&gt; builds of this project.</target>
<target><![CDATA[You have enough stocked to build <b>%max_builds%</b> builds of this project.]]></target>
</segment>
</unit>
<unit id="iuSpPbg" name="project.builds.check_project_status">
<segment state="translated">
<source>project.builds.check_project_status</source>
<target>The current project status is &lt;b&gt;"%project_status%"&lt;/b&gt;. You should check if you really want to build the project with this status!</target>
<target><![CDATA[The current project status is <b>"%project_status%"</b>. You should check if you really want to build the project with this status!]]></target>
</segment>
</unit>
<unit id="Y7vSSxi" name="project.builds.following_bom_entries_miss_instock_n">
@ -10282,7 +10282,7 @@ Element 3</target>
<unit id="GzqIwHH" name="entity.select.add_hint">
<segment state="translated">
<source>entity.select.add_hint</source>
<target>Use -&gt; to create nested structures, e.g. "Node 1-&gt;Node 1.1"</target>
<target><![CDATA[Use -> to create nested structures, e.g. "Node 1->Node 1.1"]]></target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
@ -10306,13 +10306,13 @@ Element 3</target>
<unit id="XLnXtsR" name="homepage.first_steps.introduction">
<segment state="translated">
<source>homepage.first_steps.introduction</source>
<target>Your database is still empty. You might want to read the &lt;a href="%url%"&gt;documentation&lt;/a&gt; or start to creating the following data structures:</target>
<target><![CDATA[Your database is still empty. You might want to read the <a href="%url%">documentation</a> or start to creating the following data structures:]]></target>
</segment>
</unit>
<unit id="Q79MOIk" name="homepage.first_steps.create_part">
<segment state="translated">
<source>homepage.first_steps.create_part</source>
<target>Or you can directly &lt;a href="%url%"&gt;create a new part&lt;/a&gt;.</target>
<target><![CDATA[Or you can directly <a href="%url%">create a new part</a>.]]></target>
</segment>
</unit>
<unit id="vplYq4f" name="homepage.first_steps.hide_hint">
@ -10324,7 +10324,7 @@ Element 3</target>
<unit id="MJoZl4f" name="homepage.forum.text">
<segment state="translated">
<source>homepage.forum.text</source>
<target>For questions about Part-DB use the &lt;a href="%href%" class="link-external" target="_blank"&gt;discussion forum&lt;/a&gt;</target>
<target><![CDATA[For questions about Part-DB use the <a href="%href%" class="link-external" target="_blank">discussion forum</a>]]></target>
</segment>
</unit>
<unit id="YsukbnK" name="log.element_edited.changed_fields.category">
@ -10978,7 +10978,7 @@ Element 3</target>
<unit id="p_IxB9K" name="parts.import.help_documentation">
<segment state="translated">
<source>parts.import.help_documentation</source>
<target>See the &lt;a href="%link%"&gt;documentation&lt;/a&gt; for more information on the file format.</target>
<target><![CDATA[See the <a href="%link%">documentation</a> for more information on the file format.]]></target>
</segment>
</unit>
<unit id="awbvhVq" name="parts.import.help">
@ -11158,7 +11158,7 @@ Element 3</target>
<unit id="o5u.Nnz" name="part.filter.lessThanDesired">
<segment state="translated">
<source>part.filter.lessThanDesired</source>
<target>In stock less than desired (total amount &lt; min. amount)</target>
<target><![CDATA[In stock less than desired (total amount < min. amount)]]></target>
</segment>
</unit>
<unit id="YN9eLcZ" name="part.filter.lotOwner">
@ -11970,13 +11970,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="i68lU5x" name="part.merge.confirm.title">
<segment state="translated">
<source>part.merge.confirm.title</source>
<target>Do you really want to merge &lt;b&gt;%other%&lt;/b&gt; into &lt;b&gt;%target%&lt;/b&gt;?</target>
<target><![CDATA[Do you really want to merge <b>%other%</b> into <b>%target%</b>?]]></target>
</segment>
</unit>
<unit id="k0anzYV" name="part.merge.confirm.message">
<segment state="translated">
<source>part.merge.confirm.message</source>
<target>&lt;b&gt;%other%&lt;/b&gt; will be deleted, and the part will be saved with the shown information.</target>
<target><![CDATA[<b>%other%</b> will be deleted, and the part will be saved with the shown information.]]></target>
</segment>
</unit>
<unit id="mmW5Yl1" name="part.info.merge_modal.title">
@ -12892,5 +12892,35 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<target>This part contains more than one stock. Change the location by hand to select, which stock to choose.</target>
</segment>
</unit>
<unit id="qn25jL." name="settings.ips.reichelt">
<segment>
<source>settings.ips.reichelt</source>
<target>Reichelt</target>
</segment>
</unit>
<unit id="yYazL3j" name="settings.ips.reichelt.help">
<segment>
<source>settings.ips.reichelt.help</source>
<target>Reichelt.com offers no official API, so this info provider webscrapes the website to extract info. It could break at any time, use it at your own risk.</target>
</segment>
</unit>
<unit id="2EVJVc6" name="settings.ips.reichelt.include_vat">
<segment>
<source>settings.ips.reichelt.include_vat</source>
<target>Include VAT in prices</target>
</segment>
</unit>
<unit id="Ocp5ktF" name="settings.ips.pollin">
<segment>
<source>settings.ips.pollin</source>
<target>Pollin</target>
</segment>
</unit>
<unit id="Xlj6kj2" name="settings.ips.pollin.help">
<segment>
<source>settings.ips.pollin.help</source>
<target>Pollin.de offers no official API, so this info provider webscrapes the website to extract info. It could break at any time, use it at your own risk.</target>
</segment>
</unit>
</file>
</xliff>