Merge remote-tracking branch 'upstream/master' into bulk-edit-tags

This commit is contained in:
d-buchmann 2025-08-15 08:43:04 +02:00
commit 858c7a6e0f
20 changed files with 1218 additions and 808 deletions

View file

@ -47,6 +47,7 @@
PassEnv PROVIDER_REICHELT_ENABLED PROVIDER_REICHELT_CURRENCY PROVIDER_REICHELT_COUNTRY PROVIDER_REICHELT_LANGUAGE PROVIDER_REICHELT_INCLUDE_VAT PassEnv PROVIDER_REICHELT_ENABLED PROVIDER_REICHELT_CURRENCY PROVIDER_REICHELT_COUNTRY PROVIDER_REICHELT_LANGUAGE PROVIDER_REICHELT_INCLUDE_VAT
PassEnv PROVIDER_POLLIN_ENABLED PassEnv PROVIDER_POLLIN_ENABLED
PassEnv EDA_KICAD_CATEGORY_DEPTH PassEnv EDA_KICAD_CATEGORY_DEPTH
PassEnv SHOW_PART_IMAGE_OVERLAY
# For most configuration files from conf-available/, which are # For most configuration files from conf-available/, which are
# enabled or disabled at a global level, it is possible to # enabled or disabled at a global level, it is possible to

3
.env
View file

@ -305,6 +305,9 @@ FIXER_API_KEY=CHANGEME
# When this is empty the content of config/banner.md is used as banner # When this is empty the content of config/banner.md is used as banner
BANNER="" BANNER=""
# Enable the part image overlay which shows name and filename of the picture
SHOW_PART_IMAGE_OVERLAY=1
APP_ENV=prod APP_ENV=prod
APP_SECRET=a03498528f5a5fc089273ec9ae5b2849 APP_SECRET=a03498528f5a5fc089273ec9ae5b2849

View file

@ -1 +1 @@
1.17.1 1.17.3

View file

@ -33,7 +33,10 @@ export default class extends Controller {
{ {
let value = ""; let value = "";
if (this.unitValue) { if (this.unitValue) {
value = "\\mathrm{" + this.inputTarget.value + "}"; //Escape percentage signs
value = this.inputTarget.value.replace(/%/g, '\\%');
value = "\\mathrm{" + value + "}";
} else { } else {
value = this.inputTarget.value; value = this.inputTarget.value;
} }

View file

@ -85,7 +85,9 @@ export default class extends Controller
tmp += '<span>' + katex.renderToString(data.symbol) + '</span>' tmp += '<span>' + katex.renderToString(data.symbol) + '</span>'
} }
if (data.unit) { if (data.unit) {
tmp += '<span class="ms-2">' + katex.renderToString('[' + data.unit + ']') + '</span>' let unit = data.unit.replace(/%/g, '\\%');
unit = "\\mathrm{" + unit + "}";
tmp += '<span class="ms-2">' + katex.renderToString('[' + unit + ']') + '</span>'
} }

1755
composer.lock generated

File diff suppressed because it is too large Load diff

View file

@ -21,6 +21,7 @@ twig:
available_themes: '%partdb.available_themes%' available_themes: '%partdb.available_themes%'
saml_enabled: '%partdb.saml.enabled%' saml_enabled: '%partdb.saml.enabled%'
part_preview_generator: '@App\Services\Attachments\PartPreviewGenerator' part_preview_generator: '@App\Services\Attachments\PartPreviewGenerator'
img_overlay: '%partdb.show_part_image_overlay%'
when@test: when@test:
twig: twig:

View file

@ -74,6 +74,7 @@ parameters:
# Miscellaneous # Miscellaneous
###################################################################################################################### ######################################################################################################################
partdb.demo_mode: '%env(bool:DEMO_MODE)%' # If set to true, all potentially dangerous things are disabled (like changing passwords of the own user) partdb.demo_mode: '%env(bool:DEMO_MODE)%' # If set to true, all potentially dangerous things are disabled (like changing passwords of the own user)
partdb.show_part_image_overlay: '%env(bool:SHOW_PART_IMAGE_OVERLAY)%' # If set to false, the filename overlay of the part image will be disabled
# Set the themes from which the user can choose from in the settings. # Set the themes from which the user can choose from in the settings.
# Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk! # Themes commented here by default, are not really usable, because of display problems. Enable them at your own risk!

View file

@ -95,6 +95,8 @@ bundled with Part-DB. Set `DATABASE_MYSQL_SSL_VERIFY_CERT` if you want to accept
particularly for securing and protecting various aspects of your application. It's a secret key that is used for particularly for securing and protecting various aspects of your application. It's a secret key that is used for
cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this cryptographic operations and security measures (session management, CSRF protection, etc..). Therefore this
value should be handled as confidential data and not shared publicly. value should be handled as confidential data and not shared publicly.
* `SHOW_PART_IMAGE_OVERLAY`: Set to 0 to disable the part image overlay, which appears if you hover over an image in the
part image gallery
### E-Mail settings ### E-Mail settings

View file

@ -39,7 +39,7 @@
"@ckeditor/ckeditor5-block-quote": "^44.0.0", "@ckeditor/ckeditor5-block-quote": "^44.0.0",
"@ckeditor/ckeditor5-code-block": "^44.0.0", "@ckeditor/ckeditor5-code-block": "^44.0.0",
"@ckeditor/ckeditor5-dev-translations": "^43.0.1", "@ckeditor/ckeditor5-dev-translations": "^43.0.1",
"@ckeditor/ckeditor5-dev-utils": "^43.0.1", "@ckeditor/ckeditor5-dev-utils": "43.0.*",
"@ckeditor/ckeditor5-editor-classic": "^44.0.0", "@ckeditor/ckeditor5-editor-classic": "^44.0.0",
"@ckeditor/ckeditor5-essentials": "^44.0.0", "@ckeditor/ckeditor5-essentials": "^44.0.0",
"@ckeditor/ckeditor5-find-and-replace": "^44.0.0", "@ckeditor/ckeditor5-find-and-replace": "^44.0.0",

View file

@ -217,7 +217,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
$str = ''; $str = '';
$bracket_opened = false; $bracket_opened = false;
if ($this->value_typical) { if ($this->value_typical !== null) {
$str .= $this->getValueTypicalWithUnit($latex_formatted); $str .= $this->getValueTypicalWithUnit($latex_formatted);
if ($this->value_min || $this->value_max) { if ($this->value_min || $this->value_max) {
$bracket_opened = true; $bracket_opened = true;
@ -225,11 +225,11 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
} }
} }
if ($this->value_max && $this->value_min) { if ($this->value_max !== null && $this->value_min !== null) {
$str .= $this->getValueMinWithUnit($latex_formatted).' ... '.$this->getValueMaxWithUnit($latex_formatted); $str .= $this->getValueMinWithUnit($latex_formatted).' ... '.$this->getValueMaxWithUnit($latex_formatted);
} elseif ($this->value_max) { } elseif ($this->value_max !== null) {
$str .= 'max. '.$this->getValueMaxWithUnit($latex_formatted); $str .= 'max. '.$this->getValueMaxWithUnit($latex_formatted);
} elseif ($this->value_min) { } elseif ($this->value_min !== null) {
$str .= 'min. '.$this->getValueMinWithUnit($latex_formatted); $str .= 'min. '.$this->getValueMinWithUnit($latex_formatted);
} }
@ -449,7 +449,10 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
if (!$with_latex) { if (!$with_latex) {
$unit = $this->unit; $unit = $this->unit;
} else { } else {
$unit = '$\mathrm{'.$this->unit.'}$'; //Escape the percentage sign for convenience (as latex uses it as comment and it is often used in units)
$escaped = preg_replace('/\\\\?%/', "\\\\%", $this->unit);
$unit = '$\mathrm{'.$escaped.'}$';
} }
return $str.' '.$unit; return $str.' '.$unit;
@ -457,7 +460,7 @@ abstract class AbstractParameter extends AbstractNamedDBElement implements Uniqu
return $str; return $str;
} }
/** /**
* Returns the class of the element that is allowed to be associated with this attachment. * Returns the class of the element that is allowed to be associated with this attachment.
* @return string * @return string

View file

@ -112,12 +112,12 @@ class AttachmentURLGenerator
/** /**
* Returns a URL to a thumbnail of the attachment file. * Returns a URL to a thumbnail of the attachment file.
* For external files the original URL is returned. * For external files the original URL is returned.
* @return string|null The URL or null if the attachment file is not existing * @return string|null The URL or null if the attachment file is not existing or is invalid
*/ */
public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string public function getThumbnailURL(Attachment $attachment, string $filter_name = 'thumbnail_sm'): ?string
{ {
if (!$attachment->isPicture()) { if (!$attachment->isPicture()) {
throw new InvalidArgumentException('Thumbnail creation only works for picture attachments!'); return null;
} }
if (!$attachment->hasInternal()){ if (!$attachment->hasInternal()){

View file

@ -237,6 +237,49 @@ class KiCadHelper
$result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn()); $result["fields"]["Part-DB IPN"] = $this->createField($part->getIpn());
} }
// Add supplier information from orderdetails (include obsolete orderdetails)
if ($part->getOrderdetails(false)->count() > 0) {
$supplierCounts = [];
foreach ($part->getOrderdetails(false) as $orderdetail) {
if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') {
$supplierName = $orderdetail->getSupplier()->getName();
$supplierName .= " SPN"; // Append "SPN" to the supplier name to indicate Supplier Part Number
if (!isset($supplierCounts[$supplierName])) {
$supplierCounts[$supplierName] = 0;
}
$supplierCounts[$supplierName]++;
// Create field name with sequential number if more than one from same supplier (e.g. "Mouser", "Mouser 2", etc.)
$fieldName = $supplierCounts[$supplierName] > 1
? $supplierName . ' ' . $supplierCounts[$supplierName]
: $supplierName;
$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
}
}
}
//Add fields for KiCost:
if ($part->getManufacturer() !== null) {
$result["fields"]["manf"] = $this->createField($part->getManufacturer()->getName());
}
if ($part->getManufacturerProductNumber() !== "") {
$result['fields']['manf#'] = $this->createField($part->getManufacturerProductNumber());
}
//For each supplier, add a field with the supplier name and the supplier part number for KiCost
if ($part->getOrderdetails(false)->count() > 0) {
foreach ($part->getOrderdetails(false) as $orderdetail) {
if ($orderdetail->getSupplier() !== null && $orderdetail->getSupplierPartNr() !== '') {
$fieldName = mb_strtolower($orderdetail->getSupplier()->getName()) . '#';
$result["fields"][$fieldName] = $this->createField($orderdetail->getSupplierPartNr());
}
}
}
return $result; return $result;
} }

View file

@ -57,6 +57,7 @@ class EntityImporter
/** /**
* Creates many entries at once, based on a (text) list of name. * Creates many entries at once, based on a (text) list of name.
* The created entities are not persisted to database yet, so you have to do it yourself. * The created entities are not persisted to database yet, so you have to do it yourself.
* It returns all entities in the hierachy chain (even if they are already persisted).
* *
* @template T of AbstractNamedDBElement * @template T of AbstractNamedDBElement
* @param string $lines The list of names seperated by \n * @param string $lines The list of names seperated by \n
@ -132,32 +133,38 @@ class EntityImporter
//We can only use the getNewEntityFromPath function, if the repository is a StructuralDBElementRepository //We can only use the getNewEntityFromPath function, if the repository is a StructuralDBElementRepository
if ($repo instanceof StructuralDBElementRepository) { if ($repo instanceof StructuralDBElementRepository) {
$entities = $repo->getNewEntityFromPath($new_path); $entities = $repo->getNewEntityFromPath($new_path);
$entity = end($entities); if ($entities === []) {
if ($entity === false) {
throw new InvalidArgumentException('getNewEntityFromPath returned an empty array!'); throw new InvalidArgumentException('getNewEntityFromPath returned an empty array!');
} }
} else { //Otherwise just create a new entity } else { //Otherwise just create a new entity
$entity = new $class_name; $entity = new $class_name;
$entity->setName($name); $entity->setName($name);
$entities = [$entity];
} }
//Validate entity //Validate entity
$tmp = $this->validator->validate($entity); foreach ($entities as $entity) {
//If no error occured, write entry to DB: $tmp = $this->validator->validate($entity);
if (0 === count($tmp)) { //If no error occured, write entry to DB:
$valid_entities[] = $entity; if (0 === count($tmp)) {
} else { //Otherwise log error $valid_entities[] = $entity;
$errors[] = [ } else { //Otherwise log error
'entity' => $entity, $errors[] = [
'violations' => $tmp, 'entity' => $entity,
]; 'violations' => $tmp,
];
}
} }
$last_element = $entity; $last_element = end($entities);
if ($last_element === false) {
$last_element = null;
}
} }
return $valid_entities; //Only return objects once
return array_values(array_unique($valid_entities));
} }
/** /**

View file

@ -49,8 +49,8 @@ class PollinProvider implements InfoProviderInterface
{ {
return [ return [
'name' => 'Pollin', 'name' => 'Pollin',
'description' => 'Webscrapping from pollin.de to get part information', 'description' => 'Webscraping from pollin.de to get part information',
'url' => 'https://www.reichelt.de/', 'url' => 'https://www.pollin.de/',
'disabled_help' => 'Set PROVIDER_POLLIN_ENABLED env to 1' 'disabled_help' => 'Set PROVIDER_POLLIN_ENABLED env to 1'
]; ];
} }

View file

@ -57,7 +57,7 @@ class ReicheltProvider implements InfoProviderInterface
{ {
return [ return [
'name' => 'Reichelt', 'name' => 'Reichelt',
'description' => 'Webscrapping from reichelt.com to get part information', 'description' => 'Webscraping from reichelt.com to get part information',
'url' => 'https://www.reichelt.com/', 'url' => 'https://www.reichelt.com/',
'disabled_help' => 'Set PROVIDER_REICHELT_ENABLED env to 1' 'disabled_help' => 'Set PROVIDER_REICHELT_ENABLED env to 1'
]; ];

View file

@ -63,7 +63,8 @@ class TreeViewGenerator
private readonly UrlGeneratorInterface $router, private readonly UrlGeneratorInterface $router,
protected bool $rootNodeExpandedByDefault, protected bool $rootNodeExpandedByDefault,
protected bool $rootNodeEnabled, protected bool $rootNodeEnabled,
//TODO: Make this configurable in the future
protected bool $rootNodeRedirectsToNewEntity = false,
) { ) {
} }
@ -174,10 +175,7 @@ class TreeViewGenerator
} }
if (($mode === 'list_parts_root' || $mode === 'devices') && $this->rootNodeEnabled) { if (($mode === 'list_parts_root' || $mode === 'devices') && $this->rootNodeEnabled) {
//We show the root node as a link to the list of all parts $root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $this->entityClassToRootNodeHref($class), $generic);
$show_all_parts_url = $this->router->generate('parts_show_all');
$root_node = new TreeViewNode($this->entityClassToRootNodeString($class), $show_all_parts_url, $generic);
$root_node->setExpanded($this->rootNodeExpandedByDefault); $root_node->setExpanded($this->rootNodeExpandedByDefault);
$root_node->setIcon($this->entityClassToRootNodeIcon($class)); $root_node->setIcon($this->entityClassToRootNodeIcon($class));
@ -187,6 +185,27 @@ class TreeViewGenerator
return array_merge($head, $generic); return array_merge($head, $generic);
} }
protected function entityClassToRootNodeHref(string $class): ?string
{
//If the root node should redirect to the new entity page, we return the URL for the new entity.
if ($this->rootNodeRedirectsToNewEntity) {
return match ($class) {
Category::class => $this->router->generate('category_new'),
StorageLocation::class => $this->router->generate('store_location_new'),
Footprint::class => $this->router->generate('footprint_new'),
Manufacturer::class => $this->router->generate('manufacturer_new'),
Supplier::class => $this->router->generate('supplier_new'),
Project::class => $this->router->generate('project_new'),
default => null,
};
}
return match ($class) {
Project::class => $this->router->generate('project_new'),
default => $this->router->generate('parts_show_all')
};
}
protected function entityClassToRootNodeString(string $class): string protected function entityClassToRootNodeString(string $class): string
{ {
return match ($class) { return match ($class) {

View file

@ -13,6 +13,7 @@
<div class="carousel-item {% if loop.first %}active{% endif %}"> <div class="carousel-item {% if loop.first %}active{% endif %}">
<a href="{{ entity_url(pic, 'file_view') }}" data-turbo="false" target="_blank" rel="noopener"> <a href="{{ entity_url(pic, 'file_view') }}" data-turbo="false" target="_blank" rel="noopener">
<img class="d-block w-100 img-fluid img-thumbnail bg-light part-info-image" src="{{ entity_url(pic, 'file_view') }}" alt=""> <img class="d-block w-100 img-fluid img-thumbnail bg-light part-info-image" src="{{ entity_url(pic, 'file_view') }}" alt="">
{% if img_overlay %}
<div class="mask"></div> <div class="mask"></div>
<div class="carousel-caption-hover"> <div class="carousel-caption-hover">
<div class="carousel-caption text-white"> <div class="carousel-caption text-white">
@ -21,6 +22,7 @@
<div>{{ entity_type_label(pic.element) }}</div> <div>{{ entity_type_label(pic.element) }}</div>
</div> </div>
</div> </div>
{% endif %}
</a> </a>
</div> </div>
{% endfor %} {% endfor %}

View file

@ -75,8 +75,8 @@ class EntityImporterTest extends WebTestCase
$em = self::getContainer()->get(EntityManagerInterface::class); $em = self::getContainer()->get(EntityManagerInterface::class);
$parent = $em->find(AttachmentType::class, 1); $parent = $em->find(AttachmentType::class, 1);
$results = $this->service->massCreation($lines, AttachmentType::class, $parent, $errors); $results = $this->service->massCreation($lines, AttachmentType::class, $parent, $errors);
$this->assertCount(3, $results); $this->assertCount(4, $results);
$this->assertSame($parent, $results[0]->getParent()); $this->assertSame("Test 1", $results[1]->getName());
//Test for addition of existing elements //Test for addition of existing elements
$errors = []; $errors = [];
@ -113,6 +113,31 @@ EOT;
} }
public function testMassCreationArrow(): void
{
$input = <<<EOT
Test1 -> Test1.1
Test1 -> Test1.2
Test2 -> Test2.1
Test1
Test1.3
EOT;
$errors = [];
$results = $this->service->massCreation($input, AttachmentType::class, null, $errors);
//We have 6 elements, and 0 errors
$this->assertCount(0, $errors);
$this->assertCount(6, $results);
$this->assertEquals('Test1', $results[0]->getName());
$this->assertEquals('Test1.1', $results[1]->getName());
$this->assertEquals('Test1.2', $results[2]->getName());
$this->assertEquals('Test2', $results[3]->getName());
$this->assertEquals('Test2.1', $results[4]->getName());
$this->assertEquals('Test1.3', $results[5]->getName());
}
public function testMassCreationNested(): void public function testMassCreationNested(): void
{ {
$input = <<<EOT $input = <<<EOT
@ -132,15 +157,15 @@ EOT;
//We have 7 elements, and 0 errors //We have 7 elements, and 0 errors
$this->assertCount(0, $errors); $this->assertCount(0, $errors);
$this->assertCount(7, $results); $this->assertCount(8, $results);
$element1 = $results[0]; $element1 = $results[1];
$element11 = $results[1]; $element11 = $results[2];
$element111 = $results[2]; $element111 = $results[3];
$element112 = $results[3]; $element112 = $results[4];
$element12 = $results[4]; $element12 = $results[5];
$element121 = $results[5]; $element121 = $results[6];
$element2 = $results[6]; $element2 = $results[7];
$this->assertSame('Test 1', $element1->getName()); $this->assertSame('Test 1', $element1->getName());
$this->assertSame('Test 1.1', $element11->getName()); $this->assertSame('Test 1.1', $element11->getName());

View file

@ -242,7 +242,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">
@ -731,10 +731,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">
@ -885,9 +885,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">
@ -1441,7 +1441,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">
@ -1463,7 +1463,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">
@ -1705,7 +1705,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">
@ -1735,7 +1735,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">
@ -3578,8 +3578,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">
@ -3599,7 +3599,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">
@ -3841,8 +3841,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">
@ -5313,7 +5313,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">
@ -7157,12 +7157,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</target> Element 3
Element 1 -> Element 1.1
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">
@ -9388,25 +9391,25 @@ Element 3</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">
@ -9514,7 +9517,7 @@ Element 3</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">
@ -10174,13 +10177,13 @@ Element 3</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>You have enough stocked to build &lt;b&gt;%max_builds%&lt;/b&gt; builds of this project.</target> <target><![CDATA[You have enough stocked to build <b>%max_builds%</b> builds of this project.]]></target>
</segment> </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>The current project status is &lt;b&gt;"%project_status%"&lt;/b&gt;. You should check if you really want to build the project with this status!</target> <target><![CDATA[The current project status is <b>"%project_status%"</b>. You should check if you really want to build the project with this status!]]></target>
</segment> </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">
@ -10282,7 +10285,7 @@ Element 3</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">
@ -10306,13 +10309,13 @@ Element 3</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">
@ -10324,7 +10327,7 @@ Element 3</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">
@ -10978,7 +10981,7 @@ Element 3</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">
@ -11158,7 +11161,7 @@ Element 3</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">
@ -11970,13 +11973,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">