Compare commits

..

No commits in common. "df3f069a769ee42224429b04a44c9c0daab9dd88" and "3ed62f5cee80ca4dbfa935b27caff50b9af0af1e" have entirely different histories.

5 changed files with 44 additions and 261 deletions

View file

@ -278,16 +278,6 @@ The following env configuration options are available:
* `PROVIDER_BUERKLIN_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, default: `EUR`). * `PROVIDER_BUERKLIN_CURRENCY`: The currency you want to get prices in if available (optional, 3 letter ISO-code, default: `EUR`).
* `PROVIDER_BUERKLIN_LANGUAGE`: The language you want to get the descriptions in. Possible values: `de` = German, `en` = English. (optional, default: `en`) * `PROVIDER_BUERKLIN_LANGUAGE`: The language you want to get the descriptions in. Possible values: `de` = German, `en` = English. (optional, default: `en`)
### Conrad
The conrad provider the [Conrad API](https://developer.conrad.com/) to search for parts and retried their information.
To use it you have to request access to the API, however it seems currently your mail address needs to be allowlisted before you can register for an account.
The conrad webpages uses the API key in the requests, so you might be able to extract a working API key by listening to browser requests.
That method is not officially supported nor encouraged by Part-DB, and might break at any moment.
The following env configuration options are available:
* `PROVIDER_CONRAD_API_KEY`: The API key you got from Conrad (mandatory)
### Custom provider ### Custom provider
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long

View file

@ -23,11 +23,8 @@ declare(strict_types=1);
namespace App\Services\InfoProviderSystem\Providers; namespace App\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO; use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO; 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\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Settings\InfoProviderSystem\ConradSettings; use App\Settings\InfoProviderSystem\ConradSettings;
use Symfony\Contracts\HttpClient\HttpClientInterface; use Symfony\Contracts\HttpClient\HttpClientInterface;
@ -36,18 +33,9 @@ readonly class ConradProvider implements InfoProviderInterface
{ {
private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch'; private const SEARCH_ENDPOINT = '/search/1/v3/facetSearch';
public const DISTRIBUTOR_NAME = 'Conrad';
private HttpClientInterface $httpClient; public function __construct(private HttpClientInterface $httpClient, private ConradSettings $settings)
public function __construct( HttpClientInterface $httpClient, private ConradSettings $settings)
{ {
//We want everything in JSON
$this->httpClient = $httpClient->withOptions([
'headers' => [
'Accept' => 'application/json',
],
]);
} }
public function getProviderInfo(): array public function getProviderInfo(): array
@ -87,6 +75,31 @@ readonly class ConradProvider implements InfoProviderInterface
return null; return null;
} }
private function getFootprintFromTechnicalAttributes(array $technicalDetails): ?string
{
foreach ($technicalDetails as $detail) {
if ($detail['attributeID'] === 'ATT.LOV.HOUSING_SEMICONDUCTORS') {
return $detail['values'][0]['value'] ?? null;
}
}
return null;
}
private function technicalAttributesToParameters(array $technicalAttributes): array
{
$parameters = [];
foreach ($technicalAttributes as $attribute) {
if ($attribute['multiValue'] ?? false === true) {
throw new \LogicException('Multi value attributes are not supported yet');
}
$parameters[] = ParameterDTO::parseValueField($attribute['attributeName'],
$attribute['values'][0]['value'], $attribute['values'][0]['unit']['name'] ?? null);
}
return $parameters;
}
public function searchByKeyword(string $keyword): array public function searchByKeyword(string $keyword): array
{ {
$url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/' $url = $this->settings->shopID->getAPIRoot() . self::SEARCH_ENDPOINT . '/'
@ -99,8 +112,7 @@ readonly class ConradProvider implements InfoProviderInterface
], ],
'json' => [ 'json' => [
'query' => $keyword, 'query' => $keyword,
'size' => 50, 'size' => 25,
'sort' => [["field"=>"_score","order"=>"desc"]],
], ],
]); ]);
@ -112,8 +124,8 @@ readonly class ConradProvider implements InfoProviderInterface
$out[] = new SearchResultDTO( $out[] = new SearchResultDTO(
provider_key: $this->getProviderKey(), provider_key: $this->getProviderKey(),
provider_id: $result['productId'], provider_id: $result['productId'],
name: $result['manufacturerId'] ?? $result['productId'], name: $result['title'],
description: $result['title'] ?? '', description: '',
manufacturer: $result['brand']['name'] ?? null, manufacturer: $result['brand']['name'] ?? null,
mpn: $result['manufacturerId'] ?? null, mpn: $result['manufacturerId'] ?? null,
preview_image_url: $result['image'] ?? null, preview_image_url: $result['image'] ?? null,
@ -125,158 +137,6 @@ readonly class ConradProvider implements InfoProviderInterface
return $out; return $out;
} }
private function getFootprintFromTechnicalAttributes(array $technicalDetails): ?string
{
foreach ($technicalDetails as $detail) {
if ($detail['attributeID'] === 'ATT.LOV.HOUSING_SEMICONDUCTORS') {
return $detail['values'][0]['value'] ?? null;
}
}
return null;
}
/**
* @param array $technicalAttributes
* @return array<ParameterDTO>
*/
private function technicalAttributesToParameters(array $technicalAttributes): array
{
return array_map(static function (array $p) {
if (count($p['values']) === 1) { //Single value attribute
if (array_key_exists('unit', $p['values'][0])) {
return ParameterDTO::parseValueField( //With unit
name: $p['attributeName'],
value: $p['values'][0]['value'],
unit: $p['values'][0]['unit']['name'],
);
}
return ParameterDTO::parseValueIncludingUnit(
name: $p['attributeName'],
value: $p['values'][0]['value'],
);
}
if (count($p['values']) === 2) { //Multi value attribute (e.g. min/max)
$value = $p['values'][0]['value'] ?? null;
$value2 = $p['values'][1]['value'] ?? null;
$unit = $p['values'][0]['unit']['name'] ?? '';
$unit2 = $p['values'][1]['unit']['name'] ?? '';
if ($unit === $unit2 && is_numeric($value) && is_numeric($value2)) {
if (array_key_exists('unit', $p['values'][0])) { //With unit
return new ParameterDTO(
name: $p['attributeName'],
value_min: (float)$value,
value_max: (float)$value2,
unit: $unit,
);
}
return new ParameterDTO(
name: $p['attributeName'],
value_min: (float)$value,
value_max: (float)$value2,
);
}
}
// fallback implementation
$values = implode(", ", array_map(fn($q) =>
array_key_exists('unit', $q) ? $q['value']." ". ($q['unit']['name'] ?? $q['unit']) : $q['value']
, $p['values']));
return ParameterDTO::parseValueIncludingUnit(
name: $p['attributeName'],
value: $values,
);
}, $technicalAttributes);
}
/**
* @param array $productMedia
* @return array<FileDTO>
*/
public function productMediaToDatasheets(array $productMedia): array
{
$files = [];
foreach ($productMedia['manuals'] as $manual) {
//Filter out unwanted languages
if (!empty($this->settings->attachmentLanguageFilter) && !in_array($manual['language'], $this->settings->attachmentLanguageFilter, true)) {
continue;
}
$files[] = new FileDTO($manual['fullUrl'], $manual['title'] . ' (' . $manual['language'] . ')');
}
return $files;
}
/**
* Queries prices for a given product ID. It makes a POST request to the Conrad API
* @param string $productId
* @return PurchaseInfoDTO
*/
private function queryPrices(string $productId): PurchaseInfoDTO
{
$priceQueryURL = $this->settings->shopID->getAPIRoot() . '/price-availability/4/'
. $this->settings->shopID->getShopID() . '/facade';
$response = $this->httpClient->request('POST', $priceQueryURL, [
'query' => [
'apikey' => $this->settings->apiKey,
'overrideCalculationSchema' => $this->settings->includeVAT ? 'GROSS' : 'NET'
],
'json' => [
'ns:inputArticleItemList' => [
"#namespaces" => [
"ns" => "http://www.conrad.de/ccp/basit/service/article/priceandavailabilityservice/api"
],
'articles' => [
[
"articleID" => $productId,
"calculatePrice" => true,
"checkAvailability" => true,
],
]
]
]
]);
$result = $response->toArray();
$priceInfo = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['price'] ?? [];
$price = $priceInfo['price'] ?? "0.0";
$currency = $priceInfo['currency'] ?? "EUR";
$includesVat = $priceInfo['isGrossAmount'] === "true" ?? true;
$minOrderAmount = $result['priceAndAvailabilityFacadeResponse']['priceAndAvailability']['availabilityStatus']['minimumOrderQuantity'] ?? 1;
$prices = [];
foreach ($priceInfo['priceScale'] ?? [] as $priceScale) {
$prices[] = new PriceDTO(
minimum_discount_amount: max($priceScale['scaleFrom'], $minOrderAmount),
price: (string)$priceScale['pricePerUnit'],
currency_iso_code: $currency,
includes_tax: $includesVat
);
}
if (empty($prices)) { //Fallback if no price scales are defined
$prices[] = new PriceDTO(
minimum_discount_amount: $minOrderAmount,
price: (string)$price,
currency_iso_code: $currency,
includes_tax: $includesVat
);
}
return new PurchaseInfoDTO(
distributor_name: self::DISTRIBUTOR_NAME,
order_number: $productId,
prices: $prices,
product_url: $this->getProductUrl($productId)
);
}
public function getDetails(string $id): PartDetailDTO public function getDetails(string $id): PartDetailDTO
{ {
$productInfoURL = $this->settings->shopID->getAPIRoot() . '/product/1/service/' . $this->settings->shopID->getShopID() $productInfoURL = $this->settings->shopID->getAPIRoot() . '/product/1/service/' . $this->settings->shopID->getShopID()
@ -293,28 +153,22 @@ readonly class ConradProvider implements InfoProviderInterface
return new PartDetailDTO( return new PartDetailDTO(
provider_key: $this->getProviderKey(), provider_key: $this->getProviderKey(),
provider_id: $data['shortProductNumber'], provider_id: $data['shortProductNumber'],
name: $data['productFullInformation']['manufacturer']['name'] ?? $data['productFullInformation']['manufacturer']['id'] ?? $data['shortProductNumber'], name: $data['productShortInformation']['title'],
description: $data['productShortInformation']['title'] ?? '', description: $data['productShortInformation']['shortDescription'] ?? '',
category: $data['productShortInformation']['articleGroupName'] ?? null, manufacturer: $data['brand']['displayName'] ?? null,
manufacturer: $data['brand']['displayName'] !== null ? preg_replace("/[\u{2122}\u{00ae}]/", "", $data['brand']['displayName']) : null, //Replace ™ and ® symbols
mpn: $data['productFullInformation']['manufacturer']['id'] ?? null, mpn: $data['productFullInformation']['manufacturer']['id'] ?? null,
preview_image_url: $data['productShortInformation']['mainImage']['imageUrl'] ?? null, preview_image_url: $data['productShortInformation']['mainImage']['imageUrl'] ?? null,
provider_url: $this->getProductUrl($data['shortProductNumber']), provider_url: $this->getProductUrl($data['shortProductNumber']),
footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []), footprint: $this->getFootprintFromTechnicalAttributes($data['productFullInformation']['technicalAttributes'] ?? []),
notes: $data['productFullInformation']['description'] ?? null, notes: $data['productFullInformation']['description'] ?? null,
datasheets: $this->productMediaToDatasheets($data['productMedia'] ?? []),
parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []), parameters: $this->technicalAttributesToParameters($data['productFullInformation']['technicalAttributes'] ?? []),
vendor_infos: [$this->queryPrices($data['shortProductNumber'])]
); );
} }
public function getCapabilities(): array public function getCapabilities(): array
{ {
return [ return [ProviderCapabilities::BASIC,
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE, ProviderCapabilities::PICTURE,
ProviderCapabilities::DATASHEET, ProviderCapabilities::PRICE,];
ProviderCapabilities::PRICE,
];
} }
} }

View file

@ -26,8 +26,6 @@ namespace App\Settings\InfoProviderSystem;
use App\Form\Type\APIKeyType; use App\Form\Type\APIKeyType;
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\StringType;
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;
@ -50,28 +48,10 @@ class ConradSettings
public ?string $apiKey = null; public ?string $apiKey = null;
#[SettingsParameter(label: new TM("settings.ips.conrad.shopID"), #[SettingsParameter(label: new TM("settings.ips.conrad.shopID"),
description: new TM("settings.ips.conrad.shopID.description"),
formType: EnumType::class, formType: EnumType::class,
formOptions: ['class' => ConradShopIDs::class], formOptions: ['class' => ConradShopIDs::class],
)] )]
public ConradShopIDs $shopID = ConradShopIDs::COM_B2B; public ConradShopIDs $shopID = ConradShopIDs::COM_B2B;
#[SettingsParameter(label: new TM("settings.ips.reichelt.include_vat"))]
public bool $includeVAT = true; public bool $includeVAT = true;
/**
* @var array|string[] Only attachments in these languages will be downloaded (ISO 639-1 codes)
*/
#[Assert\Unique()]
#[Assert\All([new Assert\Language()])]
#[SettingsParameter(type: ArrayType::class,
label: new TM("settings.ips.conrad.attachment_language_filter"), description: new TM("settings.ips.conrad.attachment_language_filter.description"),
options: ['type' => StringType::class],
formType: LanguageType::class,
formOptions: [
'multiple' => true,
'preferred_choices' => ['en', 'de', 'fr', 'it', 'cs', 'da', 'nl', 'hu', 'hr', 'sk', 'pl']
],
)]
public array $attachmentLanguageFilter = ['en'];
} }

View file

@ -30,22 +30,20 @@ enum ConradShopIDs: string implements TranslatableInterface
{ {
case COM_B2B = 'HP_COM_B2B'; case COM_B2B = 'HP_COM_B2B';
case DE_B2B = 'CQ_DE_B2B'; case DE_B2B = 'CQ_DE_B2B';
case DE_B2C = 'CQ_DE_B2C';
case AT_B2C = 'CQ_AT_B2C'; case AT_B2C = 'CQ_AT_B2C';
case CH_B2C_DE = 'CQ_CH_B2C_DE'; case CH_B2C = 'CQ_CH_B2C';
case CH_B2C_FR = 'CQ_CH_B2C_FR';
case SE_B2B = 'HP_SE_B2B'; case SE_B2B = 'HP_SE_B2B';
case HU_B2C = 'CQ_HU_B2C'; case HU_B2C = 'CQ_HU_B2C';
case CZ_B2B = 'HP_CZ_B2B'; case CZ_B2B = 'HP_CZ_B2B';
case SI_B2B = 'HP_SI_B2B'; case SI_B2B = 'HP_SI_B2B';
case SK_B2B = 'HP_SK_B2B'; case SK_B2B = 'HP_SK_B2B';
case BE_B2B = 'HP_BE_B2B'; case BE_B2B = 'HP_BE_B2B';
case DE_B2C = 'CQ_DE_B2C';
case PL_B2B = 'HP_PL_B2B'; case PL_B2B = 'HP_PL_B2B';
case NL_B2B = 'CQ_NL_B2B'; case NL_B2B = 'CQ_NL_B2B';
case NL_B2C = 'CQ_NL_B2C';
case DK_B2B = 'HP_DK_B2B'; case DK_B2B = 'HP_DK_B2B';
case IT_B2B = 'HP_IT_B2B'; case IT_B2B = 'HP_IT_B2B';
case NL_B2C = 'CQ_NL_B2C';
case FR_B2B = 'HP_FR_B2B'; case FR_B2B = 'HP_FR_B2B';
case AT_B2B = 'CQ_AT_B2B'; case AT_B2B = 'CQ_AT_B2B';
case HR_B2B = 'HP_HR_B2B'; case HR_B2B = 'HP_HR_B2B';
@ -56,8 +54,7 @@ enum ConradShopIDs: string implements TranslatableInterface
return match ($this) { return match ($this) {
self::DE_B2B => "conrad.de (B2B)", self::DE_B2B => "conrad.de (B2B)",
self::AT_B2C => "conrad.at (B2C)", self::AT_B2C => "conrad.at (B2C)",
self::CH_B2C_DE => "conrad.ch DE (B2C)", self::CH_B2C => "conrad.ch (B2C)",
self::CH_B2C_FR => "conrad.ch FR (B2C)",
self::SE_B2B => "conrad.se (B2B)", self::SE_B2B => "conrad.se (B2B)",
self::HU_B2C => "conrad.hu (B2C)", self::HU_B2C => "conrad.hu (B2C)",
self::CZ_B2B => "conrad.cz (B2B)", self::CZ_B2B => "conrad.cz (B2B)",
@ -67,7 +64,7 @@ enum ConradShopIDs: string implements TranslatableInterface
self::DE_B2C => "conrad.de (B2C)", self::DE_B2C => "conrad.de (B2C)",
self::PL_B2B => "conrad.pl (B2B)", self::PL_B2B => "conrad.pl (B2B)",
self::NL_B2B => "conrad.nl (B2B)", self::NL_B2B => "conrad.nl (B2B)",
self::DK_B2B => "conradelektronik.dk (B2B)", self::DK_B2B => "conrad.dk (B2B)",
self::IT_B2B => "conrad.it (B2B)", self::IT_B2B => "conrad.it (B2B)",
self::NL_B2C => "conrad.nl (B2C)", self::NL_B2C => "conrad.nl (B2C)",
self::FR_B2B => "conrad.fr (B2B)", self::FR_B2B => "conrad.fr (B2B)",
@ -79,10 +76,6 @@ enum ConradShopIDs: string implements TranslatableInterface
public function getDomain(): string public function getDomain(): string
{ {
if ($this === self::DK_B2B) {
return 'conradelektronik.dk';
}
return 'conrad.' . $this->getDomainEnd(); return 'conrad.' . $this->getDomainEnd();
} }
@ -101,10 +94,6 @@ enum ConradShopIDs: string implements TranslatableInterface
*/ */
public function getShopID(): string public function getShopID(): string
{ {
if ($this === self::CH_B2C_FR || $this === self::CH_B2C_DE) {
return 'CQ_CH_B2C';
}
return $this->value; return $this->value;
} }
@ -113,7 +102,7 @@ enum ConradShopIDs: string implements TranslatableInterface
return match ($this) { return match ($this) {
self::DE_B2B, self::DE_B2C => 'de', self::DE_B2B, self::DE_B2C => 'de',
self::AT_B2B, self::AT_B2C => 'at', self::AT_B2B, self::AT_B2C => 'at',
self::CH_B2C_DE => 'ch', self::CH_B2C_FR => 'ch', self::CH_B2C => 'ch',
self::SE_B2B => 'se', self::SE_B2B => 'se',
self::HU_B2C => 'hu', self::HU_B2C => 'hu',
self::CZ_B2B => 'cz', self::CZ_B2B => 'cz',
@ -134,7 +123,7 @@ enum ConradShopIDs: string implements TranslatableInterface
{ {
return match ($this) { return match ($this) {
self::DE_B2B, self::DE_B2C, self::AT_B2B, self::AT_B2C => 'de', self::DE_B2B, self::DE_B2C, self::AT_B2B, self::AT_B2C => 'de',
self::CH_B2C_DE => 'de', self::CH_B2C_FR => 'fr', self::CH_B2C => 'de',
self::SE_B2B => 'sv', self::SE_B2B => 'sv',
self::HU_B2C => 'hu', self::HU_B2C => 'hu',
self::CZ_B2B => 'cs', self::CZ_B2B => 'cs',
@ -161,7 +150,7 @@ enum ConradShopIDs: string implements TranslatableInterface
self::DE_B2B, self::AT_B2B, self::SE_B2B, self::CZ_B2B, self::SI_B2B, self::DE_B2B, self::AT_B2B, self::SE_B2B, self::CZ_B2B, self::SI_B2B,
self::SK_B2B, self::BE_B2B, self::PL_B2B, self::NL_B2B, self::DK_B2B, self::SK_B2B, self::BE_B2B, self::PL_B2B, self::NL_B2B, self::DK_B2B,
self::IT_B2B, self::FR_B2B, self::COM_B2B, self::HR_B2B => 'b2b', self::IT_B2B, self::FR_B2B, self::COM_B2B, self::HR_B2B => 'b2b',
self::DE_B2C, self::AT_B2C, self::CH_B2C_DE, self::CH_B2C_FR, self::HU_B2C, self::NL_B2C => 'b2c', self::DE_B2C, self::AT_B2C, self::CH_B2C, self::HU_B2C, self::NL_B2C => 'b2c',
}; };
} }
} }

View file

@ -9928,13 +9928,13 @@ Element 1 -> Element 1.2]]></target>
<unit id="NdZ1t7a" name="project.builds.number_of_builds_possible"> <unit id="NdZ1t7a" name="project.builds.number_of_builds_possible">
<segment state="translated"> <segment state="translated">
<source>project.builds.number_of_builds_possible</source> <source>project.builds.number_of_builds_possible</source>
<target><![CDATA[You have enough stocked to build <b>%max_builds%</b> builds of this [project].]]></target> <target>You have enough stocked to build &lt;b&gt;%max_builds%&lt;/b&gt; builds of this [project].</target>
</segment> </segment>
</unit> </unit>
<unit id="iuSpPbg" name="project.builds.check_project_status"> <unit id="iuSpPbg" name="project.builds.check_project_status">
<segment state="translated"> <segment state="translated">
<source>project.builds.check_project_status</source> <source>project.builds.check_project_status</source>
<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> <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>
</segment> </segment>
</unit> </unit>
<unit id="Y7vSSxi" name="project.builds.following_bom_entries_miss_instock_n"> <unit id="Y7vSSxi" name="project.builds.following_bom_entries_miss_instock_n">
@ -14286,35 +14286,5 @@ Buerklin-API Authentication server:
<target>Transport error while retrieving information from the providers. Check that your server has internet accesss. See server logs for more info.</target> <target>Transport error while retrieving information from the providers. Check that your server has internet accesss. See server logs for more info.</target>
</segment> </segment>
</unit> </unit>
<unit id="kHKChQB" name="settings.ips.conrad">
<segment>
<source>settings.ips.conrad</source>
<target>Conrad</target>
</segment>
</unit>
<unit id="gwZXJ0F" name="settings.ips.conrad.shopID">
<segment>
<source>settings.ips.conrad.shopID</source>
<target>Shop ID</target>
</segment>
</unit>
<unit id="honqnBf" name="settings.ips.conrad.shopID.description">
<segment>
<source>settings.ips.conrad.shopID.description</source>
<target>The version of the conrad store you wanna get results from. This determines language, prices and currency of the results. If both a B2B and a B2C version if available, you should choose the B2C version if you want prices including VAT. </target>
</segment>
</unit>
<unit id="EVcQbEK" name="settings.ips.conrad.attachment_language_filter">
<segment>
<source>settings.ips.conrad.attachment_language_filter</source>
<target>Language filter for attachments</target>
</segment>
</unit>
<unit id="MWPmQf2" name="settings.ips.conrad.attachment_language_filter.description">
<segment>
<source>settings.ips.conrad.attachment_language_filter.description</source>
<target>Only includes attachments in the selected languages in the results.</target>
</segment>
</unit>
</file> </file>
</xliff> </xliff>