Added info provider for Buerklin (#1151)

* Fixed Typos and mistranslations in GDPR mode (DSGVO Modus)
Fixed Typo enviroment

* Create BuerklinProvider based on LCSCProvider

* Update GET URLs for Buerklin

* Add getToken function analog to Octopart

* Remove line break in docs

* Remove trailing / in ENDPOINT_URL
Use Autowire to use values of environment variables
Remove unwanted Code from LCSC-Provider
Map json response to DTO variables

* Fix variable reference errors ($term → $keyword)
Ensure array keys exist before accessing them
Optimize API calls to prevent unnecessary requests
Improve error handling for better debugging
Enhance readability and maintainability of functions

* Bumped version v1.16.2

* Update BuerklinProvider.php

Change Order of Capabilities

* Change order of capabilities in LCSCProvider.php

* Change order of capabilities in PollinProvider.php

* Try to fix getToken BuerklinProvider.php

* Add ip_buerklin_oauth to knpu_oauth2_client.yaml

* Update buerklin authorize URL in knpu_oauth2_client.yaml

* Update knpu_oauth2_client.yaml

* Adapt Buerklin InfoProvider to new Settings mechanism

* According to Buerklin API spec it's really 'token' as urlAuthorize endpoint

* Rückgabewert ist schon ein Array deshalb kein toArray

* Fix API-Access, add image, price and parameter retrieval (Datasheets not yet implemented because it is not available in the API response)

* Add Caching of requests, use default query params (language and currency) using a function, Fix Footprint assignment, translate German code comments

* Remove DATASHEET from ProviderCapabilities because the Bürklin API doesn't include Datasheet URLs at the moment, more reverse engineering needed

* Update BuerklinSettings with existing translatable strings

* Improve Buerklin Settings Page

* Added Translation strings for Buerklin Info Provider

* Improve Buerklin Provider help message

* Adapt Buerklin-provider to new settings system

* Adapt Buerklin-provider to new settings system: add missing instance of BuerklinSettings

* Improve Compliance Parameters parsing

* Remove language-dependent RoHs Date code and use shortened ISO format, Add Customs Code without parseValueField

* Fix no results for keyword search

* Implement BatchInfoProviderInterface for Buerklin Provider

* Rename searchBatch to searchByKeywordsBatch to correctly implement BatchInfoProviderInterface

* Fix Bulk Info Provider Import for Buerklin

* Tranlate comments to English to prepare for Pull-Request

* Add phpUnit unit tests for BuerklinProvider

* Try fixing PHPStan issues

* Remove OAuthTokenManager from BuerklinProviderTest

Removed OAuthTokenManager mock from BuerklinProviderTest setup.

* Fix Settings must not be instantiated directly

* Fix UnitTest for value_typ

* edd5fb3319 (r2622249199)
Revert "Change order of capabilities in LCSCProvider.php"

This reverts commit dfd6f33e52.

* edd5fb3319 (r2622249861)
Revert "Change order of capabilities in PollinProvider.php"

This reverts commit fc2e7265be.

* Use language setting for ProductShortURL

* Update default language for Buerklin provider to English in documentation

* Add suggested improvements from SonarQube

* Removed unused use directives

* Revert SonarQube proposed change. Having more than one return is acceptable nowadays

* Improve getProviderInfo: disable oauth_app_name, add settings_class, improve disabled_help

* Implement retrieveROPCToken as proposed in https://github.com/Part-DB/Part-DB-server/pull/1151#discussion_r2622976206

* Add missing ) to retrieveROPCToken

* add use OAuthTokenManager and create instance in constructor

* Revert the following commits that tried to implement getToken using OAuthTokenManager

Revert "add use OAuthTokenManager and create instance in constructor"This reverts commit 2a1e7c9b0974ebd7e082d5a2fa62753d6254a767.Revert "Add missing ) to retrieveROPCToken"This reverts commit 8df5cfc49e.
Revert "Implement retrieveROPCToken as proposed in https://github.com/Part-DB/Part-DB-server/pull/1151#discussion_r2622976206"
This reverts commit 66cc732082.

* Remove OAuthTokenManager leftovers

* Disable buerklin provider if settings fields are empty

* Improved docs

* Added TODO comment

---------

Co-authored-by: root <root@part-db.fritz.box>
Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
This commit is contained in:
Marc 2026-01-04 21:05:47 +01:00 committed by GitHub
parent 7116c2ceb9
commit be35c36c58
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 1170 additions and 114 deletions

View file

@ -260,6 +260,24 @@ This is not an official API and could break at any time. So use it at your own r
The following env configuration options are available: The following env configuration options are available:
* `PROVIDER_POLLIN_ENABLED`: Set this to `1` to enable the Pollin provider * `PROVIDER_POLLIN_ENABLED`: Set this to `1` to enable the Pollin provider
### Buerklin
The Buerklin provider uses the [Buerklin API](https://www.buerklin.com/en/services/eprocurement/) to search for parts and get information.
To use it you have to request access to the API.
You will get an e-mail with the client ID and client secret, which you have to put in the Part-DB configuration (see below).
Please note that the Buerklin API is limited to 100 requests/minute per IP address and
access to the Authentication server is limited to 10 requests/minute per IP address
The following env configuration options are available:
* `PROVIDER_BUERKLIN_CLIENT_ID`: The client ID you got from Buerklin (mandatory)
* `PROVIDER_BUERKLIN_SECRET`: The client secret you got from Buerklin (mandatory)
* `PROVIDER_BUERKLIN_USERNAME`: The username you got from Buerklin (mandatory)
* `PROVIDER_BUERKLIN_PASSWORD`: The password you got from Buerklin (mandatory)
* `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`)
### 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

@ -0,0 +1,639 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2023 Jan Böhmer (https://github.com/jbtronics)
* Copyright (C) 2025 Marc Kreidler (https://github.com/mkne)
*
* 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\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\FileDTO;
use App\Services\InfoProviderSystem\DTOs\ParameterDTO;
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\BuerklinSettings;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class BuerklinProvider implements BatchInfoProviderInterface
{
private const ENDPOINT_URL = 'https://www.buerklin.com/buerklinws/v2/buerklin';
public const DISTRIBUTOR_NAME = 'Buerklin';
private const CACHE_TTL = 600;
/**
* Local in-request cache to avoid hitting the PSR cache repeatedly for the same product.
* @var array<string, array>
*/
private array $productCache = [];
public function __construct(
private readonly HttpClientInterface $client,
private readonly CacheItemPoolInterface $partInfoCache,
private readonly BuerklinSettings $settings,
) {
}
/**
* Gets the latest OAuth token for the Buerklin API, or creates a new one if none is available
* TODO: Rework this to use the OAuth token manager system in the database...
* @return string
*/
private function getToken(): string
{
// Cache token to avoid hammering the auth server on every request
$cacheKey = 'buerklin.oauth.token';
$item = $this->partInfoCache->getItem($cacheKey);
if ($item->isHit()) {
$token = $item->get();
if (is_string($token) && $token !== '') {
return $token;
}
}
// Buerklin OAuth2 password grant (ROPC)
$resp = $this->client->request('POST', 'https://www.buerklin.com/authorizationserver/oauth/token/', [
'headers' => [
'Accept' => 'application/json',
'Content-Type' => 'application/x-www-form-urlencoded',
],
'body' => [
'grant_type' => 'password',
'client_id' => $this->settings->clientId,
'client_secret' => $this->settings->secret,
'username' => $this->settings->username,
'password' => $this->settings->password,
],
]);
$data = $resp->toArray(false);
if (!isset($data['access_token'])) {
throw new \RuntimeException(
'Invalid token response from Buerklin: HTTP ' . $resp->getStatusCode() . ' body=' . $resp->getContent(false)
);
}
$token = (string) $data['access_token'];
// Cache for (expires_in - 30s) if available
$ttl = 300;
if (isset($data['expires_in']) && is_numeric($data['expires_in'])) {
$ttl = max(60, (int) $data['expires_in'] - 30);
}
$item->set($token);
$item->expiresAfter($ttl);
$this->partInfoCache->save($item);
return $token;
}
private function getDefaultQueryParams(): array
{
return [
'curr' => $this->settings->currency ?: 'EUR',
'language' => $this->settings->language ?: 'en',
];
}
private function getProduct(string $code): array
{
$code = strtoupper(trim($code));
if ($code === '') {
throw new \InvalidArgumentException('Product code must not be empty.');
}
$cacheKey = sprintf(
'buerklin.product.%s',
md5($code . '|' . $this->settings->language . '|' . $this->settings->currency)
);
if (isset($this->productCache[$cacheKey])) {
return $this->productCache[$cacheKey];
}
$item = $this->partInfoCache->getItem($cacheKey);
if ($item->isHit() && is_array($cached = $item->get())) {
return $this->productCache[$cacheKey] = $cached;
}
$product = $this->makeAPICall('/products/' . rawurlencode($code) . '/');
$item->set($product);
$item->expiresAfter(self::CACHE_TTL);
$this->partInfoCache->save($item);
return $this->productCache[$cacheKey] = $product;
}
private function makeAPICall(string $endpoint, array $queryParams = []): array
{
try {
$response = $this->client->request('GET', self::ENDPOINT_URL . $endpoint, [
'auth_bearer' => $this->getToken(),
'headers' => ['Accept' => 'application/json'],
'query' => array_merge($this->getDefaultQueryParams(), $queryParams),
]);
return $response->toArray();
} catch (\Exception $e) {
throw new \RuntimeException("Buerklin API request failed: " .
"Endpoint: " . $endpoint .
"Token: [redacted] " .
"QueryParams: " . json_encode($queryParams, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . " " .
"Exception message: " . $e->getMessage());
}
}
public function getProviderInfo(): array
{
return [
'name' => 'Buerklin',
'description' => 'This provider uses the Buerklin API to search for parts.',
'url' => 'https://www.buerklin.com/',
'disabled_help' => 'Configure the API Client ID, Secret, Username and Password provided by Buerklin in the provider settings to enable.',
'settings_class' => BuerklinSettings::class
];
}
public function getProviderKey(): string
{
return 'buerklin';
}
// This provider is considered active if settings are present
public function isActive(): bool
{
// The client credentials and user credentials must be set
return $this->settings->clientId !== null && $this->settings->clientId !== ''
&& $this->settings->secret !== null && $this->settings->secret !== ''
&& $this->settings->username !== null && $this->settings->username !== ''
&& $this->settings->password !== null && $this->settings->password !== '';
}
/**
* Sanitizes a field by removing any HTML tags and other unwanted characters
* @param string|null $field
* @return string|null
*/
private function sanitizeField(?string $field): ?string
{
if ($field === null) {
return null;
}
return strip_tags($field);
}
/**
* Takes a deserialized JSON object of the product and returns a PartDetailDTO
* @param array $product
* @return PartDetailDTO
*/
private function getPartDetail(array $product): PartDetailDTO
{
// If this is a search-result object, it may not contain prices/features/images -> reload full details.
if ((!isset($product['price']) && !isset($product['volumePrices'])) && isset($product['code'])) {
try {
$product = $this->getProduct((string) $product['code']);
} catch (\Throwable $e) {
// If reload fails, keep the partial product data and continue.
}
}
// Extract images from API response
$productImages = $this->getProductImages($product['images'] ?? null);
// Set preview image
$preview = $productImages[0]->url ?? null;
// Extract features (parameters) from classifications[0].features of Buerklin JSON response
$features = $product['classifications'][0]['features'] ?? [];
// Feature parameters (from classifications->features)
$featureParams = $this->attributesToParameters($features, ''); // leave group empty for normal parameters
// Compliance parameters (from top-level fields like RoHS/SVHC/…)
$complianceParams = $this->complianceToParameters($product, 'Compliance');
// Merge all parameters
$allParams = array_merge($featureParams, $complianceParams);
// Assign footprint: "Design" (en) / "Bauform" (de) / "Enclosure" (en) / "Gehäuse" (de)
$footprint = null;
if (is_array($features)) {
foreach ($features as $feature) {
$name = $feature['name'] ?? null;
if ($name === 'Design' || $name === 'Bauform' || $name === 'Enclosure' || $name === 'Gehäuse') {
$footprint = $feature['featureValues'][0]['value'] ?? null;
break;
}
}
}
// Prices: prefer volumePrices, fallback to single price
$code = (string) ($product['orderNumber'] ?? $product['code'] ?? '');
$prices = $product['volumePrices'] ?? null;
if (!is_array($prices) || count($prices) === 0) {
$pVal = $product['price']['value'] ?? null;
$pCur = $product['price']['currencyIso'] ?? ($this->settings->currency ?: 'EUR');
if (is_numeric($pVal)) {
$prices = [
[
'minQuantity' => 1,
'value' => (float) $pVal,
'currencyIso' => (string) $pCur,
]
];
} else {
$prices = [];
}
}
return new PartDetailDTO(
provider_key: $this->getProviderKey(),
provider_id: (string) ($product['code'] ?? $code),
name: (string) ($product['manufacturerProductId'] ?? $code),
description: $this->sanitizeField($product['description'] ?? null),
category: $this->sanitizeField($product['classifications'][0]['name'] ?? ($product['categories'][0]['name'] ?? null)),
manufacturer: $this->sanitizeField($product['manufacturer'] ?? null),
mpn: $this->sanitizeField($product['manufacturerProductId'] ?? null),
preview_image_url: $preview,
manufacturing_status: null,
provider_url: $this->getProductShortURL((string) ($product['code'] ?? $code)),
footprint: $footprint,
datasheets: null, // not found in JSON response, the Buerklin website however has links to datasheets
images: $productImages,
parameters: $allParams,
vendor_infos: $this->pricesToVendorInfo(
sku: $code,
url: $this->getProductShortURL($code),
prices: $prices
),
mass: $product['weight'] ?? null,
);
}
/**
* Converts the price array to a VendorInfoDTO array to be used in the PartDetailDTO
* @param string $sku
* @param string $url
* @param array $prices
* @return array
*/
private function pricesToVendorInfo(string $sku, string $url, array $prices): array
{
$priceDTOs = array_map(function ($price) {
$val = $price['value'] ?? null;
$valStr = is_numeric($val)
? number_format((float) $val, 6, '.', '') // 6 decimal places, trailing zeros are fine
: (string) $val;
// Optional: softly trim unnecessary trailing zeros (e.g. 75.550000 -> 75.55)
$valStr = rtrim(rtrim($valStr, '0'), '.');
return new PriceDTO(
minimum_discount_amount: (float) ($price['minQuantity'] ?? 1),
price: $valStr,
currency_iso_code: (string) ($price['currencyIso'] ?? $this->settings->currency ?? 'EUR'),
includes_tax: false
);
}, $prices);
return [
new PurchaseInfoDTO(
distributor_name: self::DISTRIBUTOR_NAME,
order_number: $sku,
prices: $priceDTOs,
product_url: $url,
)
];
}
/**
* Returns a valid Buerklin product short URL from product code
* @param string $product_code
* @return string
*/
private function getProductShortURL(string $product_code): string
{
return 'https://www.buerklin.com/' . $this->settings->language . '/p/' . $product_code . '/';
}
/**
* Returns a deduplicated list of product images as FileDTOs.
*
* - takes only real image arrays (with 'url' field)
* - makes relative URLs absolute
* - deduplicates using URL
* - prefers 'zoom' format, then 'product' format, then all others
*
* @param array|null $images
* @return \App\Services\InfoProviderSystem\DTOs\FileDTO[]
*/
private function getProductImages(?array $images): array
{
if (!is_array($images)) {
return [];
}
// 1) Only real image entries with URL
$imgs = array_values(array_filter($images, fn($i) => is_array($i) && !empty($i['url'])));
// 2) Prefer zoom images
$zoom = array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'zoom'));
$chosen = count($zoom) > 0
? $zoom
: array_values(array_filter($imgs, fn($i) => ($i['format'] ?? null) === 'product'));
// 3) If still none, take all
if (count($chosen) === 0) {
$chosen = $imgs;
}
// 4) Deduplicate by URL (after making absolute)
$byUrl = [];
foreach ($chosen as $img) {
$url = (string) $img['url'];
if (!str_starts_with($url, 'http://') && !str_starts_with($url, 'https://')) {
$url = 'https://www.buerklin.com' . $url;
}
if (!filter_var($url, FILTER_VALIDATE_URL)) {
continue;
}
$byUrl[$url] = $url;
}
return array_map(
fn($url) => new FileDTO($url),
array_values($byUrl)
);
}
private function attributesToParameters(array $features, ?string $group = null): array
{
$out = [];
foreach ($features as $f) {
if (!is_array($f)) {
continue;
}
$name = $f['name'] ?? null;
if (!is_string($name) || trim($name) === '') {
continue;
}
$vals = [];
foreach (($f['featureValues'] ?? []) as $fv) {
if (is_array($fv) && isset($fv['value']) && is_string($fv['value']) && trim($fv['value']) !== '') {
$vals[] = trim($fv['value']);
}
}
if (empty($vals)) {
continue;
}
// Multiple values: join with comma
$value = implode(', ', array_values(array_unique($vals)));
// Unit/symbol from Buerklin feature
$unit = $f['featureUnit']['symbol'] ?? null;
if (!is_string($unit) || trim($unit) === '') {
$unit = null;
}
// ParameterDTO parses value field (handles value + unit)
$out[] = ParameterDTO::parseValueField(
name: $name,
value: $value,
unit: $unit,
symbol: null,
group: $group
);
}
// Deduplicate by name
$byName = [];
foreach ($out as $p) {
$byName[$p->name] ??= $p;
}
return array_values($byName);
}
/**
* @return PartDetailDTO[]
*/
public function searchByKeyword(string $keyword): array
{
$keyword = strtoupper(trim($keyword));
if ($keyword === '') {
return [];
}
$response = $this->makeAPICall('/products/search/', [
'pageSize' => 50,
'currentPage' => 0,
'query' => $keyword,
'sort' => 'relevance',
]);
$products = $response['products'] ?? [];
// Normal case: products found in search results
if (is_array($products) && !empty($products)) {
return array_map(fn($p) => $this->getPartDetail($p), $products);
}
// Fallback: try direct lookup by code
try {
$product = $this->getProduct($keyword);
return [$this->getPartDetail($product)];
} catch (\Throwable $e) {
return [];
}
}
public function getDetails(string $id): PartDetailDTO
{
// Detail endpoint is /products/{code}/
$response = $this->getProduct($id);
return $this->getPartDetail($response);
}
public function getCapabilities(): array
{
return [
ProviderCapabilities::BASIC,
ProviderCapabilities::PICTURE,
//ProviderCapabilities::DATASHEET, // currently not implemented
ProviderCapabilities::PRICE,
ProviderCapabilities::FOOTPRINT,
];
}
private function complianceToParameters(array $product, ?string $group = 'Compliance'): array
{
$params = [];
$add = function (string $name, $value) use (&$params, $group) {
if ($value === null) {
return;
}
if (is_bool($value)) {
$value = $value ? 'Yes' : 'No';
} elseif (is_array($value) || is_object($value)) {
// Avoid dumping large or complex structures
return;
} else {
$value = trim((string) $value);
if ($value === '') {
return;
}
}
$params[] = ParameterDTO::parseValueField(
name: $name,
value: (string) $value,
unit: null,
symbol: null,
group: $group
);
};
$add('RoHS conform', $product['labelRoHS'] ?? null); // "yes"/"no"
$rawRoHsDate = $product['dateRoHS'] ?? null;
// Try to parse and reformat date to Y-m-d (do not use language-dependent formats)
if (is_string($rawRoHsDate) && $rawRoHsDate !== '') {
try {
$dt = new \DateTimeImmutable($rawRoHsDate);
$formatted = $dt->format('Y-m-d');
} catch (\Exception $e) {
$formatted = $rawRoHsDate;
}
// Always use the same parameter name (do not use language-dependent names)
$add('RoHS date', $formatted);
}
$add('SVHC free', $product['SVHC'] ?? null); // bool
$add('Hazardous good', $product['hazardousGood'] ?? null); // bool
$add('Hazardous materials', $product['hazardousMaterials'] ?? null); // bool
$add('Country of origin', $product['countryOfOrigin'] ?? null);
// Customs tariff code must always be stored as string, otherwise "85411000" may be stored as "8.5411e+7"
if (isset($product['articleCustomsCode'])) {
// Raw value as string
$codeRaw = (string) $product['articleCustomsCode'];
// Optionally keep only digits (in case of spaces or other characters)
$code = preg_replace('/\D/', '', $codeRaw) ?? $codeRaw;
$code = trim($code);
if ($code !== '') {
$params[] = new ParameterDTO(
name: 'Customs code',
value_text: $code,
value_typ: null,
value_min: null,
value_max: null,
unit: null,
symbol: null,
group: $group
);
}
}
return $params;
}
/**
* @param string[] $keywords
* @return array<string, SearchResultDTO[]>
*/
public function searchByKeywordsBatch(array $keywords): array
{
/** @var array<string, SearchResultDTO[]> $results */
$results = [];
foreach ($keywords as $keyword) {
$keyword = strtoupper(trim((string) $keyword));
if ($keyword === '') {
continue;
}
// Reuse existing single search -> returns PartDetailDTO[]
/** @var PartDetailDTO[] $partDetails */
$partDetails = $this->searchByKeyword($keyword);
// Convert to SearchResultDTO[]
$results[$keyword] = array_map(
fn(PartDetailDTO $detail) => $this->convertPartDetailToSearchResult($detail),
$partDetails
);
}
return $results;
}
/**
* Converts a PartDetailDTO into a SearchResultDTO for bulk search.
*/
private function convertPartDetailToSearchResult(PartDetailDTO $detail): SearchResultDTO
{
return new SearchResultDTO(
provider_key: $detail->provider_key,
provider_id: $detail->provider_id,
name: $detail->name,
description: $detail->description ?? '',
category: $detail->category ?? null,
manufacturer: $detail->manufacturer ?? null,
mpn: $detail->mpn ?? null,
preview_image_url: $detail->preview_image_url ?? null,
manufacturing_status: $detail->manufacturing_status ?? null,
provider_url: $detail->provider_url ?? null,
footprint: $detail->footprint ?? null,
);
}
}

View file

@ -0,0 +1,84 @@
<?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)
* Copyright (C) 2025 Marc Kreidler (https://github.com/mkne)
*
* 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\Form\Type\APIKeyType;
use App\Settings\SettingsIcon;
use Jbtronics\SettingsBundle\Metadata\EnvVarMode;
use Jbtronics\SettingsBundle\Settings\Settings;
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 Jbtronics\SettingsBundle\Settings\SettingsParameter;
use Symfony\Component\Validator\Constraints as Assert;
#[Settings(label: new TM("settings.ips.buerklin"), description: new TM("settings.ips.buerklin.help"))]
#[SettingsIcon("fa-plug")]
class BuerklinSettings
{
use SettingsTrait;
#[SettingsParameter(
label: new TM("settings.ips.digikey.client_id"),
formType: APIKeyType::class,
envVar: "PROVIDER_BUERKLIN_CLIENT_ID", envVarMode: EnvVarMode::OVERWRITE
)]
public ?string $clientId = null;
#[SettingsParameter(
label: new TM("settings.ips.digikey.secret"),
formType: APIKeyType::class,
envVar: "PROVIDER_BUERKLIN_SECRET", envVarMode: EnvVarMode::OVERWRITE
)]
public ?string $secret = null;
#[SettingsParameter(
label: new TM("settings.ips.buerklin.username"),
formType: APIKeyType::class,
envVar: "PROVIDER_BUERKLIN_USER", envVarMode: EnvVarMode::OVERWRITE
)]
public ?string $username = null;
#[SettingsParameter(
label: new TM("user.edit.password"),
formType: APIKeyType::class,
envVar: "PROVIDER_BUERKLIN_PASSWORD", envVarMode: EnvVarMode::OVERWRITE
)]
public ?string $password = null;
#[SettingsParameter(label: new TM("settings.ips.tme.currency"), formType: CurrencyType::class,
formOptions: ["preferred_choices" => ["EUR"]],
envVar: "PROVIDER_BUERKLIN_CURRENCY", envVarMode: EnvVarMode::OVERWRITE)]
#[Assert\Currency()]
public string $currency = "EUR";
#[SettingsParameter(label: new TM("settings.ips.tme.language"), formType: LanguageType::class,
formOptions: ["preferred_choices" => ["en", "de"]],
envVar: "PROVIDER_BUERKLIN_LANGUAGE", envVarMode: EnvVarMode::OVERWRITE)]
#[Assert\Language]
public string $language = "en";
}

View file

@ -63,4 +63,7 @@ class InfoProviderSettings
#[EmbeddedSettings] #[EmbeddedSettings]
public ?PollinSettings $pollin = null; public ?PollinSettings $pollin = null;
#[EmbeddedSettings]
public ?BuerklinSettings $buerklin = null;
} }

View file

@ -0,0 +1,271 @@
<?php
declare(strict_types=1);
namespace App\Tests\Services\InfoProviderSystem\Providers;
use App\Services\InfoProviderSystem\DTOs\PartDetailDTO;
use App\Services\InfoProviderSystem\DTOs\SearchResultDTO;
use App\Services\InfoProviderSystem\Providers\BuerklinProvider;
use App\Settings\InfoProviderSystem\BuerklinSettings;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Symfony\Contracts\HttpClient\ResponseInterface;
/**
* Full behavioral test suite for BuerklinProvider.
* Includes parameter parsing, compliance parsing, images, prices and batch mode.
*/
class BuerklinProviderTest extends TestCase
{
private HttpClientInterface $httpClient;
private CacheItemPoolInterface $cache;
private BuerklinSettings $settings;
private BuerklinProvider $provider;
protected function setUp(): void
{
$this->httpClient = $this->createMock(HttpClientInterface::class);
// Cache mock
$cacheItem = $this->createMock(CacheItemInterface::class);
$cacheItem->method('isHit')->willReturn(false);
$cacheItem->method('set')->willReturn($cacheItem);
$this->cache = $this->createMock(CacheItemPoolInterface::class);
$this->cache->method('getItem')->willReturn($cacheItem);
// IMPORTANT: Settings must not be instantiated directly (SettingsBundle forbids constructor)
$ref = new \ReflectionClass(BuerklinSettings::class);
/** @var BuerklinSettings $settings */
$settings = $ref->newInstanceWithoutConstructor();
$settings->clientId = 'CID';
$settings->secret = 'SECRET';
$settings->username = 'USER';
$settings->password = 'PASS';
$settings->language = 'en';
$settings->currency = 'EUR';
$this->settings = $settings;
$this->provider = new BuerklinProvider(
client: $this->httpClient,
partInfoCache: $this->cache,
settings: $this->settings,
);
}
private function mockApi(string $expectedUrl, array $jsonResponse): void
{
$response = $this->createMock(ResponseInterface::class);
$response->method('toArray')->willReturn($jsonResponse);
$this->httpClient
->method('request')
->with(
'GET',
$this->callback(fn($url) => str_contains((string) $url, $expectedUrl)),
$this->anything()
)
->willReturn($response);
}
public function testAttributesToParametersParsesUnitsAndValues(): void
{
$method = new \ReflectionMethod(BuerklinProvider::class, 'attributesToParameters');
$method->setAccessible(true);
$features = [
[
'name' => 'Zener voltage',
'featureUnit' => ['symbol' => 'V'],
'featureValues' => [
['value' => '12']
]
],
[
'name' => 'Length',
'featureUnit' => ['symbol' => 'mm'],
'featureValues' => [
['value' => '2.9']
]
],
[
'name' => 'Assembly',
'featureUnit' => [],
'featureValues' => [
['value' => 'SMD']
]
]
];
$params = $method->invoke($this->provider, $features, '');
$this->assertCount(3, $params);
$this->assertSame('Zener voltage', $params[0]->name);
$this->assertNull($params[0]->value_text);
$this->assertSame(12.0, $params[0]->value_typ);
$this->assertNull($params[0]->value_min);
$this->assertNull($params[0]->value_max);
$this->assertSame('V', $params[0]->unit);
$this->assertSame('Length', $params[1]->name);
$this->assertNull($params[1]->value_text);
$this->assertSame(2.9, $params[1]->value_typ);
$this->assertSame('mm', $params[1]->unit);
$this->assertSame('Assembly', $params[2]->name);
$this->assertSame('SMD', $params[2]->value_text);
$this->assertNull($params[2]->unit);
}
public function testComplianceParameters(): void
{
$method = new \ReflectionMethod(BuerklinProvider::class, 'complianceToParameters');
$method->setAccessible(true);
$product = [
'labelRoHS' => 'Yes',
'dateRoHS' => '2015-03-31T00:00+0000',
'SVHC' => true,
'hazardousGood' => false,
'hazardousMaterials' => false,
'countryOfOrigin' => 'China',
'articleCustomsCode' => '85411000'
];
$params = $method->invoke($this->provider, $product, 'Compliance');
$map = [];
foreach ($params as $p) {
$map[$p->name] = $p->value_text;
}
$this->assertSame('Yes', $map['RoHS conform']);
$this->assertSame('2015-03-31', $map['RoHS date']);
$this->assertSame('Yes', $map['SVHC free']);
$this->assertSame('No', $map['Hazardous good']);
$this->assertSame('No', $map['Hazardous materials']);
$this->assertSame('China', $map['Country of origin']);
$this->assertSame('85411000', $map['Customs code']);
}
public function testImageSelectionPrefersZoomAndDeduplicates(): void
{
$method = new \ReflectionMethod(BuerklinProvider::class, 'getProductImages');
$method->setAccessible(true);
$images = [
['format' => 'product', 'url' => '/img/a.webp'],
['format' => 'zoom', 'url' => '/img/z.webp'],
['format' => 'zoom', 'url' => '/img/z.webp'], // duplicate
['format' => 'thumbnail', 'url' => '/img/t.webp']
];
$results = $method->invoke($this->provider, $images);
$this->assertCount(1, $results);
$this->assertSame('https://www.buerklin.com/img/z.webp', $results[0]->url);
}
public function testFootprintExtraction(): void
{
$method = new \ReflectionMethod(BuerklinProvider::class, 'getPartDetail');
$method->setAccessible(true);
$product = [
'code' => 'TEST1',
'manufacturerProductId' => 'ABC',
'description' => 'X',
'images' => [],
'classifications' => [
[
'name' => 'Cat',
'features' => [
[
'name' => 'Enclosure',
'featureValues' => [['value' => 'SOT-23']]
]
]
]
],
'price' => ['value' => 1, 'currencyIso' => 'EUR']
];
$dto = $method->invoke($this->provider, $product);
$this->assertSame('SOT-23', $dto->footprint);
}
public function testPriceFormatting(): void
{
$detailPrice = [
[
'minQuantity' => 1,
'value' => 0.0885,
'currencyIso' => 'EUR'
]
];
$method = new \ReflectionMethod(BuerklinProvider::class, 'pricesToVendorInfo');
$method->setAccessible(true);
$vendorInfo = $method->invoke($this->provider, 'SKU1', 'https://x', $detailPrice);
$price = $vendorInfo[0]->prices[0];
$this->assertSame('0.0885', $price->price);
}
public function testBatchSearchReturnsSearchResultDTO(): void
{
$mockDetail = new PartDetailDTO(
provider_key: 'buerklin',
provider_id: 'TESTID',
name: 'Zener',
description: 'Desc'
);
$provider = $this->getMockBuilder(BuerklinProvider::class)
->setConstructorArgs([
$this->httpClient,
$this->cache,
$this->settings
])
->onlyMethods(['searchByKeyword'])
->getMock();
$provider->method('searchByKeyword')->willReturn([$mockDetail]);
$result = $provider->searchByKeywordsBatch(['ABC']);
$this->assertArrayHasKey('ABC', $result);
$this->assertIsArray($result['ABC']);
$this->assertCount(1, $result['ABC']);
$this->assertInstanceOf(SearchResultDTO::class, $result['ABC'][0]);
$this->assertSame('Zener', $result['ABC'][0]->name);
}
public function testConvertPartDetailToSearchResult(): void
{
$detail = new PartDetailDTO(
provider_key: 'buerklin',
provider_id: 'X1',
name: 'PartX',
description: 'D',
preview_image_url: 'https://img'
);
$method = new \ReflectionMethod(BuerklinProvider::class, 'convertPartDetailToSearchResult');
$method->setAccessible(true);
$dto = $method->invoke($this->provider, $detail);
$this->assertInstanceOf(SearchResultDTO::class, $dto);
$this->assertSame('X1', $dto->provider_id);
$this->assertSame('PartX', $dto->name);
$this->assertSame('https://img', $dto->preview_image_url);
}
}

View file

@ -1,4 +1,4 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de"> <xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
<file id="messages.de"> <file id="messages.de">
<unit id="x_wTSQS" name="attachment_type.caption"> <unit id="x_wTSQS" name="attachment_type.caption">
@ -231,7 +231,7 @@
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>part.info.timetravel_hint</source> <source>part.info.timetravel_hint</source>
<target>So sah das Bauteil vor %timestamp% aus. &lt;i&gt;Beachten Sie, dass dieses Feature experimentell ist und die angezeigten Infos daher nicht unbedingt korrekt sind.&lt;/i&gt;</target> <target><![CDATA[So sah das Bauteil vor %timestamp% aus. <i>Beachten Sie, dass dieses Feature experimentell ist und die angezeigten Infos daher nicht unbedingt korrekt sind.</i>]]></target>
</segment> </segment>
</unit> </unit>
<unit id="3exvSpl" name="standard.label"> <unit id="3exvSpl" name="standard.label">
@ -537,7 +537,7 @@
<target>Maßeinheit</target> <target>Maßeinheit</target>
</segment> </segment>
</unit> </unit>
<unit id="3bcKBzY" name="part_custom_state.caption"> <unit id="IqR.a4n" name="part_custom_state.caption">
<segment state="translated"> <segment state="translated">
<source>part_custom_state.caption</source> <source>part_custom_state.caption</source>
<target>Benutzerdefinierter Bauteilstatus</target> <target>Benutzerdefinierter Bauteilstatus</target>
@ -715,9 +715,9 @@
</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>Dies wird &lt;b&gt;alle aktiven Zwei-Faktor-Authentifizierungsmethoden des Nutzers deaktivieren&lt;/b&gt; und die &lt;b&gt;Backupcodes löschen&lt;/b&gt;! &lt;br&gt; <target><![CDATA[Dies wird <b>alle aktiven Zwei-Faktor-Authentifizierungsmethoden des Nutzers deaktivieren</b> und die <b>Backupcodes löschen</b>! <br>
Der Benutzer wird alle Zwei-Faktor-Authentifizierungmethoden neu einrichten müssen und neue Backupcodes ausdrucken müssen! &lt;br&gt;&lt;br&gt; Der Benutzer wird alle Zwei-Faktor-Authentifizierungmethoden neu einrichten müssen und neue Backupcodes ausdrucken müssen! <br><br>
&lt;b&gt;Führen sie dies nur durch, wenn Sie über die Identität des (um Hilfe suchenden) Benutzers absolut sicher sind, da ansonsten eine Kompromittierung des Accounts durch einen Angreifer erfolgen könnte!&lt;/b&gt;</target> <b>Führen sie dies nur durch, wenn Sie über die Identität des (um Hilfe suchenden) Benutzers absolut sicher sind, da ansonsten eine Kompromittierung des Accounts durch einen Angreifer erfolgen könnte!</b>]]></target>
</segment> </segment>
</unit> </unit>
<unit id="APsHYu0" name="user.edit.tfa.disable_tfa.btn"> <unit id="APsHYu0" name="user.edit.tfa.disable_tfa.btn">
@ -1424,7 +1424,7 @@ Subelemente werden beim Löschen nach oben verschoben.</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>homepage.github.text</source> <source>homepage.github.text</source>
<target>Quellcode, Downloads, Bugreports, ToDo-Liste usw. gibts auf der &lt;a class="link-external" target="_blank" href="%href%"&gt;GitHub Projektseite&lt;/a&gt;</target> <target><![CDATA[Quellcode, Downloads, Bugreports, ToDo-Liste usw. gibts auf der <a class="link-external" target="_blank" href="%href%">GitHub Projektseite</a>]]></target>
</segment> </segment>
</unit> </unit>
<unit id="D5OKsgU" name="homepage.help.caption"> <unit id="D5OKsgU" name="homepage.help.caption">
@ -1446,7 +1446,7 @@ Subelemente werden beim Löschen nach oben verschoben.</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>homepage.help.text</source> <source>homepage.help.text</source>
<target>Hilfe und Tipps finden sie im &lt;a class="link-external" rel="noopener" target="_blank" href="%href%"&gt;Wiki&lt;/a&gt; der GitHub Seite.</target> <target><![CDATA[Hilfe und Tipps finden sie im <a class="link-external" rel="noopener" target="_blank" href="%href%">Wiki</a> der GitHub Seite.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="dnirx4v" name="homepage.forum.caption"> <unit id="dnirx4v" name="homepage.forum.caption">
@ -1688,7 +1688,7 @@ Subelemente werden beim Löschen nach oben verschoben.</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>email.pw_reset.fallback</source> <source>email.pw_reset.fallback</source>
<target>Wenn dies nicht funktioniert, rufen Sie &lt;a href="%url%"&gt;%url%&lt;/a&gt; auf und geben Sie die folgenden Daten ein</target> <target><![CDATA[Wenn dies nicht funktioniert, rufen Sie <a href="%url%">%url%</a> auf und geben Sie die folgenden Daten ein]]></target>
</segment> </segment>
</unit> </unit>
<unit id="DduL9Hu" name="email.pw_reset.username"> <unit id="DduL9Hu" name="email.pw_reset.username">
@ -1718,7 +1718,7 @@ Subelemente werden beim Löschen nach oben verschoben.</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>email.pw_reset.valid_unit %date%</source> <source>email.pw_reset.valid_unit %date%</source>
<target>Das Reset-Token ist gültig bis &lt;i&gt;%date%&lt;/i&gt;</target> <target><![CDATA[Das Reset-Token ist gültig bis <i>%date%</i>]]></target>
</segment> </segment>
</unit> </unit>
<unit id="8sBnjRy" name="orderdetail.delete"> <unit id="8sBnjRy" name="orderdetail.delete">
@ -3591,8 +3591,8 @@ Subelemente werden beim Löschen nach oben verschoben.</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>tfa_google.disable.confirm_message</source> <source>tfa_google.disable.confirm_message</source>
<target>Wenn Sie die Authenticator App deaktivieren, werden alle Backupcodes gelöscht, daher sie müssen sie evtl. neu ausdrucken.&lt;br&gt; <target><![CDATA[Wenn Sie die Authenticator App deaktivieren, werden alle Backupcodes gelöscht, daher sie müssen sie evtl. neu ausdrucken.<br>
Beachten Sie außerdem, dass ihr Account ohne Zwei-Faktor-Authentifizierung nicht mehr so gut gegen Angreifer geschützt ist!</target> Beachten Sie außerdem, dass ihr Account ohne Zwei-Faktor-Authentifizierung nicht mehr so gut gegen Angreifer geschützt ist!]]></target>
</segment> </segment>
</unit> </unit>
<unit id="yu9MSt5" name="tfa_google.disabled_message"> <unit id="yu9MSt5" name="tfa_google.disabled_message">
@ -3612,7 +3612,7 @@ Beachten Sie außerdem, dass ihr Account ohne Zwei-Faktor-Authentifizierung nich
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>tfa_google.step.download</source> <source>tfa_google.step.download</source>
<target>Laden Sie eine Authenticator App herunter (z.B. &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[Laden Sie eine Authenticator App herunter (z.B. <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> </segment>
</unit> </unit>
<unit id="eriwJoR" name="tfa_google.step.scan"> <unit id="eriwJoR" name="tfa_google.step.scan">
@ -3854,8 +3854,8 @@ Beachten Sie außerdem, dass ihr Account ohne Zwei-Faktor-Authentifizierung nich
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>tfa_trustedDevices.explanation</source> <source>tfa_trustedDevices.explanation</source>
<target>Bei der Überprüfung des zweiten Faktors, kann der aktuelle Computer als vertrauenswürdig gekennzeichnet werden, daher werden keine Zwei-Faktor-Überprüfungen mehr an diesem Computer benötigt. <target><![CDATA[Bei der Überprüfung des zweiten Faktors, kann der aktuelle Computer als vertrauenswürdig gekennzeichnet werden, daher werden keine Zwei-Faktor-Überprüfungen mehr an diesem Computer benötigt.
Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertrauenswürdig ist, können Sie hier den Status &lt;i&gt;aller &lt;/i&gt;Computer zurücksetzen.</target> Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertrauenswürdig ist, können Sie hier den Status <i>aller </i>Computer zurücksetzen.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="FZINq8z" name="tfa_trustedDevices.invalidate.confirm_title"> <unit id="FZINq8z" name="tfa_trustedDevices.invalidate.confirm_title">
@ -4813,7 +4813,7 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
<target>Maßeinheit</target> <target>Maßeinheit</target>
</segment> </segment>
</unit> </unit>
<unit id="G1hmQdb" name="part.table.partCustomState"> <unit id="JjTO6Nq" name="part.table.partCustomState">
<segment state="translated"> <segment state="translated">
<source>part.table.partCustomState</source> <source>part.table.partCustomState</source>
<target>Benutzerdefinierter Bauteilstatus</target> <target>Benutzerdefinierter Bauteilstatus</target>
@ -5301,7 +5301,7 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>label_options.lines_mode.help</source> <source>label_options.lines_mode.help</source>
<target>Wenn Sie hier Twig auswählen, wird das Contentfeld als Twig-Template interpretiert. Weitere Hilfe gibt es in der &lt;a href="https://twig.symfony.com/doc/3.x/templates.html"&gt;Twig Dokumentation&lt;/a&gt; und dem &lt;a href="https://docs.part-db.de/usage/labels.html#twig-mode"&gt;Wiki&lt;/a&gt;.</target> <target><![CDATA[Wenn Sie hier Twig auswählen, wird das Contentfeld als Twig-Template interpretiert. Weitere Hilfe gibt es in der <a href="https://twig.symfony.com/doc/3.x/templates.html">Twig Dokumentation</a> und dem <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a>.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="isvxbiX" name="label_options.page_size.label"> <unit id="isvxbiX" name="label_options.page_size.label">
@ -5683,7 +5683,7 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
<target>Maßeinheit</target> <target>Maßeinheit</target>
</segment> </segment>
</unit> </unit>
<unit id="kE1wJ1a" name="part.edit.partCustomState"> <unit id="ro8Iwr_" name="part.edit.partCustomState">
<segment state="translated"> <segment state="translated">
<source>part.edit.partCustomState</source> <source>part.edit.partCustomState</source>
<target>Benutzerdefinierter Bauteilstatus</target> <target>Benutzerdefinierter Bauteilstatus</target>
@ -5976,7 +5976,7 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
<target>Maßeinheit</target> <target>Maßeinheit</target>
</segment> </segment>
</unit> </unit>
<unit id="a1mPcMw" name="part_custom_state.label"> <unit id="NpDx4rr" name="part_custom_state.label">
<segment state="translated"> <segment state="translated">
<source>part_custom_state.label</source> <source>part_custom_state.label</source>
<target>Benutzerdefinierter Bauteilstatus</target> <target>Benutzerdefinierter Bauteilstatus</target>
@ -6225,7 +6225,7 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
<target>[[Measurement_unit]]</target> <target>[[Measurement_unit]]</target>
</segment> </segment>
</unit> </unit>
<unit id="5adacKb" name="tree.tools.edit.part_custom_state"> <unit id="oYLWbbv" name="tree.tools.edit.part_custom_state">
<segment state="translated"> <segment state="translated">
<source>tree.tools.edit.part_custom_state</source> <source>tree.tools.edit.part_custom_state</source>
<target>[[Part_custom_state]]</target> <target>[[Part_custom_state]]</target>
@ -7149,15 +7149,15 @@ Wenn Sie dies fehlerhafterweise gemacht haben oder ein Computer nicht mehr vertr
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>mass_creation.lines.placeholder</source> <source>mass_creation.lines.placeholder</source>
<target>Element 1 <target><![CDATA[Element 1
Element 1.1 Element 1.1
Element 1.1.1 Element 1.1.1
Element 1.2 Element 1.2
Element 2 Element 2
Element 3 Element 3
Element 1 -&gt; Element 1.1 Element 1 -> Element 1.1
Element 1 -&gt; Element 1.2</target> Element 1 -> Element 1.2]]></target>
</segment> </segment>
</unit> </unit>
<unit id="TWSqPFi" name="entity.mass_creation.btn"> <unit id="TWSqPFi" name="entity.mass_creation.btn">
@ -8372,7 +8372,7 @@ Element 1 -&gt; Element 1.2</target>
<target>Maßeinheiten</target> <target>Maßeinheiten</target>
</segment> </segment>
</unit> </unit>
<unit id="1b5ja1c" name="perm.part_custom_states"> <unit id="zckNn8G" name="perm.part_custom_states">
<segment state="translated"> <segment state="translated">
<source>perm.part_custom_states</source> <source>perm.part_custom_states</source>
<target>Benutzerdefinierter Bauteilstatus</target> <target>Benutzerdefinierter Bauteilstatus</target>
@ -9303,25 +9303,25 @@ Element 1 -&gt; Element 1.2</target>
<unit id="r4vDLAt" name="filter.parameter_value_constraint.operator.&lt;"> <unit id="r4vDLAt" name="filter.parameter_value_constraint.operator.&lt;">
<segment state="translated"> <segment state="translated">
<source>filter.parameter_value_constraint.operator.&lt;</source> <source>filter.parameter_value_constraint.operator.&lt;</source>
<target>Typ. Wert &lt;</target> <target><![CDATA[Typ. Wert <]]></target>
</segment> </segment>
</unit> </unit>
<unit id="X9SA3UP" name="filter.parameter_value_constraint.operator.&gt;"> <unit id="X9SA3UP" name="filter.parameter_value_constraint.operator.&gt;">
<segment state="translated"> <segment state="translated">
<source>filter.parameter_value_constraint.operator.&gt;</source> <source>filter.parameter_value_constraint.operator.&gt;</source>
<target>Typ. Wert &gt;</target> <target><![CDATA[Typ. Wert >]]></target>
</segment> </segment>
</unit> </unit>
<unit id="BQGaoQS" name="filter.parameter_value_constraint.operator.&lt;="> <unit id="BQGaoQS" name="filter.parameter_value_constraint.operator.&lt;=">
<segment state="translated"> <segment state="translated">
<source>filter.parameter_value_constraint.operator.&lt;=</source> <source>filter.parameter_value_constraint.operator.&lt;=</source>
<target>Typ. Wert &lt;=</target> <target><![CDATA[Typ. Wert <=]]></target>
</segment> </segment>
</unit> </unit>
<unit id="2ha3P6g" name="filter.parameter_value_constraint.operator.&gt;="> <unit id="2ha3P6g" name="filter.parameter_value_constraint.operator.&gt;=">
<segment state="translated"> <segment state="translated">
<source>filter.parameter_value_constraint.operator.&gt;=</source> <source>filter.parameter_value_constraint.operator.&gt;=</source>
<target>Typ. Wert &gt;=</target> <target><![CDATA[Typ. Wert >=]]></target>
</segment> </segment>
</unit> </unit>
<unit id="4DaBace" name="filter.parameter_value_constraint.operator.BETWEEN"> <unit id="4DaBace" name="filter.parameter_value_constraint.operator.BETWEEN">
@ -9429,7 +9429,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="4tHhDtU" name="parts_list.search.searching_for"> <unit id="4tHhDtU" name="parts_list.search.searching_for">
<segment state="translated"> <segment state="translated">
<source>parts_list.search.searching_for</source> <source>parts_list.search.searching_for</source>
<target>Suche Teile mit dem Suchbegriff &lt;b&gt;%keyword%&lt;/b&gt;</target> <target><![CDATA[Suche Teile mit dem Suchbegriff <b>%keyword%</b>]]></target>
</segment> </segment>
</unit> </unit>
<unit id="4vomKLa" name="parts_list.search_options.caption"> <unit id="4vomKLa" name="parts_list.search_options.caption">
@ -10089,13 +10089,13 @@ Element 1 -&gt; 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>Sie haben genug Bauteile auf Lager, um &lt;b&gt;%max_builds%&lt;/b&gt; Exemplare dieses Projektes zu bauen.</target> <target><![CDATA[Sie haben genug Bauteile auf Lager, um <b>%max_builds%</b> Exemplare dieses Projektes zu bauen.]]></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>Der aktuelle Projektstatus ist &lt;b&gt;"%project_status%"&lt;/b&gt;. Sie sollten überprüfen, ob sie das Projekt mit diesem Status wirklich bauen wollen!</target> <target><![CDATA[Der aktuelle Projektstatus ist <b>"%project_status%"</b>. Sie sollten überprüfen, ob sie das Projekt mit diesem Status wirklich bauen wollen!]]></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">
@ -10209,7 +10209,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="GzqIwHH" name="entity.select.add_hint"> <unit id="GzqIwHH" name="entity.select.add_hint">
<segment state="translated"> <segment state="translated">
<source>entity.select.add_hint</source> <source>entity.select.add_hint</source>
<target>Nutzen Sie -&gt; um verschachtelte Strukturen anzulegen, z.B. "Element 1-&gt;Element 1.1"</target> <target><![CDATA[Nutzen Sie -> um verschachtelte Strukturen anzulegen, z.B. "Element 1->Element 1.1"]]></target>
</segment> </segment>
</unit> </unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB"> <unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
@ -10233,13 +10233,13 @@ Element 1 -&gt; Element 1.2</target>
<unit id="XLnXtsR" name="homepage.first_steps.introduction"> <unit id="XLnXtsR" name="homepage.first_steps.introduction">
<segment state="translated"> <segment state="translated">
<source>homepage.first_steps.introduction</source> <source>homepage.first_steps.introduction</source>
<target>Die Datenbank ist momentan noch leer. Sie möchten möglicherweise die &lt;a href="%url%"&gt;Dokumentation&lt;/a&gt; lesen oder anfangen, die folgenden Datenstrukturen anzulegen.</target> <target><![CDATA[Die Datenbank ist momentan noch leer. Sie möchten möglicherweise die <a href="%url%">Dokumentation</a> lesen oder anfangen, die folgenden Datenstrukturen anzulegen.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="Q79MOIk" name="homepage.first_steps.create_part"> <unit id="Q79MOIk" name="homepage.first_steps.create_part">
<segment state="translated"> <segment state="translated">
<source>homepage.first_steps.create_part</source> <source>homepage.first_steps.create_part</source>
<target>Oder Sie können direkt ein &lt;a href="%url%"&gt;neues Bauteil erstellen&lt;/a&gt;.</target> <target><![CDATA[Oder Sie können direkt ein <a href="%url%">neues Bauteil erstellen</a>.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="vplYq4f" name="homepage.first_steps.hide_hint"> <unit id="vplYq4f" name="homepage.first_steps.hide_hint">
@ -10251,7 +10251,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="MJoZl4f" name="homepage.forum.text"> <unit id="MJoZl4f" name="homepage.forum.text">
<segment state="translated"> <segment state="translated">
<source>homepage.forum.text</source> <source>homepage.forum.text</source>
<target>Für Fragen rund um Part-DB, nutze das &lt;a class="link-external" rel="noopener" target="_blank" href="%href%"&gt;Diskussionsforum&lt;/a&gt;</target> <target><![CDATA[Für Fragen rund um Part-DB, nutze das <a class="link-external" rel="noopener" target="_blank" href="%href%">Diskussionsforum</a>]]></target>
</segment> </segment>
</unit> </unit>
<unit id="YsukbnK" name="log.element_edited.changed_fields.category"> <unit id="YsukbnK" name="log.element_edited.changed_fields.category">
@ -10752,7 +10752,7 @@ Element 1 -&gt; Element 1.2</target>
<target>Maßeinheit</target> <target>Maßeinheit</target>
</segment> </segment>
</unit> </unit>
<unit id="2COnw1k" name="log.element_edited.changed_fields.partCustomState"> <unit id="8QD.2.r" name="log.element_edited.changed_fields.partCustomState">
<segment state="translated"> <segment state="translated">
<source>log.element_edited.changed_fields.partCustomState</source> <source>log.element_edited.changed_fields.partCustomState</source>
<target>Benutzerdefinierter Bauteilstatus</target> <target>Benutzerdefinierter Bauteilstatus</target>
@ -10917,7 +10917,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="p_IxB9K" name="parts.import.help_documentation"> <unit id="p_IxB9K" name="parts.import.help_documentation">
<segment state="translated"> <segment state="translated">
<source>parts.import.help_documentation</source> <source>parts.import.help_documentation</source>
<target>Konsultieren Sie die &lt;a href="%link%"&gt;Dokumentation&lt;/a&gt; für weiter Informationen über das Dateiformat.</target> <target><![CDATA[Konsultieren Sie die <a href="%link%">Dokumentation</a> für weiter Informationen über das Dateiformat.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="awbvhVq" name="parts.import.help"> <unit id="awbvhVq" name="parts.import.help">
@ -11022,13 +11022,13 @@ Element 1 -&gt; Element 1.2</target>
<target>Bearbeite [Measurement_unit]</target> <target>Bearbeite [Measurement_unit]</target>
</segment> </segment>
</unit> </unit>
<unit id="ba52d.g" name="part_custom_state.new"> <unit id="Ae0GMtY" name="part_custom_state.new">
<segment state="translated"> <segment state="translated">
<source>part_custom_state.new</source> <source>part_custom_state.new</source>
<target>Neuer [Part_custom_state]</target> <target>Neuer [Part_custom_state]</target>
</segment> </segment>
</unit> </unit>
<unit id="c1.gb2d" name="part_custom_state.edit"> <unit id="5uZ23wR" name="part_custom_state.edit">
<segment state="translated"> <segment state="translated">
<source>part_custom_state.edit</source> <source>part_custom_state.edit</source>
<target>Bearbeite [Part_custom_state]</target> <target>Bearbeite [Part_custom_state]</target>
@ -11109,7 +11109,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="o5u.Nnz" name="part.filter.lessThanDesired"> <unit id="o5u.Nnz" name="part.filter.lessThanDesired">
<segment state="translated"> <segment state="translated">
<source>part.filter.lessThanDesired</source> <source>part.filter.lessThanDesired</source>
<target>Weniger vorhanden als gewünscht (Gesamtmenge &lt; Mindestmenge)</target> <target><![CDATA[Weniger vorhanden als gewünscht (Gesamtmenge < Mindestmenge)]]></target>
</segment> </segment>
</unit> </unit>
<unit id="YN9eLcZ" name="part.filter.lotOwner"> <unit id="YN9eLcZ" name="part.filter.lotOwner">
@ -11915,13 +11915,13 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="i68lU5x" name="part.merge.confirm.title"> <unit id="i68lU5x" name="part.merge.confirm.title">
<segment state="translated"> <segment state="translated">
<source>part.merge.confirm.title</source> <source>part.merge.confirm.title</source>
<target>Möchten Sie wirklich &lt;b&gt;%other%&lt;/b&gt; in &lt;b&gt;%target%&lt;/b&gt; zusammenführen?</target> <target><![CDATA[Möchten Sie wirklich <b>%other%</b> in <b>%target%</b> zusammenführen?]]></target>
</segment> </segment>
</unit> </unit>
<unit id="k0anzYV" name="part.merge.confirm.message"> <unit id="k0anzYV" name="part.merge.confirm.message">
<segment state="translated"> <segment state="translated">
<source>part.merge.confirm.message</source> <source>part.merge.confirm.message</source>
<target>&lt;b&gt;%other%&lt;/b&gt; wird gelöscht, und das aktuelle Bauteil wird mit den angezeigten Daten gespeichert.</target> <target><![CDATA[<b>%other%</b> wird gelöscht, und das aktuelle Bauteil wird mit den angezeigten Daten gespeichert.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="mmW5Yl1" name="part.info.merge_modal.title"> <unit id="mmW5Yl1" name="part.info.merge_modal.title">
@ -12275,7 +12275,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="p7LGAIX" name="settings.ips.element14.apiKey.help"> <unit id="p7LGAIX" name="settings.ips.element14.apiKey.help">
<segment state="translated"> <segment state="translated">
<source>settings.ips.element14.apiKey.help</source> <source>settings.ips.element14.apiKey.help</source>
<target>Sie können sich unter &lt;a href="https://partner.element14.com/"&gt;https://partner.element14.com/&lt;/a&gt; für einen API-Schlüssel registrieren.</target> <target><![CDATA[Sie können sich unter <a href="https://partner.element14.com/">https://partner.element14.com/</a> für einen API-Schlüssel registrieren.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="ZdUHpZc" name="settings.ips.element14.storeId"> <unit id="ZdUHpZc" name="settings.ips.element14.storeId">
@ -12287,7 +12287,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="XXGUxF6" name="settings.ips.element14.storeId.help"> <unit id="XXGUxF6" name="settings.ips.element14.storeId.help">
<segment state="translated"> <segment state="translated">
<source>settings.ips.element14.storeId.help</source> <source>settings.ips.element14.storeId.help</source>
<target>Die Domain des Shops, aus dem die Daten abgerufen werden sollen. Diese bestimmt die Sprache und Währung der Ergebnisse. Eine Liste der gültigen Domains finden Sie &lt;a href="https://partner.element14.com/docs/Product_Search_API_REST__Description"&gt;hier&lt;/a&gt;.</target> <target><![CDATA[Die Domain des Shops, aus dem die Daten abgerufen werden sollen. Diese bestimmt die Sprache und Währung der Ergebnisse. Eine Liste der gültigen Domains finden Sie <a href="https://partner.element14.com/docs/Product_Search_API_REST__Description">hier</a>.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="WKWZIm2" name="settings.ips.tme"> <unit id="WKWZIm2" name="settings.ips.tme">
@ -12305,7 +12305,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="_pYLrPT" name="settings.ips.tme.token.help"> <unit id="_pYLrPT" name="settings.ips.tme.token.help">
<segment state="translated"> <segment state="translated">
<source>settings.ips.tme.token.help</source> <source>settings.ips.tme.token.help</source>
<target>Sie können einen API-Token und einen geheimen Schlüssel unter &lt;a href="https://developers.tme.eu/en/"&gt;https://developers.tme.eu/en/&lt;/a&gt; erhalten.</target> <target><![CDATA[Sie können einen API-Token und einen geheimen Schlüssel unter <a href="https://developers.tme.eu/en/">https://developers.tme.eu/en/</a> erhalten.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="yswx4bq" name="settings.ips.tme.secret"> <unit id="yswx4bq" name="settings.ips.tme.secret">
@ -12353,7 +12353,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="gu.JlpT" name="settings.ips.mouser.apiKey.help"> <unit id="gu.JlpT" name="settings.ips.mouser.apiKey.help">
<segment state="translated"> <segment state="translated">
<source>settings.ips.mouser.apiKey.help</source> <source>settings.ips.mouser.apiKey.help</source>
<target>Sie können sich unter &lt;a href="https://eu.mouser.com/api-hub/"&gt;https://eu.mouser.com/api-hub/&lt;/a&gt; für einen API-Schlüssel registrieren.</target> <target><![CDATA[Sie können sich unter <a href="https://eu.mouser.com/api-hub/">https://eu.mouser.com/api-hub/</a> für einen API-Schlüssel registrieren.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="Q66CNjw" name="settings.ips.mouser.searchLimit"> <unit id="Q66CNjw" name="settings.ips.mouser.searchLimit">
@ -12401,7 +12401,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="xU8_Qw." name="settings.ips.mouser.searchOptions.rohsAndInStock"> <unit id="xU8_Qw." name="settings.ips.mouser.searchOptions.rohsAndInStock">
<segment state="translated"> <segment state="translated">
<source>settings.ips.mouser.searchOptions.rohsAndInStock</source> <source>settings.ips.mouser.searchOptions.rohsAndInStock</source>
<target>Sofort verfügbar &amp; RoHS konform</target> <target><![CDATA[Sofort verfügbar & RoHS konform]]></target>
</segment> </segment>
</unit> </unit>
<unit id="fQYt0Om" name="settings.ips.lcsc"> <unit id="fQYt0Om" name="settings.ips.lcsc">
@ -12431,7 +12431,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="kKv0J3." name="settings.system.attachments"> <unit id="kKv0J3." name="settings.system.attachments">
<segment state="translated"> <segment state="translated">
<source>settings.system.attachments</source> <source>settings.system.attachments</source>
<target>Anhänge &amp; Dateien</target> <target><![CDATA[Anhänge & Dateien]]></target>
</segment> </segment>
</unit> </unit>
<unit id="dsRff8T" name="settings.system.attachments.maxFileSize"> <unit id="dsRff8T" name="settings.system.attachments.maxFileSize">
@ -12455,7 +12455,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="T.PBu5P" name="settings.system.attachments.allowDownloads.help"> <unit id="T.PBu5P" name="settings.system.attachments.allowDownloads.help">
<segment state="translated"> <segment state="translated">
<source>settings.system.attachments.allowDownloads.help</source> <source>settings.system.attachments.allowDownloads.help</source>
<target>Mit dieser Option können Benutzer externe Dateien in die Part-DB herunterladen, indem sie eine URL angeben. &lt;b&gt;Achtung: Dies kann ein Sicherheitsrisiko darstellen, da Benutzer dadurch möglicherweise über die Part-DB auf Intranet-Ressourcen zugreifen können!&lt;/b&gt;</target> <target><![CDATA[Mit dieser Option können Benutzer externe Dateien in die Part-DB herunterladen, indem sie eine URL angeben. <b>Achtung: Dies kann ein Sicherheitsrisiko darstellen, da Benutzer dadurch möglicherweise über die Part-DB auf Intranet-Ressourcen zugreifen können!</b>]]></target>
</segment> </segment>
</unit> </unit>
<unit id=".OyihML" name="settings.system.attachments.downloadByDefault"> <unit id=".OyihML" name="settings.system.attachments.downloadByDefault">
@ -12629,8 +12629,8 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="0GRlEe5" name="settings.system.localization.base_currency_description"> <unit id="0GRlEe5" name="settings.system.localization.base_currency_description">
<segment state="translated"> <segment state="translated">
<source>settings.system.localization.base_currency_description</source> <source>settings.system.localization.base_currency_description</source>
<target>Die Währung, in der Preisinformationen und Wechselkurse gespeichert werden. Diese Währung wird angenommen, wenn für eine Preisinformation keine Währung festgelegt ist. <target><![CDATA[Die Währung, in der Preisinformationen und Wechselkurse gespeichert werden. Diese Währung wird angenommen, wenn für eine Preisinformation keine Währung festgelegt ist.
&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> <b>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!</b>]]></target>
</segment> </segment>
</unit> </unit>
<unit id="cvpTUeY" name="settings.system.privacy"> <unit id="cvpTUeY" name="settings.system.privacy">
@ -12660,7 +12660,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="w07P3Dt" name="settings.misc.kicad_eda.category_depth.help"> <unit id="w07P3Dt" name="settings.misc.kicad_eda.category_depth.help">
<segment state="translated"> <segment state="translated">
<source>settings.misc.kicad_eda.category_depth.help</source> <source>settings.misc.kicad_eda.category_depth.help</source>
<target>Dieser Wert bestimmt die Tiefe des Kategoriebaums, der in KiCad sichtbar ist. 0 bedeutet, dass nur die Kategorien der obersten Ebene sichtbar sind. Setzen Sie den Wert auf &gt; 0, um weitere Ebenen anzuzeigen. Setzen Sie den Wert auf -1, um alle Teile der Part-DB innerhalb einer einzigen Kategorie in KiCad anzuzeigen.</target> <target><![CDATA[Dieser Wert bestimmt die Tiefe des Kategoriebaums, der in KiCad sichtbar ist. 0 bedeutet, dass nur die Kategorien der obersten Ebene sichtbar sind. Setzen Sie den Wert auf > 0, um weitere Ebenen anzuzeigen. Setzen Sie den Wert auf -1, um alle Teile der Part-DB innerhalb einer einzigen Kategorie in KiCad anzuzeigen.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="VwvmcWE" name="settings.behavior.sidebar"> <unit id="VwvmcWE" name="settings.behavior.sidebar">
@ -12678,7 +12678,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="jc0JTvL" name="settings.behavior.sidebar.items.help"> <unit id="jc0JTvL" name="settings.behavior.sidebar.items.help">
<segment state="translated"> <segment state="translated">
<source>settings.behavior.sidebar.items.help</source> <source>settings.behavior.sidebar.items.help</source>
<target>Die Menüs, die standardmäßig in der Seitenleiste angezeigt werden. Die Reihenfolge der Elemente kann per Drag &amp; Drop geändert werden.</target> <target><![CDATA[Die Menüs, die standardmäßig in der Seitenleiste angezeigt werden. Die Reihenfolge der Elemente kann per Drag & Drop geändert werden.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="gVSWDkE" name="settings.behavior.sidebar.rootNodeEnabled"> <unit id="gVSWDkE" name="settings.behavior.sidebar.rootNodeEnabled">
@ -12726,7 +12726,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="SUD8H3b" name="settings.behavior.table.parts_default_columns.help"> <unit id="SUD8H3b" name="settings.behavior.table.parts_default_columns.help">
<segment state="translated"> <segment state="translated">
<source>settings.behavior.table.parts_default_columns.help</source> <source>settings.behavior.table.parts_default_columns.help</source>
<target>Die Spalten, die standardmäßig in Bauteiltabellen angezeigt werden sollen. Die Reihenfolge der Elemente kann per Drag &amp; Drop geändert werden.</target> <target><![CDATA[Die Spalten, die standardmäßig in Bauteiltabellen angezeigt werden sollen. Die Reihenfolge der Elemente kann per Drag & Drop geändert werden.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="hazr_g5" name="settings.ips.oemsecrets"> <unit id="hazr_g5" name="settings.ips.oemsecrets">
@ -12780,7 +12780,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="KLJYfJ0" name="settings.ips.oemsecrets.sortMode.M"> <unit id="KLJYfJ0" name="settings.ips.oemsecrets.sortMode.M">
<segment state="translated"> <segment state="translated">
<source>settings.ips.oemsecrets.sortMode.M</source> <source>settings.ips.oemsecrets.sortMode.M</source>
<target>Vollständigkeit &amp; Herstellername</target> <target><![CDATA[Vollständigkeit & Herstellername]]></target>
</segment> </segment>
</unit> </unit>
<unit id="8C9ijHM" name="entity.export.flash.error.no_entities"> <unit id="8C9ijHM" name="entity.export.flash.error.no_entities">
@ -13440,7 +13440,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="FsrRdkp" name="settings.behavior.homepage.items.help"> <unit id="FsrRdkp" name="settings.behavior.homepage.items.help">
<segment state="translated"> <segment state="translated">
<source>settings.behavior.homepage.items.help</source> <source>settings.behavior.homepage.items.help</source>
<target>Die Elemente, die auf der Startseite angezeigt werden sollen. Die Reihenfolge kann per Drag &amp; Drop geändert werden.</target> <target><![CDATA[Die Elemente, die auf der Startseite angezeigt werden sollen. Die Reihenfolge kann per Drag & Drop geändert werden.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="CYw3_pS" name="settings.system.customization.showVersionOnHomepage"> <unit id="CYw3_pS" name="settings.system.customization.showVersionOnHomepage">
@ -14154,7 +14154,7 @@ Bitte beachten Sie, dass Sie sich nicht als deaktivierter Benutzer ausgeben kön
<unit id="Ej2znKK" name="settings.system.localization.language_menu_entries.description"> <unit id="Ej2znKK" name="settings.system.localization.language_menu_entries.description">
<segment state="translated"> <segment state="translated">
<source>settings.system.localization.language_menu_entries.description</source> <source>settings.system.localization.language_menu_entries.description</source>
<target>Die Sprachen, die im Sprachen Dropdown-Menü angezeigt werden sollen. Die Reihenfolge kann via Drag&amp;Drop geändert werden. Lassen Sie das Feld leer, um alle verfügbaren Sprachen anzuzeigen.</target> <target><![CDATA[Die Sprachen, die im Sprachen Dropdown-Menü angezeigt werden sollen. Die Reihenfolge kann via Drag&Drop geändert werden. Lassen Sie das Feld leer, um alle verfügbaren Sprachen anzuzeigen.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="xIZ_mEX" name="project.builds.no_bom_entries"> <unit id="xIZ_mEX" name="project.builds.no_bom_entries">
@ -14430,13 +14430,33 @@ Bitte beachten Sie, dass dieses System derzeit experimentell ist und die hier de
</segment> </segment>
</unit> </unit>
<unit id="MxKRRx_" name="datatable.datatable.lengthMenu"> <unit id="MxKRRx_" name="datatable.datatable.lengthMenu">
<notes> <notes>
<note priority="1">Do not remove! Used for datatables rendering.</note> <note priority="1">Do not remove! Used for datatables rendering.</note>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>datatable.datatable.lengthMenu</source> <source>datatable.datatable.lengthMenu</source>
<target>_MENU_</target> <target>_MENU_</target>
</segment> </segment>
</unit>
<unit id="Ae8pGfM" name="settings.ips.buerklin">
<segment>
<source>settings.ips.buerklin</source>
<target>Buerklin</target>
</segment>
</unit>
<unit id="nlVH1Nb" name="settings.ips.buerklin.username">
<segment>
<source>settings.ips.buerklin.username</source>
<target>Benutzername</target>
</segment>
</unit>
<unit id="BlR_EQc" name="settings.ips.buerklin.help">
<segment>
<source>settings.ips.buerklin.help</source>
<target>Buerklin-API-Zugriffsbeschränkungen: 100 Requests/Minute pro IP-Adresse
Buerklin-API-Authentication-Server:
10 Requests/Minute pro IP-Adresse</target>
</segment>
</unit> </unit>
</file> </file>
</xliff> </xliff>

View file

@ -221,7 +221,7 @@
</notes> </notes>
<segment state="final"> <segment state="final">
<source>part.info.timetravel_hint</source> <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> </segment>
</unit> </unit>
<unit id="3exvSpl" name="standard.label"> <unit id="3exvSpl" name="standard.label">
@ -649,10 +649,10 @@
</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>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;! <target><![CDATA[This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>!
&lt;br&gt; <br>
The user will have to set up all two-factor authentication methods again and print new backup codes! &lt;br&gt;&lt;br&gt; The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br>
&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> <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> </segment>
</unit> </unit>
<unit id="APsHYu0" name="user.edit.tfa.disable_tfa.btn"> <unit id="APsHYu0" name="user.edit.tfa.disable_tfa.btn">
@ -803,9 +803,9 @@ The user will have to set up all two-factor authentication methods again and pri
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>entity.delete.message</source> <source>entity.delete.message</source>
<target>This can not be undone! <target><![CDATA[This can not be undone!
&lt;br&gt; <br>
Sub elements will be moved upwards.</target> Sub elements will be moved upwards.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="2tKAqHw" name="entity.delete"> <unit id="2tKAqHw" name="entity.delete">
@ -1359,7 +1359,7 @@ Sub elements will be moved upwards.</target>
</notes> </notes>
<segment state="final"> <segment state="final">
<source>homepage.github.text</source> <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> </segment>
</unit> </unit>
<unit id="D5OKsgU" name="homepage.help.caption"> <unit id="D5OKsgU" name="homepage.help.caption">
@ -1381,7 +1381,7 @@ Sub elements will be moved upwards.</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>homepage.help.text</source> <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> </segment>
</unit> </unit>
<unit id="dnirx4v" name="homepage.forum.caption"> <unit id="dnirx4v" name="homepage.forum.caption">
@ -1623,7 +1623,7 @@ Sub elements will be moved upwards.</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>email.pw_reset.fallback</source> <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> </segment>
</unit> </unit>
<unit id="DduL9Hu" name="email.pw_reset.username"> <unit id="DduL9Hu" name="email.pw_reset.username">
@ -1653,7 +1653,7 @@ Sub elements will be moved upwards.</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>email.pw_reset.valid_unit %date%</source> <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> </segment>
</unit> </unit>
<unit id="8sBnjRy" name="orderdetail.delete"> <unit id="8sBnjRy" name="orderdetail.delete">
@ -3526,8 +3526,8 @@ Sub elements will be moved upwards.</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>tfa_google.disable.confirm_message</source> <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; <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> Also note that without two-factor authentication, your account is no longer as well protected against attackers!]]></target>
</segment> </segment>
</unit> </unit>
<unit id="yu9MSt5" name="tfa_google.disabled_message"> <unit id="yu9MSt5" name="tfa_google.disabled_message">
@ -3547,7 +3547,7 @@ Also note that without two-factor authentication, your account is no longer as w
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>tfa_google.step.download</source> <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> </segment>
</unit> </unit>
<unit id="eriwJoR" name="tfa_google.step.scan"> <unit id="eriwJoR" name="tfa_google.step.scan">
@ -3789,8 +3789,8 @@ Also note that without two-factor authentication, your account is no longer as w
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>tfa_trustedDevices.explanation</source> <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. <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 &lt;i&gt;all &lt;/i&gt;computers here.</target> 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> </segment>
</unit> </unit>
<unit id="FZINq8z" name="tfa_trustedDevices.invalidate.confirm_title"> <unit id="FZINq8z" name="tfa_trustedDevices.invalidate.confirm_title">
@ -5236,7 +5236,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>label_options.lines_mode.help</source> <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> </segment>
</unit> </unit>
<unit id="isvxbiX" name="label_options.page_size.label"> <unit id="isvxbiX" name="label_options.page_size.label">
@ -7084,15 +7084,15 @@ Exampletown</target>
</notes> </notes>
<segment state="translated"> <segment state="translated">
<source>mass_creation.lines.placeholder</source> <source>mass_creation.lines.placeholder</source>
<target>Element 1 <target><![CDATA[Element 1
Element 1.1 Element 1.1
Element 1.1.1 Element 1.1.1
Element 1.2 Element 1.2
Element 2 Element 2
Element 3 Element 3
Element 1 -&gt; Element 1.1 Element 1 -> Element 1.1
Element 1 -&gt; Element 1.2</target> Element 1 -> Element 1.2]]></target>
</segment> </segment>
</unit> </unit>
<unit id="TWSqPFi" name="entity.mass_creation.btn"> <unit id="TWSqPFi" name="entity.mass_creation.btn">
@ -9152,25 +9152,25 @@ Element 1 -&gt; Element 1.2</target>
<unit id="r4vDLAt" name="filter.parameter_value_constraint.operator.&lt;"> <unit id="r4vDLAt" name="filter.parameter_value_constraint.operator.&lt;">
<segment state="translated"> <segment state="translated">
<source>filter.parameter_value_constraint.operator.&lt;</source> <source>filter.parameter_value_constraint.operator.&lt;</source>
<target>Typ. Value &lt;</target> <target><![CDATA[Typ. Value <]]></target>
</segment> </segment>
</unit> </unit>
<unit id="X9SA3UP" name="filter.parameter_value_constraint.operator.&gt;"> <unit id="X9SA3UP" name="filter.parameter_value_constraint.operator.&gt;">
<segment state="translated"> <segment state="translated">
<source>filter.parameter_value_constraint.operator.&gt;</source> <source>filter.parameter_value_constraint.operator.&gt;</source>
<target>Typ. Value &gt;</target> <target><![CDATA[Typ. Value >]]></target>
</segment> </segment>
</unit> </unit>
<unit id="BQGaoQS" name="filter.parameter_value_constraint.operator.&lt;="> <unit id="BQGaoQS" name="filter.parameter_value_constraint.operator.&lt;=">
<segment state="translated"> <segment state="translated">
<source>filter.parameter_value_constraint.operator.&lt;=</source> <source>filter.parameter_value_constraint.operator.&lt;=</source>
<target>Typ. Value &lt;=</target> <target><![CDATA[Typ. Value <=]]></target>
</segment> </segment>
</unit> </unit>
<unit id="2ha3P6g" name="filter.parameter_value_constraint.operator.&gt;="> <unit id="2ha3P6g" name="filter.parameter_value_constraint.operator.&gt;=">
<segment state="translated"> <segment state="translated">
<source>filter.parameter_value_constraint.operator.&gt;=</source> <source>filter.parameter_value_constraint.operator.&gt;=</source>
<target>Typ. Value &gt;=</target> <target><![CDATA[Typ. Value >=]]></target>
</segment> </segment>
</unit> </unit>
<unit id="4DaBace" name="filter.parameter_value_constraint.operator.BETWEEN"> <unit id="4DaBace" name="filter.parameter_value_constraint.operator.BETWEEN">
@ -9278,7 +9278,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="4tHhDtU" name="parts_list.search.searching_for"> <unit id="4tHhDtU" name="parts_list.search.searching_for">
<segment state="translated"> <segment state="translated">
<source>parts_list.search.searching_for</source> <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> </segment>
</unit> </unit>
<unit id="4vomKLa" name="parts_list.search_options.caption"> <unit id="4vomKLa" name="parts_list.search_options.caption">
@ -10058,7 +10058,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="GzqIwHH" name="entity.select.add_hint"> <unit id="GzqIwHH" name="entity.select.add_hint">
<segment state="translated"> <segment state="translated">
<source>entity.select.add_hint</source> <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> </segment>
</unit> </unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB"> <unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
@ -10082,13 +10082,13 @@ Element 1 -&gt; Element 1.2</target>
<unit id="XLnXtsR" name="homepage.first_steps.introduction"> <unit id="XLnXtsR" name="homepage.first_steps.introduction">
<segment state="translated"> <segment state="translated">
<source>homepage.first_steps.introduction</source> <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> </segment>
</unit> </unit>
<unit id="Q79MOIk" name="homepage.first_steps.create_part"> <unit id="Q79MOIk" name="homepage.first_steps.create_part">
<segment state="translated"> <segment state="translated">
<source>homepage.first_steps.create_part</source> <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> </segment>
</unit> </unit>
<unit id="vplYq4f" name="homepage.first_steps.hide_hint"> <unit id="vplYq4f" name="homepage.first_steps.hide_hint">
@ -10100,7 +10100,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="MJoZl4f" name="homepage.forum.text"> <unit id="MJoZl4f" name="homepage.forum.text">
<segment state="translated"> <segment state="translated">
<source>homepage.forum.text</source> <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> </segment>
</unit> </unit>
<unit id="YsukbnK" name="log.element_edited.changed_fields.category"> <unit id="YsukbnK" name="log.element_edited.changed_fields.category">
@ -10766,7 +10766,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="p_IxB9K" name="parts.import.help_documentation"> <unit id="p_IxB9K" name="parts.import.help_documentation">
<segment state="translated"> <segment state="translated">
<source>parts.import.help_documentation</source> <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> </segment>
</unit> </unit>
<unit id="awbvhVq" name="parts.import.help"> <unit id="awbvhVq" name="parts.import.help">
@ -10958,7 +10958,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="o5u.Nnz" name="part.filter.lessThanDesired"> <unit id="o5u.Nnz" name="part.filter.lessThanDesired">
<segment state="translated"> <segment state="translated">
<source>part.filter.lessThanDesired</source> <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> </segment>
</unit> </unit>
<unit id="YN9eLcZ" name="part.filter.lotOwner"> <unit id="YN9eLcZ" name="part.filter.lotOwner">
@ -11764,13 +11764,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"> <unit id="i68lU5x" name="part.merge.confirm.title">
<segment state="translated"> <segment state="translated">
<source>part.merge.confirm.title</source> <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> </segment>
</unit> </unit>
<unit id="k0anzYV" name="part.merge.confirm.message"> <unit id="k0anzYV" name="part.merge.confirm.message">
<segment state="translated"> <segment state="translated">
<source>part.merge.confirm.message</source> <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> </segment>
</unit> </unit>
<unit id="mmW5Yl1" name="part.info.merge_modal.title"> <unit id="mmW5Yl1" name="part.info.merge_modal.title">
@ -12124,7 +12124,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="p7LGAIX" name="settings.ips.element14.apiKey.help"> <unit id="p7LGAIX" name="settings.ips.element14.apiKey.help">
<segment state="translated"> <segment state="translated">
<source>settings.ips.element14.apiKey.help</source> <source>settings.ips.element14.apiKey.help</source>
<target>You can register for an API key on &lt;a href="https://partner.element14.com/"&gt;https://partner.element14.com/&lt;/a&gt;.</target> <target><![CDATA[You can register for an API key on <a href="https://partner.element14.com/">https://partner.element14.com/</a>.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="ZdUHpZc" name="settings.ips.element14.storeId"> <unit id="ZdUHpZc" name="settings.ips.element14.storeId">
@ -12136,7 +12136,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="XXGUxF6" name="settings.ips.element14.storeId.help"> <unit id="XXGUxF6" name="settings.ips.element14.storeId.help">
<segment state="translated"> <segment state="translated">
<source>settings.ips.element14.storeId.help</source> <source>settings.ips.element14.storeId.help</source>
<target>The store domain to retrieve the data from. This decides the language and currency of results. See &lt;a href="https://partner.element14.com/docs/Product_Search_API_REST__Description"&gt;here&lt;/a&gt; for a list of valid domains.</target> <target><![CDATA[The store domain to retrieve the data from. This decides the language and currency of results. See <a href="https://partner.element14.com/docs/Product_Search_API_REST__Description">here</a> for a list of valid domains.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="WKWZIm2" name="settings.ips.tme"> <unit id="WKWZIm2" name="settings.ips.tme">
@ -12154,7 +12154,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="_pYLrPT" name="settings.ips.tme.token.help"> <unit id="_pYLrPT" name="settings.ips.tme.token.help">
<segment state="translated"> <segment state="translated">
<source>settings.ips.tme.token.help</source> <source>settings.ips.tme.token.help</source>
<target>You can get an API token and secret on &lt;a href="https://developers.tme.eu/en/"&gt;https://developers.tme.eu/en/&lt;/a&gt;.</target> <target><![CDATA[You can get an API token and secret on <a href="https://developers.tme.eu/en/">https://developers.tme.eu/en/</a>.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="yswx4bq" name="settings.ips.tme.secret"> <unit id="yswx4bq" name="settings.ips.tme.secret">
@ -12202,7 +12202,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="gu.JlpT" name="settings.ips.mouser.apiKey.help"> <unit id="gu.JlpT" name="settings.ips.mouser.apiKey.help">
<segment state="translated"> <segment state="translated">
<source>settings.ips.mouser.apiKey.help</source> <source>settings.ips.mouser.apiKey.help</source>
<target>You can register for an API key on &lt;a href="https://eu.mouser.com/api-hub/"&gt;https://eu.mouser.com/api-hub/&lt;/a&gt;.</target> <target><![CDATA[You can register for an API key on <a href="https://eu.mouser.com/api-hub/">https://eu.mouser.com/api-hub/</a>.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="Q66CNjw" name="settings.ips.mouser.searchLimit"> <unit id="Q66CNjw" name="settings.ips.mouser.searchLimit">
@ -12280,7 +12280,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="kKv0J3." name="settings.system.attachments"> <unit id="kKv0J3." name="settings.system.attachments">
<segment state="translated"> <segment state="translated">
<source>settings.system.attachments</source> <source>settings.system.attachments</source>
<target>Attachments &amp; Files</target> <target><![CDATA[Attachments & Files]]></target>
</segment> </segment>
</unit> </unit>
<unit id="dsRff8T" name="settings.system.attachments.maxFileSize"> <unit id="dsRff8T" name="settings.system.attachments.maxFileSize">
@ -12304,7 +12304,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="T.PBu5P" name="settings.system.attachments.allowDownloads.help"> <unit id="T.PBu5P" name="settings.system.attachments.allowDownloads.help">
<segment state="translated"> <segment state="translated">
<source>settings.system.attachments.allowDownloads.help</source> <source>settings.system.attachments.allowDownloads.help</source>
<target>With this option users can download external files into Part-DB by providing an URL. &lt;b&gt;Attention: This can be a security issue, as it might allow users to access intranet ressources via Part-DB!&lt;/b&gt;</target> <target><![CDATA[With this option users can download external files into Part-DB by providing an URL. <b>Attention: This can be a security issue, as it might allow users to access intranet ressources via Part-DB!</b>]]></target>
</segment> </segment>
</unit> </unit>
<unit id=".OyihML" name="settings.system.attachments.downloadByDefault"> <unit id=".OyihML" name="settings.system.attachments.downloadByDefault">
@ -12478,8 +12478,8 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="0GRlEe5" name="settings.system.localization.base_currency_description"> <unit id="0GRlEe5" name="settings.system.localization.base_currency_description">
<segment state="translated"> <segment state="translated">
<source>settings.system.localization.base_currency_description</source> <source>settings.system.localization.base_currency_description</source>
<target>The currency that is used to store price information and exchange rates in. This currency is assumed, when no currency is set for a price information. <target><![CDATA[The currency that is used to store price information and exchange rates in. This currency is assumed, when no currency is set for a price information.
&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> <b>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!</b>]]></target>
</segment> </segment>
</unit> </unit>
<unit id="cvpTUeY" name="settings.system.privacy"> <unit id="cvpTUeY" name="settings.system.privacy">
@ -12509,7 +12509,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="w07P3Dt" name="settings.misc.kicad_eda.category_depth.help"> <unit id="w07P3Dt" name="settings.misc.kicad_eda.category_depth.help">
<segment state="translated"> <segment state="translated">
<source>settings.misc.kicad_eda.category_depth.help</source> <source>settings.misc.kicad_eda.category_depth.help</source>
<target>This value determines the depth of the category tree, that is visible inside KiCad. 0 means that only the top level categories are visible. Set to a value &gt; 0 to show more levels. Set to -1, to show all parts of Part-DB inside a sigle cnategory in KiCad.</target> <target><![CDATA[This value determines the depth of the category tree, that is visible inside KiCad. 0 means that only the top level categories are visible. Set to a value > 0 to show more levels. Set to -1, to show all parts of Part-DB inside a sigle cnategory in KiCad.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="VwvmcWE" name="settings.behavior.sidebar"> <unit id="VwvmcWE" name="settings.behavior.sidebar">
@ -12527,7 +12527,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="jc0JTvL" name="settings.behavior.sidebar.items.help"> <unit id="jc0JTvL" name="settings.behavior.sidebar.items.help">
<segment state="translated"> <segment state="translated">
<source>settings.behavior.sidebar.items.help</source> <source>settings.behavior.sidebar.items.help</source>
<target>The menus which appear at the sidebar by default. Order of items can be changed via drag &amp; drop.</target> <target><![CDATA[The menus which appear at the sidebar by default. Order of items can be changed via drag & drop.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="gVSWDkE" name="settings.behavior.sidebar.rootNodeEnabled"> <unit id="gVSWDkE" name="settings.behavior.sidebar.rootNodeEnabled">
@ -12575,7 +12575,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="SUD8H3b" name="settings.behavior.table.parts_default_columns.help"> <unit id="SUD8H3b" name="settings.behavior.table.parts_default_columns.help">
<segment state="translated"> <segment state="translated">
<source>settings.behavior.table.parts_default_columns.help</source> <source>settings.behavior.table.parts_default_columns.help</source>
<target>The columns to show by default in part tables. Order of items can be changed via drag &amp; drop.</target> <target><![CDATA[The columns to show by default in part tables. Order of items can be changed via drag & drop.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="hazr_g5" name="settings.ips.oemsecrets"> <unit id="hazr_g5" name="settings.ips.oemsecrets">
@ -12629,7 +12629,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="KLJYfJ0" name="settings.ips.oemsecrets.sortMode.M"> <unit id="KLJYfJ0" name="settings.ips.oemsecrets.sortMode.M">
<segment state="translated"> <segment state="translated">
<source>settings.ips.oemsecrets.sortMode.M</source> <source>settings.ips.oemsecrets.sortMode.M</source>
<target>Completeness &amp; Manufacturer name</target> <target><![CDATA[Completeness & Manufacturer name]]></target>
</segment> </segment>
</unit> </unit>
<unit id="8C9ijHM" name="entity.export.flash.error.no_entities"> <unit id="8C9ijHM" name="entity.export.flash.error.no_entities">
@ -13289,7 +13289,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="FsrRdkp" name="settings.behavior.homepage.items.help"> <unit id="FsrRdkp" name="settings.behavior.homepage.items.help">
<segment state="translated"> <segment state="translated">
<source>settings.behavior.homepage.items.help</source> <source>settings.behavior.homepage.items.help</source>
<target>The items to show at the homepage. Order can be changed via drag &amp; drop.</target> <target><![CDATA[The items to show at the homepage. Order can be changed via drag & drop.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="CYw3_pS" name="settings.system.customization.showVersionOnHomepage"> <unit id="CYw3_pS" name="settings.system.customization.showVersionOnHomepage">
@ -14003,7 +14003,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="Ej2znKK" name="settings.system.localization.language_menu_entries.description"> <unit id="Ej2znKK" name="settings.system.localization.language_menu_entries.description">
<segment state="translated"> <segment state="translated">
<source>settings.system.localization.language_menu_entries.description</source> <source>settings.system.localization.language_menu_entries.description</source>
<target>The languages to show in the language drop-down menu. Order can be changed via drag &amp; drop. Leave empty to show all available languages.</target> <target><![CDATA[The languages to show in the language drop-down menu. Order can be changed via drag & drop. Leave empty to show all available languages.]]></target>
</segment> </segment>
</unit> </unit>
<unit id="xIZ_mEX" name="project.builds.no_bom_entries"> <unit id="xIZ_mEX" name="project.builds.no_bom_entries">
@ -14287,6 +14287,27 @@ Please note that this system is currently experimental, and the synonyms defined
<target>_MENU_</target> <target>_MENU_</target>
</segment> </segment>
</unit> </unit>
<unit id="Ae8pGfM" name="settings.ips.buerklin">
<segment>
<source>settings.ips.buerklin</source>
<target>Buerklin</target>
</segment>
</unit>
<unit id="nlVH1Nb" name="settings.ips.buerklin.username">
<segment>
<source>settings.ips.buerklin.username</source>
<target>User name</target>
</segment>
</unit>
<unit id="BlR_EQc" name="settings.ips.buerklin.help">
<segment>
<source>settings.ips.buerklin.help</source>
<target>Buerklin-API access limits:
100 requests/minute per IP address
Buerklin-API Authentication server:
10 requests/minute per IP address</target>
</segment>
</unit>
<unit id="8BkjGyq" name="project.bom.part_id"> <unit id="8BkjGyq" name="project.bom.part_id">
<segment> <segment>
<source>project.bom.part_id</source> <source>project.bom.part_id</source>