Compare commits

..

87 Commits

Author SHA1 Message Date
Jan Böhmer
ae4c0786b2 Bumped to version 2.5.1 2026-01-25 21:38:49 +01:00
Niklas
3aad70934b Support dynamic supplier SPNs in BOM import comments (#1208)
* Fix: Use correct field name for LCSC supplier part numbers in BOM import

The field mapping system uses 'LCSC SPN' as the target field name for LCSC
supplier part numbers (following the pattern SupplierName + ' SPN'), but the
code in parseKiCADSchematic() was incorrectly checking for 'LCSC'.

This caused LCSC supplier part numbers to be silently ignored and not included
in the BOM entry comments during schematic import.

Changed isset($mapped_entry['LCSC']) to isset($mapped_entry['LCSC SPN']) to
match the actual field name produced by the field mapping system.

* regression test: check for LCSC SPN in comment

* Support dynamic supplier SPNs in BOM import comments

Replace hardcoded LCSC SPN handling with dynamic supplier lookup to support all configured suppliers in BOM import. This allows any supplier defined in
Part-DB to have their SPN fields recognized and included in the BOM entry
comments during BOM import.

* Optimize BOM import by only calculating supplier SPN keys once
2026-01-25 21:32:14 +01:00
Jan Böhmer
e15d12c0bf Merge remote-tracking branch 'origin/l10n_master' 2026-01-25 21:27:51 +01:00
Copilot
ff7fa67682 Install Yarn via npm instead of Debian packages in Dockerfiles (#1207)
* Initial plan

* Change yarn installation from Debian packages to npm in both Dockerfiles

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
2026-01-25 21:25:08 +01:00
Jan Böhmer
2b723e05ff New translations frontend.en.xlf (English) 2026-01-25 21:16:04 +01:00
Jan Böhmer
a8d2204c7f New translations validators.en.xlf (German) 2026-01-25 21:15:51 +01:00
Jan Böhmer
29050178bd New translations messages.en.xlf (German) 2026-01-25 21:15:50 +01:00
Jan Böhmer
af61772c88 Revert "Fixed frankenphp docker build"
This reverts commit b91cd44926.
2026-01-25 20:31:10 +01:00
Jan Böhmer
b91cd44926 Fixed frankenphp docker build 2026-01-25 20:15:29 +01:00
Jan Böhmer
c476c98d56 Added clear button to optional part select fields
Fixes #1156
2026-01-25 19:12:27 +01:00
Jan Böhmer
fe458b7ff1 When uploading a file, automatically determine the best fitting attachment type 2026-01-25 18:41:11 +01:00
Jan Böhmer
7b8f3aaf62 New translations messages.en.xlf (English) 2026-01-25 18:23:26 +01:00
Jan Böhmer
d93dfd577e Fail more gracefully when an error occurs in the info providers 2026-01-25 18:22:47 +01:00
Jan Böhmer
4095d0fd49 New translations frontend.en.xlf (Danish) 2026-01-25 10:50:30 +01:00
Jan Böhmer
6d3197497e New translations security.en.xlf (Danish) 2026-01-25 10:50:26 +01:00
Jan Böhmer
f438a8b4cd New translations validators.en.xlf (Danish) 2026-01-25 10:50:25 +01:00
Jan Böhmer
56fa2a9396 Updated yarn dependencies 2026-01-25 00:51:57 +01:00
Jan Böhmer
3975a3ba61 Updated composer dependencies
We can now use the most recent symfony property-info versions now again, as the bug was fixed in upstream
2026-01-25 00:51:00 +01:00
Jan Böhmer
aa9aedc5fd Prevent the extra column of the log data tables to be ordered
This makes not much sense because its JSON data under the hood, and PostgreSQL errors when trying to do it.
2026-01-25 00:38:11 +01:00
Jan Böhmer
766ba07105 Properly disable the id search by default
Follow up on PR #1184
2026-01-18 23:48:04 +01:00
Jan Böhmer
d0b827c2c3 Do not use the wrong language for trees when no user is logged in 2026-01-18 23:44:11 +01:00
Jan Böhmer
cd7dbd5f7b Bumped version to 2.5.0 2026-01-18 22:59:35 +01:00
Jan Böhmer
8efbca798a Merge remote-tracking branch 'origin/master' 2026-01-18 22:53:40 +01:00
Jan Böhmer
dd6c20780b Ensure that the ids passed to DBElementRepository::findByIDInMatchingOrder are all ints
This might help to diagnose #1188
2026-01-18 22:53:37 +01:00
Lukas Runge
af81e15ef2 Set "Excluded from sim" to false by default for new categories to avoid annoying symbol at kicad parts. 2026-01-18 22:35:37 +01:00
Jan Böhmer
09cc2ba8ff Use requestSubmit() in form cleanup controller to avoid CSFR issues
See #1191
2026-01-18 22:24:17 +01:00
swdee
131023da67 change barcode scan form to use requestSubmit() to fix CSRF token not being generated on submission (#1191) 2026-01-18 22:14:17 +01:00
Jan Böhmer
4636aa4e0d New translations frontend.en.xlf (Hungarian) 2026-01-18 22:00:38 +01:00
Jan Böhmer
006cfd7b5d New translations frontend.en.xlf (German) 2026-01-18 22:00:37 +01:00
Jan Böhmer
86f53b2956 Update Crowdin configuration file 2026-01-18 21:58:54 +01:00
Jan Böhmer
1923abecdf New translations messages.en.xlf (English) 2026-01-18 19:50:33 +01:00
Copilot
a3d992a016 Move frontend translations to separate domain to reduce bundle size (#1197)
* Initial plan

* Create frontend translation files and update configuration

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

* Fix missing semicolon in password strength controller

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

* Remove frontend-only translations from messages domain and set frontend as default domain

Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: jbtronics <5410681+jbtronics@users.noreply.github.com>
2026-01-18 18:50:38 +01:00
Jan Böhmer
6402cfe619 Enforce to use jquery 3 for now, as something seems to be broken with jquery 4 and webpack 2026-01-18 18:44:59 +01:00
Jan Böhmer
ea71fcd120 Merge remote-tracking branch 'origin/master' 2026-01-18 12:47:11 +01:00
Jan Böhmer
82e3e31277 Only compress assets with a certain minimum size. Otherwise its quite inefficientg 2026-01-18 12:44:33 +01:00
Jan Böhmer
0d4f935b43 Updated marked and webpack-bundle-analyzer dependencies 2026-01-18 12:40:07 +01:00
Jan Böhmer
0205dd523b Updated dependencies 2026-01-18 12:32:09 +01:00
d-buchmann
0a8199d81f Update OEMSecretsProvider.php (#1187)
most probably only a typo
2026-01-13 12:53:22 +01:00
Jan Böhmer
3f6a6cc767 updated dependencies 2026-01-11 19:02:39 +01:00
Jan Böhmer
33a3dc6203 Merge branch 'dbid_search_and_display_in_bom' 2026-01-11 18:33:22 +01:00
Jan Böhmer
1cd0b459be Fixed JS translation fox new UX/translator version 2026-01-10 21:44:57 +01:00
Jan Böhmer
6828ce5803 Updated dependencies 2026-01-10 21:34:01 +01:00
Jan Böhmer
644a44e8e9 Merge branch 'db_converter' 2026-01-10 21:14:38 +01:00
Jan Böhmer
6c3e4d7880 Added documentation about the database conversion command 2026-01-10 21:14:27 +01:00
Jan Böhmer
aefb69c51e Fixed error that users could not be converted due to settings and backupCodes not allowed as nullable 2026-01-09 21:17:51 +01:00
Jan Böhmer
300ee33be2 Allow to continue even if source and target db platform are the same 2026-01-09 19:46:09 +01:00
kernchen-brc
64efca4786 Added ID to search options. Fixed seach option by using equal to instead of like for the ID. 2026-01-09 11:37:30 +01:00
Jan Böhmer
ddbfc87ce1 Set help for DBPlatformConvertCommand 2026-01-08 22:22:47 +01:00
Jan Böhmer
3454fa51de Support %kernel.project_dir% in db conversion tool 2026-01-08 22:22:07 +01:00
Jan Böhmer
343ad6beff Check that databases are up to date 2026-01-08 22:16:38 +01:00
Jan Böhmer
d385303a52 Made DBMigrationCommand take a DB url so we do not need a special doctrine config 2026-01-08 21:03:38 +01:00
Jan Böhmer
00b35e3306 Fix sequences of postgres after migration 2026-01-05 23:25:53 +01:00
Jan Böhmer
e0a25009d9 fixed 2026-01-05 23:16:33 +01:00
Jan Böhmer
3f0e4b09ce Added a progress bar 2026-01-05 23:14:40 +01:00
Jan Böhmer
96a37a0cb0 Implemented proof of concept to convert between database types 2026-01-05 22:41:40 +01:00
Jan Böhmer
3e071f2b74 New translations messages.en.xlf (English) 2026-01-04 22:00:55 +01:00
Jan Böhmer
2157916e9b Bumped version to 2.4.0 2026-01-04 21:53:44 +01:00
Marc
be35c36c58 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>
2026-01-04 21:05:47 +01:00
Jan Böhmer
7116c2ceb9 Removed unused service import 2026-01-04 20:03:14 +01:00
Jan Böhmer
89322d329c New translations messages.en.xlf (English) 2026-01-04 18:00:49 +01:00
Jan Böhmer
c1d4ce77db Fixed exception when digikey has no media available for a part
Makes PR #1154 obsolete
2026-01-04 17:50:24 +01:00
Jan Böhmer
bba3bd90a9 Merge remote-tracking branch 'origin/master' 2026-01-04 17:36:57 +01:00
Jan Böhmer
eaaf3ac75c Bring provider capabilities into a fixed order for better comparison
Fixes #1166 and made PR #1167 obsolete
2026-01-04 17:36:53 +01:00
Marc
8957e55a9e Increase default height of the PDF preview container from 250px to 280px and so Chromium-based browsers display the PDF toolbar by default. Fixes #1165. (#1171) 2026-01-04 17:14:27 +01:00
dependabot[bot]
a232671302 Bump actions/upload-artifact from 5 to 6 (#1162)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 5 to 6.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-04 17:05:03 +01:00
Jan Böhmer
5a53423594 Merge remote-tracking branch 'origin/master' 2026-01-04 17:04:50 +01:00
Jan Böhmer
390206f529 Merge remote-tracking branch 'origin/l10n_master' 2026-01-04 17:04:44 +01:00
dependabot[bot]
74862c7bb8 Bump actions/cache from 4 to 5 (#1163)
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-01-04 17:02:23 +01:00
fsbrc
0e61a84ea6 Allow to view part ID in project BOM
* added feature of part-id in project bom view

* Made part id column label translatable

---------

Co-authored-by: Jan Böhmer <mail@jan-boehmer.de>
2026-01-04 17:01:06 +01:00
Jan Böhmer
3e380f82d2 Revert "Declare nativeType of parent property explicitly as workaround for bug in symfony TypeInfo"
This reverts commit 2f580c92d1.
2026-01-03 22:18:10 +01:00
Jan Böhmer
a5d7a5f1d3 Downgrade symfony/type-info to 7.4.0 to prevent issue that fails proper type resolving of static 2026-01-03 22:17:52 +01:00
Jan Böhmer
876cfc0375 Updated dependencies 2026-01-03 22:04:11 +01:00
Jan Böhmer
641c8388c1 Use xxh3 for generating hash keys instead of md5 as it offers better performance 2026-01-03 00:55:49 +01:00
Jan Böhmer
2f580c92d1 Declare nativeType of parent property explicitly as workaround for bug in symfony TypeInfo
Symfony/type-info returns an invalid property type for the parent property based on the @phpstan-var static phpdoc in the parent. It returns some App\Entity\Base\AttachmentType which does not exists.
Symfony issue: https://github.com/symfony/symfony/issues/62922
2026-01-03 00:47:49 +01:00
Jan Böhmer
402edf096d Upgraded yarn dependencies 2026-01-02 18:50:34 +01:00
Jan Böhmer
f467002619 Updated composer dependencies 2026-01-02 18:35:31 +01:00
Jan Böhmer
6fcdc0b0c3 New translations messages.en.xlf (English) 2025-12-07 14:12:08 +01:00
Jan Böhmer
225e347c24 New translations messages.en.xlf (English) 2025-12-06 23:32:23 +01:00
Jan Böhmer
fb805e2e0a New translations validators.en.xlf (English) 2025-12-05 00:40:29 +01:00
Jan Böhmer
8548237522 New translations messages.en.xlf (English) 2025-12-05 00:40:28 +01:00
Jan Böhmer
77819af9a8 New translations security.en.xlf (German) 2025-12-05 00:40:26 +01:00
Jan Böhmer
68217f50c4 New translations messages.en.xlf (English) 2025-12-03 22:01:49 +01:00
Jan Böhmer
d42f728fad New translations messages.en.xlf (English) 2025-12-02 00:13:06 +01:00
Jan Böhmer
b1210bc3b5 New translations messages.en.xlf (English) 2025-11-30 15:57:13 +01:00
Jan Böhmer
045362de0e New translations messages.en.xlf (English) 2025-11-30 14:53:03 +01:00
Jan Böhmer
6a5039326c New translations validators.en.xlf (English) 2025-11-12 22:31:26 +01:00
Jan Böhmer
bee1542cce New translations messages.en.xlf (English) 2025-11-12 22:31:25 +01:00
85 changed files with 6699 additions and 4595 deletions

View File

@@ -37,7 +37,7 @@ jobs:
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -51,7 +51,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
- uses: actions/cache@v5
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
@@ -80,13 +80,13 @@ jobs:
run: zip -r /tmp/partdb_assets.zip public/build/ vendor/
- name: Upload assets artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: Only dependencies and built assets
path: /tmp/partdb_assets.zip
- name: Upload full artifact
uses: actions/upload-artifact@v5
uses: actions/upload-artifact@v6
with:
name: Full Part-DB including dependencies and built assets
path: /tmp/partdb_with_assets.zip

View File

@@ -34,7 +34,7 @@ jobs:
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}

View File

@@ -81,7 +81,7 @@ jobs:
id: composer-cache
run: |
echo "::set-output name=dir::$(composer config cache-files-dir)"
- uses: actions/cache@v4
- uses: actions/cache@v5
with:
path: ${{ steps.composer-cache.outputs.dir }}
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
@@ -92,7 +92,7 @@ jobs:
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v4
- uses: actions/cache@v5
id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}

View File

@@ -46,13 +46,11 @@ RUN apt-get update && apt-get -y install \
&& rm -rvf /var/www/html/*
# Install node and yarn
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - && \
echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list && \
curl -sL https://deb.nodesource.com/setup_22.x | bash - && \
RUN curl -sL https://deb.nodesource.com/setup_22.x | bash - && \
apt-get update && apt-get install -y \
nodejs \
yarn \
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/* && \
npm install -g yarn
# Install composer
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer

View File

@@ -14,31 +14,21 @@ RUN apt-get update && apt-get -y install \
&& apt-get -y autoremove && apt-get clean autoclean && rm -rf /var/lib/apt/lists/*;
RUN set -eux; \
# Prepare keyrings directory
mkdir -p /etc/apt/keyrings; \
\
# Import Yarn GPG key
curl -fsSL https://dl.yarnpkg.com/debian/pubkey.gpg \
| tee /etc/apt/keyrings/yarn.gpg >/dev/null; \
chmod 644 /etc/apt/keyrings/yarn.gpg; \
\
# Add Yarn repo with signed-by
echo "deb [signed-by=/etc/apt/keyrings/yarn.gpg] https://dl.yarnpkg.com/debian stable main" \
| tee /etc/apt/sources.list.d/yarn.list; \
\
# Run NodeSource setup script (unchanged)
# Run NodeSource setup script
curl -sL https://deb.nodesource.com/setup_22.x | bash -; \
\
# Install Node.js + Yarn
# Install Node.js
apt-get update; \
apt-get install -y --no-install-recommends \
nodejs \
yarn; \
nodejs; \
\
# Cleanup
apt-get -y autoremove; \
apt-get clean autoclean; \
rm -rf /var/lib/apt/lists/*
rm -rf /var/lib/apt/lists/*; \
\
# Install Yarn via npm
npm install -g yarn
# Install PHP

View File

@@ -1 +1 @@
2.3.0
2.5.1

View File

@@ -21,6 +21,7 @@ import {Controller} from "@hotwired/stimulus";
import * as bootbox from "bootbox";
import "../../css/components/bootbox_extensions.css";
import accept from "attr-accept";
export default class extends Controller {
static values = {
@@ -112,6 +113,33 @@ export default class extends Controller {
dataTransfer.items.add(file);
rowInput.files = dataTransfer.files;
//Check the file extension and find the corresponding attachment type based on the data-filetype_filter attribute
const attachmentTypeSelect = newElement.querySelector("select");
if (attachmentTypeSelect) {
let foundMatch = false;
for (let j = 0; j < attachmentTypeSelect.options.length; j++) {
const option = attachmentTypeSelect.options[j];
//skip disabled options
if (option.disabled) {
continue;
}
const filter = option.getAttribute('data-filetype_filter');
if (filter) {
if (accept({name: file.name, type: file.type}, filter)) {
attachmentTypeSelect.value = option.value;
foundMatch = true;
break;
}
} else { //If no filter is set, chose this option until we find a better match
if (!foundMatch) {
attachmentTypeSelect.value = option.value;
foundMatch = true;
}
}
}
}
}
});
@@ -189,4 +217,4 @@ export default class extends Controller {
del();
}
}
}
}

View File

@@ -26,9 +26,6 @@ import {marked} from "marked";
import {
trans,
SEARCH_PLACEHOLDER,
SEARCH_SUBMIT,
STATISTICS_PARTS
} from '../../translator';
@@ -82,9 +79,9 @@ export default class extends Controller {
panelPlacement: this.element.dataset.panelPlacement,
plugins: [recentSearchesPlugin],
openOnFocus: true,
placeholder: trans(SEARCH_PLACEHOLDER),
placeholder: trans("search.placeholder"),
translations: {
submitButtonTitle: trans(SEARCH_SUBMIT)
submitButtonTitle: trans("search.submit")
},
// Use a navigator compatible with turbo:
@@ -153,7 +150,7 @@ export default class extends Controller {
},
templates: {
header({ html }) {
return html`<span class="aa-SourceHeaderTitle">${trans(STATISTICS_PARTS)}</span>
return html`<span class="aa-SourceHeaderTitle">${trans("part.labelp")}</span>
<div class="aa-SourceHeaderLine" />`;
},
item({item, components, html}) {
@@ -197,4 +194,4 @@ export default class extends Controller {
}
}
}
}

View File

@@ -18,7 +18,7 @@ export default class extends Controller {
let settings = {
allowEmptyOption: true,
plugins: ['dropdown_input'],
plugins: ['dropdown_input', this.element.required ? null : 'clear_button'],
searchField: ["name", "description", "category", "footprint"],
valueField: "id",
labelField: "name",

View File

@@ -25,8 +25,7 @@ import * as zxcvbnEnPackage from '@zxcvbn-ts/language-en';
import * as zxcvbnDePackage from '@zxcvbn-ts/language-de';
import * as zxcvbnFrPackage from '@zxcvbn-ts/language-fr';
import * as zxcvbnJaPackage from '@zxcvbn-ts/language-ja';
import {trans, USER_PASSWORD_STRENGTH_VERY_WEAK, USER_PASSWORD_STRENGTH_WEAK, USER_PASSWORD_STRENGTH_MEDIUM,
USER_PASSWORD_STRENGTH_STRONG, USER_PASSWORD_STRENGTH_VERY_STRONG} from '../../translator.js';
import {trans} from '../../translator.js';
/* stimulusFetch: 'lazy' */
export default class extends Controller {
@@ -89,23 +88,23 @@ export default class extends Controller {
switch (level) {
case 0:
text = trans(USER_PASSWORD_STRENGTH_VERY_WEAK);
text = trans("user.password_strength.very_weak");
classes = "bg-danger badge-danger";
break;
case 1:
text = trans(USER_PASSWORD_STRENGTH_WEAK);
text = trans("user.password_strength.weak");
classes = "bg-warning badge-warning";
break;
case 2:
text = trans(USER_PASSWORD_STRENGTH_MEDIUM)
text = trans("user.password_strength.medium");
classes = "bg-info badge-info";
break;
case 3:
text = trans(USER_PASSWORD_STRENGTH_STRONG);
text = trans("user.password_strength.strong");
classes = "bg-primary badge-primary";
break;
case 4:
text = trans(USER_PASSWORD_STRENGTH_VERY_STRONG);
text = trans("user.password_strength.very_strong");
classes = "bg-success badge-success";
break;
default:
@@ -120,4 +119,4 @@ export default class extends Controller {
this.badgeTarget.classList.add("badge");
this.badgeTarget.classList.add(...classes.split(" "));
}
}
}

View File

@@ -22,7 +22,7 @@ import '../../css/components/tom-select_extensions.css';
import TomSelect from "tom-select";
import {Controller} from "@hotwired/stimulus";
import {trans, ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB} from '../../translator.js'
import {trans} from '../../translator.js'
import TomSelect_autoselect_typed from '../../tomselect/autoselect_typed/autoselect_typed'
TomSelect.define('autoselect_typed', TomSelect_autoselect_typed)
@@ -204,7 +204,7 @@ export default class extends Controller {
if (data.not_in_db_yet) {
//Not yet added items are shown italic and with a badge
name += "<i><b>" + escape(data.text) + "</b></i>" + "<span class='ms-3 badge bg-info badge-info'>" + trans(ENTITY_SELECT_GROUP_NEW_NOT_ADDED_TO_DB) + "</span>";
name += "<i><b>" + escape(data.text) + "</b></i>" + "<span class='ms-3 badge bg-info badge-info'>" + trans("entity.select.group.new_not_added_to_DB") + "</span>";
} else {
name += "<b>" + escape(data.text) + "</b>";
}

View File

@@ -62,6 +62,6 @@ export default class extends Controller {
element.disabled = true;
}
form.submit();
form.requestSubmit();
}
}
}

View File

@@ -70,6 +70,6 @@ export default class extends Controller {
//Put our decoded Text into the input box
document.getElementById('scan_dialog_input').value = decodedText;
//Submit form
document.getElementById('scan_dialog_form').submit();
document.getElementById('scan_dialog_form').requestSubmit();
}
}
}

View File

@@ -44,7 +44,7 @@ import "./register_events";
import "./tristate_checkboxes";
//Define jquery globally
window.$ = window.jQuery = require("jquery");
global.$ = global.jQuery = require("jquery");
//Use the local WASM file for the ZXing library
import {

View File

@@ -198,6 +198,7 @@ class WebauthnTFA {
{
const resultField = document.getElementById('_auth_code');
resultField.value = JSON.stringify(data)
//requestSubmit() do not work here, probably because the submit is considered invalid. But as we do not use CSFR tokens, it should be fine.
form.submit();
}
@@ -232,4 +233,4 @@ class WebauthnTFA {
}
}
window.webauthnTFA = new WebauthnTFA();
window.webauthnTFA = new WebauthnTFA();

View File

@@ -1,5 +1,6 @@
import { localeFallbacks } from '../var/translations/configuration';
import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-translator';
import { createTranslator } from '@symfony/ux-translator';
import { messages, localeFallbacks } from '../var/translations/index.js';
/*
* This file is part of the Symfony UX Translator package.
*
@@ -9,8 +10,12 @@ import { trans, getLocale, setLocale, setLocaleFallbacks } from '@symfony/ux-tra
* If you use TypeScript, you can rename this file to "translator.ts" to take advantage of types checking.
*/
setLocaleFallbacks(localeFallbacks);
const translator = createTranslator({
messages,
localeFallbacks,
});
export { trans };
export * from '../var/translations';
// Wrapper function with default domain set to 'frontend'
export const trans = (id, parameters = {}, domain = 'frontend', locale = null) => {
return translator.trans(id, parameters, domain, locale);
};

View File

@@ -79,7 +79,8 @@
"symfony/string": "7.4.*",
"symfony/translation": "7.4.*",
"symfony/twig-bundle": "7.4.*",
"symfony/ux-translator": "^2.10",
"symfony/type-info": "7.4.*",
"symfony/ux-translator": "^2.32.0",
"symfony/ux-turbo": "^2.0",
"symfony/validator": "7.4.*",
"symfony/web-link": "7.4.*",

1896
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -35,4 +35,4 @@ knpu_oauth2_client:
provider_options:
urlAuthorize: 'https://identity.nexar.com/connect/authorize'
urlAccessToken: 'https://identity.nexar.com/connect/token'
urlResourceOwnerDetails: ''
urlResourceOwnerDetails: ''

View File

@@ -1,3 +1,12 @@
ux_translator:
# The directory where the JavaScript translations are dumped
dump_directory: '%kernel.project_dir%/var/translations'
# Only include the frontend translation domain in the JavaScript bundle
domains:
- 'frontend'
when@prod:
ux_translator:
# Control whether TypeScript types are dumped alongside translations.
# Disable this if you do not use TypeScript (e.g. in production when using AssetMapper), to speed up cache warmup.
# dump_typescript: false

File diff suppressed because it is too large Load Diff

View File

@@ -5,3 +5,5 @@ files:
translation: /translations/validators.%two_letters_code%.xlf
- source: /translations/security.en.xlf
translation: /translations/security.%two_letters_code%.xlf
- source: /translations/frontend.en.xlf
translation: /translations/frontend.%two_letters_code%.xlf

View File

@@ -21,8 +21,8 @@ differences between them, which might be important for you. Therefore the pros a
are listed here.
{: .important }
You have to choose between the database types before you start using Part-DB and **you can not change it (easily) after
you have started creating data**. So you should choose the database type for your use case (and possible future uses).
While you can change the database platform later (see below), it is still experimental and not recommended.
So you should choose the database type for your use case (and possible future uses).
## Comparison
@@ -180,3 +180,23 @@ and it is automatically used if available.
For SQLite and MySQL < 10.7 it has to be emulated if wanted, which is pretty slow. Therefore it has to be explicitly enabled by setting the
`DATABASE_EMULATE_NATURAL_SORT` environment variable to `1`. If it is 0 the classical binary sorting is used, on these databases. The emulations
might have some quirks and issues, so it is recommended to use a database which supports natural sorting natively, if you want to use it.
## Converting between database platforms
{: .important }
The database conversion is still experimental. Therefore it is recommended to backup your database before performing a conversion, and check if everything works as expected afterwards.
If you want to change the database platform of your Part-DB installation (e.g. from SQLite to MySQL/MariaDB or PostgreSQL, or vice versa), there is the `partdb:migrations:convert-db-platform` console command, which can help you with that:
1. Make a backup of your current database to be safe if something goes wrong (see the backup documentation).
2. Ensure that your database is at the latest schema by running the migrations: `php bin/console doctrine:migrations:migrate`
3. Change the `DATABASE_URL` environment variable to the new database platform and connection information. Copy the old `DATABASE_URL` as you will need it later.
4. Run `php bin/console doctrine:migrations:migrate` again to create the database schema in the new database. You will not need the admin password, that is shown when running the migrations.
5. Run the conversion command, where you have to provide the old `DATABASE_URL` as parameter: `php bin/console partdb:migrations:convert-db-platform <OLD_DATABASE_URL>`
Replace `<OLD_DATABASE_URL` with the actual old `DATABASE_URL` value (e.g. `sqlite:///%kernel.project_dir%/var/app.db`):
```bash
php bin/console partdb:migrations:convert-db-platform sqlite:///%kernel.project_dir%/var/app.db
```
6. The command will purge all data in the new database and copy all data from the old database to the new one. This might take some time and memory depending on the size of your database.
7. Clear the cache: `php bin/console partdb:cache:clear`
8. You can login with your existing user accounts in the new database now. Check if everything works as expected.

View File

@@ -68,6 +68,7 @@ docker exec --user=www-data partdb php bin/console cache:clear
deleted!*
* `settings:migrate-env-to-settings`: Migrate configuration from environment variables to the settings interface.
The value of the environment variable is copied to the settings database, so the environment variable can be removed afterwards without losing the configuration.
* `partdb:migrations:convert-db-platform`: Convert the database platform (e.g. from SQLite to MySQL/MariaDB or PostgreSQL, or vice versa).
## Database commands

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:
* `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
To create a custom provider, you have to create a new class implementing the `InfoProviderInterface` interface. As long

View File

@@ -17,7 +17,7 @@
"popper.js": "^1.14.7",
"regenerator-runtime": "^0.13.9",
"webpack": "^5.74.0",
"webpack-bundle-analyzer": "^4.3.0",
"webpack-bundle-analyzer": "^5.1.1",
"webpack-cli": "^5.1.0",
"webpack-notifier": "^1.15.0"
},
@@ -46,6 +46,7 @@
"@zxcvbn-ts/language-en": "^3.0.1",
"@zxcvbn-ts/language-fr": "^3.0.1",
"@zxcvbn-ts/language-ja": "^3.0.1",
"attr-accept": "^2.2.5",
"barcode-detector": "^3.0.5",
"bootbox": "^6.0.0",
"bootswatch": "^5.1.3",
@@ -65,7 +66,7 @@
"json-formatter-js": "^2.3.4",
"jszip": "^3.2.0",
"katex": "^0.16.0",
"marked": "^16.1.1",
"marked": "^17.0.1",
"marked-gfm-heading-id": "^4.1.1",
"marked-mangle": "^1.0.1",
"pdfmake": "^0.2.2",
@@ -73,5 +74,8 @@
"tom-select": "^2.1.0",
"ts-loader": "^9.2.6",
"typescript": "^5.7.2"
},
"resolutions": {
"jquery": "^3.5.1"
}
}

View File

@@ -0,0 +1,253 @@
<?php
/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2026 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
declare(strict_types=1);
namespace App\Command\Migrations;
use App\Entity\UserSystem\User;
use App\Services\ImportExportSystem\PartKeeprImporter\PKImportHelper;
use Doctrine\Bundle\DoctrineBundle\ConnectionFactory;
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager;
use Doctrine\Migrations\Configuration\Migration\ExistingConfiguration;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\Migrations\DependencyFactory;
use Doctrine\ORM\Id\AssignedGenerator;
use Doctrine\ORM\Mapping\ClassMetadata;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
#[AsCommand('partdb:migrations:convert-db-platform', 'Convert the database to a different platform')]
class DBPlatformConvertCommand extends Command
{
public function __construct(
private readonly EntityManagerInterface $targetEM,
private readonly PKImportHelper $importHelper,
private readonly DependencyFactory $dependencyFactory,
#[Autowire('%kernel.project_dir%')]
private readonly string $kernelProjectDir,
)
{
parent::__construct();
}
public function configure(): void
{
$this
->setHelp('This command allows you to migrate the database from one database platform to another (e.g. from MySQL to PostgreSQL).')
->addArgument('url', InputArgument::REQUIRED, 'The database connection URL of the source database to migrate from');
}
public function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$sourceEM = $this->getSourceEm($input->getArgument('url'));
//Check that both databases are not using the same driver
if ($sourceEM->getConnection()->getDatabasePlatform()::class === $this->targetEM->getConnection()->getDatabasePlatform()::class) {
$io->warning('Source and target database are using the same database platform / driver. This command is only intended to migrate between different database platforms (e.g. from MySQL to PostgreSQL).');
if (!$io->confirm('Do you want to continue anyway?', false)) {
$io->info('Aborting migration process.');
return Command::SUCCESS;
}
}
$this->ensureVersionUpToDate($sourceEM);
$io->note('This command is still in development. If you encounter any problems, please report them to the issue tracker on GitHub.');
$io->warning(sprintf('This command will delete all existing data in the target database "%s". Make sure that you have no important data in the database before you continue!',
$this->targetEM->getConnection()->getDatabase() ?? 'unknown'
));
//$users = $sourceEM->getRepository(User::class)->findAll();
//dump($users);
$io->ask('Please type "DELETE ALL DATA" to continue.', '', function ($answer) {
if (strtoupper($answer) !== 'DELETE ALL DATA') {
throw new \RuntimeException('You did not type "DELETE ALL DATA"!');
}
return $answer;
});
// Example migration logic (to be replaced with actual migration code)
$io->info('Starting database migration...');
//Disable all event listeners on target EM to avoid unwanted side effects
$eventManager = $this->targetEM->getEventManager();
foreach ($eventManager->getAllListeners() as $event => $listeners) {
foreach ($listeners as $listener) {
$eventManager->removeEventListener($event, $listener);
}
}
$io->info('Clear target database...');
$this->importHelper->purgeDatabaseForImport($this->targetEM, ['internal', 'migration_versions']);
$metadata = $this->targetEM->getMetadataFactory()->getAllMetadata();
$io->info('Modifying entity metadata for migration...');
//First we modify each entity metadata to have an persist cascade on all relations
foreach ($metadata as $metadatum) {
$entityClass = $metadatum->getName();
$io->writeln('Modifying cascade and ID settings for entity: ' . $entityClass, OutputInterface::VERBOSITY_VERBOSE);
foreach ($metadatum->getAssociationNames() as $fieldName) {
$mapping = $metadatum->getAssociationMapping($fieldName);
$mapping->cascade = array_unique(array_merge($mapping->cascade, ['persist']));
$mapping->fetch = ClassMetadata::FETCH_EAGER; //Avoid lazy loading issues during migration
$metadatum->setIdGeneratorType(ClassMetadata::GENERATOR_TYPE_NONE);
$metadatum->setIdGenerator(new AssignedGenerator());
}
}
$io->progressStart(count($metadata));
//First we migrate users to avoid foreign key constraint issues
$io->info('Migrating users first to avoid foreign key constraint issues...');
$this->fixUsers($sourceEM);
//Afterward we migrate all entities
foreach ($metadata as $metadatum) {
//skip all superclasses
if ($metadatum->isMappedSuperclass) {
continue;
}
$entityClass = $metadatum->getName();
$io->note('Migrating entity: ' . $entityClass);
$repo = $sourceEM->getRepository($entityClass);
$items = $repo->findAll();
foreach ($items as $index => $item) {
$this->targetEM->persist($item);
}
$this->targetEM->flush();
}
$io->progressFinish();
//Fix sequences / auto increment values on target database
$io->info('Fixing sequences / auto increment values on target database...');
$this->fixAutoIncrements($this->targetEM);
$io->success('Database migration completed successfully.');
if ($io->isVerbose()) {
$io->info('Process took peak memory: ' . round(memory_get_peak_usage(true) / 1024 / 1024, 2) . ' MB');
}
return Command::SUCCESS;
}
/**
* Construct a source EntityManager based on the given connection URL
* @param string $url
* @return EntityManagerInterface
*/
private function getSourceEm(string $url): EntityManagerInterface
{
//Replace any %kernel.project_dir% placeholders
$url = str_replace('%kernel.project_dir%', $this->kernelProjectDir, $url);
$connectionFactory = new ConnectionFactory();
$connection = $connectionFactory->createConnection(['url' => $url]);
return new EntityManager($connection, $this->targetEM->getConfiguration());
}
private function ensureVersionUpToDate(EntityManagerInterface $sourceEM): void
{
//Ensure that target database is up to date
$migrationStatusCalculator = $this->dependencyFactory->getMigrationStatusCalculator();
$newMigrations = $migrationStatusCalculator->getNewMigrations();
if (count($newMigrations->getItems()) > 0) {
throw new \RuntimeException("Target database is not up to date. Please run all migrations (with doctrine:migrations:migrate) before starting the migration process.");
}
$sourceDependencyLoader = DependencyFactory::fromEntityManager(new ExistingConfiguration($this->dependencyFactory->getConfiguration()), new ExistingEntityManager($sourceEM));
$sourceMigrationStatusCalculator = $sourceDependencyLoader->getMigrationStatusCalculator();
$sourceNewMigrations = $sourceMigrationStatusCalculator->getNewMigrations();
if (count($sourceNewMigrations->getItems()) > 0) {
throw new \RuntimeException("Source database is not up to date. Please run all migrations (with doctrine:migrations:migrate) on the source database before starting the migration process.");
}
}
private function fixUsers(EntityManagerInterface $sourceEM): void
{
//To avoid a problem with (Column 'settings' cannot be null) in MySQL we need to migrate the user entities first
//and fix the settings and backupCodes fields
$reflClass = new \ReflectionClass(User::class);
foreach ($sourceEM->getRepository(User::class)->findAll() as $user) {
foreach (['settings', 'backupCodes'] as $field) {
$property = $reflClass->getProperty($field);
if (!$property->isInitialized($user) || $property->getValue($user) === null) {
$property->setValue($user, []);
}
}
$this->targetEM->persist($user);
}
}
private function fixAutoIncrements(EntityManagerInterface $em): void
{
$connection = $em->getConnection();
$platform = $connection->getDatabasePlatform();
if ($platform instanceof PostgreSQLPlatform) {
$connection->executeStatement(
//From: https://wiki.postgresql.org/wiki/Fixing_Sequences
<<<SQL
SELECT 'SELECT SETVAL(' ||
quote_literal(quote_ident(PGT.schemaname) || '.' || quote_ident(S.relname)) ||
', COALESCE(MAX(' ||quote_ident(C.attname)|| '), 1) ) FROM ' ||
quote_ident(PGT.schemaname)|| '.'||quote_ident(T.relname)|| ';'
FROM pg_class AS S,
pg_depend AS D,
pg_class AS T,
pg_attribute AS C,
pg_tables AS PGT
WHERE S.relkind = 'S'
AND S.oid = D.objid
AND D.refobjid = T.oid
AND D.refobjid = C.attrelid
AND D.refobjsubid = C.attnum
AND T.relname = PGT.tablename
ORDER BY S.relname;
SQL);
}
}
}

View File

@@ -40,10 +40,13 @@ use Symfony\Bridge\Doctrine\Attribute\MapEntity;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
use Symfony\Component\HttpClient\Exception\ClientException;
use Symfony\Component\HttpClient\Exception\TransportException;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Contracts\HttpClient\Exception\ExceptionInterface;
use function Symfony\Component\Translation\t;
#[Route('/tools/info_providers')]
@@ -178,6 +181,13 @@ class InfoProviderController extends AbstractController
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
} catch (OAuthReconnectRequiredException $e) {
$this->addFlash('error', t('info_providers.search.error.oauth_reconnect', ['%provider%' => $e->getProviderName()]));
} catch (TransportException $e) {
$this->addFlash('error', t('info_providers.search.error.transport_exception'));
$exceptionLogger->error('Transport error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
} catch (\RuntimeException $e) {
$this->addFlash('error', t('info_providers.search.error.general_exception', ['%type%' => (new \ReflectionClass($e))->getShortName()]));
//Log the exception
$exceptionLogger->error('Error during info provider search: ' . $e->getMessage(), ['exception' => $e]);
}

View File

@@ -319,6 +319,7 @@ class PartListsController extends AbstractController
//As an unchecked checkbox is not set in the query, the default value for all bools have to be false (which is the default argument value)!
$filter->setName($request->query->getBoolean('name'));
$filter->setDbId($request->query->getBoolean('dbid'));
$filter->setCategory($request->query->getBoolean('category'));
$filter->setDescription($request->query->getBoolean('description'));
$filter->setMpn($request->query->getBoolean('mpn'));

View File

@@ -23,6 +23,7 @@ declare(strict_types=1);
namespace App\DataTables\Filters;
use App\DataTables\Filters\Constraints\AbstractConstraint;
use Doctrine\ORM\QueryBuilder;
use Doctrine\DBAL\ParameterType;
class PartSearchFilter implements FilterInterface
{
@@ -33,6 +34,9 @@ class PartSearchFilter implements FilterInterface
/** @var bool Use name field for searching */
protected bool $name = true;
/** @var bool Use id field for searching */
protected bool $dbId = false;
/** @var bool Use category name for searching */
protected bool $category = true;
@@ -120,33 +124,51 @@ class PartSearchFilter implements FilterInterface
public function apply(QueryBuilder $queryBuilder): void
{
$fields_to_search = $this->getFieldsToSearch();
$is_numeric = preg_match('/^\d+$/', $this->keyword) === 1;
// Add exact ID match only when the keyword is numeric
$search_dbId = $is_numeric && (bool)$this->dbId;
//If we have nothing to search for, do nothing
if ($fields_to_search === [] || $this->keyword === '') {
if (($fields_to_search === [] && !$search_dbId) || $this->keyword === '') {
return;
}
//Convert the fields to search to a list of expressions
$expressions = array_map(function (string $field): string {
$expressions = [];
if($fields_to_search !== []) {
//Convert the fields to search to a list of expressions
$expressions = array_map(function (string $field): string {
if ($this->regex) {
return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
}
return sprintf("ILIKE(%s, :search_query) = TRUE", $field);
}, $fields_to_search);
//For regex, we pass the query as is, for like we add % to the start and end as wildcards
if ($this->regex) {
return sprintf("REGEXP(%s, :search_query) = TRUE", $field);
$queryBuilder->setParameter('search_query', $this->keyword);
} else {
//Escape % and _ characters in the keyword
$this->keyword = str_replace(['%', '_'], ['\%', '\_'], $this->keyword);
$queryBuilder->setParameter('search_query', '%' . $this->keyword . '%');
}
}
return sprintf("ILIKE(%s, :search_query) = TRUE", $field);
}, $fields_to_search);
//Use equal expression to just search for exact numeric matches
if ($search_dbId) {
$expressions[] = $queryBuilder->expr()->eq('part.id', ':id_exact');
$queryBuilder->setParameter('id_exact', (int) $this->keyword,
\Doctrine\DBAL\ParameterType::INTEGER);
}
//Add Or concatenation of the expressions to our query
$queryBuilder->andWhere(
$queryBuilder->expr()->orX(...$expressions)
);
//For regex, we pass the query as is, for like we add % to the start and end as wildcards
if ($this->regex) {
$queryBuilder->setParameter('search_query', $this->keyword);
} else {
//Escape % and _ characters in the keyword
$this->keyword = str_replace(['%', '_'], ['\%', '\_'], $this->keyword);
$queryBuilder->setParameter('search_query', '%' . $this->keyword . '%');
//Guard condition
if (!empty($expressions)) {
//Add Or concatenation of the expressions to our query
$queryBuilder->andWhere(
$queryBuilder->expr()->orX(...$expressions)
);
}
}
@@ -183,6 +205,17 @@ class PartSearchFilter implements FilterInterface
return $this;
}
public function isDbId(): bool
{
return $this->dbId;
}
public function setDbId(bool $dbId): PartSearchFilter
{
$this->dbId = $dbId;
return $this;
}
public function isCategory(): bool
{
return $this->category;

View File

@@ -208,6 +208,7 @@ class LogDataTable implements DataTableTypeInterface
$dataTable->add('extra', LogEntryExtraColumn::class, [
'label' => 'log.extra',
'orderable' => false, //Sorting the JSON column makes no sense: MySQL/Sqlite does it via the string representation, PostgreSQL errors out
]);
$dataTable->add('timeTravel', IconLinkColumn::class, [

View File

@@ -29,6 +29,7 @@ use App\DataTables\Helpers\PartDataTableHelper;
use App\Entity\Attachments\Attachment;
use App\Entity\Parts\Part;
use App\Entity\ProjectSystem\ProjectBOMEntry;
use App\Services\ElementTypeNameGenerator;
use App\Services\EntityURLGenerator;
use App\Services\Formatters\AmountFormatter;
use Doctrine\ORM\QueryBuilder;
@@ -41,7 +42,8 @@ use Symfony\Contracts\Translation\TranslatorInterface;
class ProjectBomEntriesDataTable implements DataTableTypeInterface
{
public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper, protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter)
public function __construct(protected TranslatorInterface $translator, protected PartDataTableHelper $partDataTableHelper,
protected EntityURLGenerator $entityURLGenerator, protected AmountFormatter $amountFormatter)
{
}
@@ -79,7 +81,14 @@ class ProjectBomEntriesDataTable implements DataTableTypeInterface
return htmlspecialchars($this->amountFormatter->format($context->getQuantity(), $context->getPart()->getPartUnit()));
},
])
->add('partId', TextColumn::class, [
'label' => $this->translator->trans('project.bom.part_id'),
'visible' => true,
'orderField' => 'part.id',
'render' => function ($value, ProjectBOMEntry $context) {
return $context->getPart() instanceof Part ? (string) $context->getPart()->getId() : '';
},
])
->add('name', TextColumn::class, [
'label' => $this->translator->trans('part.table.name'),
'orderField' => 'NATSORT(part.name)',

View File

@@ -104,7 +104,7 @@ final class FieldHelper
{
$db_platform = $qb->getEntityManager()->getConnection()->getDatabasePlatform();
$key = 'field2_' . md5($field_expr);
$key = 'field2_' . hash('xxh3', $field_expr);
//If we are on MySQL, we can just use the FIELD function
if ($db_platform instanceof AbstractMySQLPlatform) {
@@ -121,4 +121,4 @@ final class FieldHelper
return $qb;
}
}
}

View File

@@ -58,7 +58,7 @@ class EDACategoryInfo
/** @var bool|null If this is set to true, then this part will be excluded in the simulation */
#[Column(type: Types::BOOLEAN, nullable: true)]
#[Groups(['full', 'category:read', 'category:write', 'import'])]
private ?bool $exclude_from_sim = true;
private ?bool $exclude_from_sim = null;
/** @var string|null The KiCAD schematic symbol, which should be used (the path to the library) */
#[Column(type: Types::STRING, nullable: true)]

View File

@@ -109,6 +109,13 @@ class DBElementRepository extends EntityRepository
return [];
}
//Ensure that all IDs are integers and none is null
foreach ($ids as $id) {
if (!is_int($id)) {
throw new \InvalidArgumentException('Non-integer ID given to findByIDInMatchingOrder: ' . var_export($id, true));
}
}
$cache_key = implode(',', $ids);
//Check if the result is already cached

View File

@@ -139,7 +139,7 @@ class FileTypeFilterTools
{
$filter = trim($filter);
return $this->cache->get('filter_exts_'.md5($filter), function (ItemInterface $item) use ($filter) {
return $this->cache->get('filter_exts_'.hash('xxh3', $filter), function (ItemInterface $item) use ($filter) {
$elements = explode(',', $filter);
$extensions = [];

View File

@@ -57,7 +57,7 @@ class UserCacheKeyGenerator
//If the user is null, then treat it as anonymous user.
//When the anonymous user is passed as user then use this path too.
if (!($user instanceof User) || User::ID_ANONYMOUS === $user->getID()) {
return 'user$_'.User::ID_ANONYMOUS;
return 'user$_'.User::ID_ANONYMOUS . '_'.$locale;
}
//Use the unique user id and the locale to generate the key

View File

@@ -189,7 +189,7 @@ class KiCadHelper
"symbolIdStr" => $part->getEdaInfo()->getKicadSymbol() ?? $part->getCategory()?->getEdaInfo()->getKicadSymbol() ?? "",
"exclude_from_bom" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBom() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBom() ?? false),
"exclude_from_board" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromBoard() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromBoard() ?? false),
"exclude_from_sim" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromSim() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromSim() ?? true),
"exclude_from_sim" => $this->boolToKicadBool($part->getEdaInfo()->getExcludeFromSim() ?? $part->getCategory()?->getEdaInfo()->getExcludeFromSim() ?? false),
"fields" => []
];

View File

@@ -274,6 +274,13 @@ class BOMImporter
$entries_by_key = []; // Track entries by name+part combination
$mapped_entries = []; // Collect all mapped entries for validation
// Fetch suppliers once for efficiency
$suppliers = $this->entityManager->getRepository(\App\Entity\Parts\Supplier::class)->findAll();
$supplierSPNKeys = [];
foreach ($suppliers as $supplier) {
$supplierSPNKeys[] = $supplier->getName() . ' SPN';
}
foreach ($csv->getRecords() as $offset => $entry) {
// Apply field mapping to translate column names
$mapped_entry = $this->applyFieldMapping($entry, $field_mapping, $field_priorities);
@@ -400,9 +407,14 @@ class BOMImporter
if (isset($mapped_entry['Manufacturer'])) {
$comment_parts[] = 'Manf: ' . $mapped_entry['Manufacturer'];
}
if (isset($mapped_entry['LCSC'])) {
$comment_parts[] = 'LCSC: ' . $mapped_entry['LCSC'];
// Add supplier part numbers dynamically
foreach ($supplierSPNKeys as $spnKey) {
if (isset($mapped_entry[$spnKey]) && !empty($mapped_entry[$spnKey])) {
$comment_parts[] = $spnKey . ': ' . $mapped_entry[$spnKey];
}
}
if (isset($mapped_entry['Supplier and ref'])) {
$comment_parts[] = $mapped_entry['Supplier and ref'];
}

View File

@@ -39,10 +39,10 @@ class PKImportHelper
* Existing users and groups are not purged.
* This is needed to avoid ID collisions.
*/
public function purgeDatabaseForImport(): void
public function purgeDatabaseForImport(?EntityManagerInterface $entityManager = null, array $excluded_tables = ['users', 'groups', 'u2f_keys', 'internal', 'migration_versions']): void
{
//We use the ResetAutoIncrementORMPurger to reset the auto increment values of the tables. Also it normalizes table names before checking for exclusion.
$purger = new ResetAutoIncrementORMPurger($this->em, ['users', 'groups', 'u2f_keys', 'internal', 'migration_versions']);
$purger = new ResetAutoIncrementORMPurger($entityManager ?? $this->em, $excluded_tables);
$purger->purge();
}

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

@@ -311,6 +311,14 @@ class DigikeyProvider implements InfoProviderInterface
'auth_bearer' => $this->authTokenManager->getAlwaysValidTokenString(self::OAUTH_APP_NAME)
]);
if ($response->getStatusCode() === 404) {
//No media found
return [
'datasheets' => [],
'images' => [],
];
}
$media_array = $response->toArray();
foreach ($media_array['MediaLinks'] as $media_link) {

View File

@@ -397,13 +397,13 @@ class OEMSecretsProvider implements InfoProviderInterface
* Generates a cache key for storing part details based on the provided provider ID.
*
* This method creates a unique cache key by prefixing the provider ID with 'part_details_'
* and hashing the provider ID using MD5 to ensure a consistent and compact key format.
* and hashing the provider ID using XXH3 to ensure a consistent and compact key format.
*
* @param string $provider_id The unique identifier of the provider or part.
* @return string The generated cache key.
*/
private function getCacheKey(string $provider_id): string {
return 'oemsecrets_part_' . md5($provider_id);
return 'oemsecrets_part_' . hash('xxh3', $provider_id);
}
@@ -680,7 +680,7 @@ class OEMSecretsProvider implements InfoProviderInterface
if (is_array($prices)) {
// Step 1: Check if prices exist in the preferred currency
if (isset($prices[$this->settings->currency]) && is_array($prices[$this->settings->currency])) {
$priceDetails = $prices[$this->$this->settings->currency];
$priceDetails = $prices[$this->settings->currency];
foreach ($priceDetails as $priceDetail) {
if (
is_array($priceDetail) &&

View File

@@ -248,4 +248,4 @@ class PollinProvider implements InfoProviderInterface
ProviderCapabilities::DATASHEET
];
}
}
}

View File

@@ -31,9 +31,6 @@ enum ProviderCapabilities
/** Basic information about a part, like the name, description, part number, manufacturer etc */
case BASIC;
/** Information about the footprint of a part */
case FOOTPRINT;
/** Provider can provide a picture for a part */
case PICTURE;
@@ -43,6 +40,24 @@ enum ProviderCapabilities
/** Provider can provide prices for a part */
case PRICE;
/** Information about the footprint of a part */
case FOOTPRINT;
/**
* Get the order index for displaying capabilities in a stable order.
* @return int
*/
public function getOrderIndex(): int
{
return match($this) {
self::BASIC => 1,
self::PICTURE => 2,
self::DATASHEET => 3,
self::PRICE => 4,
self::FOOTPRINT => 5,
};
}
public function getTranslationKey(): string
{
return 'info_providers.capabilities.' . match($this) {

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]
public ?PollinSettings $pollin = null;
#[EmbeddedSettings]
public ?BuerklinSettings $buerklin = null;
}

View File

@@ -718,18 +718,17 @@
"files": []
},
"symfony/ux-translator": {
"version": "2.9",
"version": "2.32",
"recipe": {
"repo": "github.com/symfony/recipes",
"branch": "main",
"version": "2.9",
"ref": "bc396565cc4cab95692dd6df810553dc22e352e1"
"version": "2.32",
"ref": "20e2abac415da4c3a9a6bafa059a6419beb74593"
},
"files": [
"./assets/translator.js",
"./config/packages/ux_translator.yaml",
"./var/translations/configuration.js",
"./var/translations/index.js"
"assets/translator.js",
"config/packages/ux_translator.yaml",
"var/translations/index.js"
]
},
"symfony/ux-turbo": {

View File

@@ -11,6 +11,10 @@
<input type="checkbox" class="form-check-input" id="search_name" name="name" value="1" checked {{ stimulus_controller('elements/localStorage_checkbox') }}>
<label for="search_name" class="form-check-label justify-content-start">{% trans %}name.label{% endtrans %}</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="search_dbid" name="dbid" value="1" {{ stimulus_controller('elements/localStorage_checkbox') }}>
<label for="search_dbid" class="form-check-label justify-content-start">{% trans %}id.label{% endtrans %}</label>
</div>
<div class="form-check">
<input type="checkbox" class="form-check-input" id="search_category" name="category" value="1" checked {{ stimulus_controller('elements/localStorage_checkbox') }}>
<label for="search_category" class="form-check-label justify-content-start">{% trans %}category.label{% endtrans %}</label>
@@ -102,4 +106,4 @@
{{ _self.settings_drodown(is_navbar) }}
{% endif %}
</form>
{% endmacro %}
{% endmacro %}

View File

@@ -27,7 +27,7 @@
title="{% trans %}info_providers.settings.title{% endtrans %}"
><i class="fa-solid fa-cog"></i></a>
{% endif %}
{% for capability in provider.capabilities %}
{% for capability in provider.capabilities|sort((a, b) => a.orderIndex <=> b.orderIndex) %}
{# @var capability \App\Services\InfoProviderSystem\Providers\ProviderCapabilities #}
<span class="badge text-bg-secondary">
<i class="{{ capability.fAIconClass }} fa-fw"></i>

View File

@@ -135,8 +135,8 @@
{% block additional_content %}
{% if pdf_data %}
<div class="card mt-2 p-1 border-secondary" style="resize: vertical; overflow: scroll; height: 250px">
<object id="pdf_preview" data="{{ pdf_data | data_uri(mime='application/pdf') }}"style="height: inherit">
<div class="card mt-2 p-1 border-secondary" style="resize: vertical; overflow: scroll; height: 280px">
<object id="pdf_preview" data="{{ pdf_data | data_uri(mime='application/pdf') }}" style="height: inherit">
</object>
</div>
{% endif %}

View File

@@ -353,6 +353,16 @@ class BOMImporterTest extends WebTestCase
public function testStringToBOMEntriesKiCADSchematic(): void
{
// Create test suppliers for this test
$lcscSupplier = new Supplier();
$lcscSupplier->setName('LCSC');
$mouserSupplier = new Supplier();
$mouserSupplier->setName('Mouser');
$this->entityManager->persist($lcscSupplier);
$this->entityManager->persist($mouserSupplier);
$this->entityManager->flush();
$input = <<<CSV
"Reference","Value","Footprint","Quantity","MPN","Manufacturer","LCSC SPN","Mouser SPN"
"R1,R2","10k","R_0805_2012Metric",2,"CRCW080510K0FKEA","Vishay","C123456","123-M10K"
@@ -386,10 +396,20 @@ class BOMImporterTest extends WebTestCase
$this->assertStringContainsString('Value: 10k', $bom_entries[0]->getComment());
$this->assertStringContainsString('MPN: CRCW080510K0FKEA', $bom_entries[0]->getComment());
$this->assertStringContainsString('Manf: Vishay', $bom_entries[0]->getComment());
$this->assertStringContainsString('LCSC SPN: C123456', $bom_entries[0]->getComment());
$this->assertStringContainsString('Mouser SPN: 123-M10K', $bom_entries[0]->getComment());
// Check second entry
$this->assertEquals('C1', $bom_entries[1]->getMountnames());
$this->assertEquals(1.0, $bom_entries[1]->getQuantity());
$this->assertStringContainsString('LCSC SPN: C789012', $bom_entries[1]->getComment());
$this->assertStringContainsString('Mouser SPN: 80-CL21A104KOCLRNC', $bom_entries[1]->getComment());
// Clean up
$this->entityManager->remove($lcscSupplier);
$this->entityManager->remove($mouserSupplier);
$this->entityManager->flush();
}
public function testStringToBOMEntriesKiCADSchematicWithPriority(): void

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

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="cs">
<file id="frontend.cs">
<unit id="eLrezdb" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Hledat</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment>
<source>part.labelp</source>
<target>Díly</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>Nový (zatím nebyl přidán do DB)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Velmi slabé</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Slabé</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Střední</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Silné</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Velmi silné</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Jdi!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="da">
<file id="frontend.en">
<unit id="eLrezdb" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Søg</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment state="translated">
<source>part.labelp</source>
<target>Dele/parter</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>Ny (endnu ikke tilføjet til databasen)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Meget svag</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Svag</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Middel</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Stærk</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Meget stærk</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Kom nu!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="de">
<file id="frontend.en">
<unit id="eLrezdb" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Suche</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment state="translated">
<source>part.labelp</source>
<target>Bauteile</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>Neu (noch nicht zur DB hinzugefügt)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Sehr schwach</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Schwach</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Mittel</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Stark</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Sehr stark</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Los!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="el">
<file id="frontend.el">
<unit id="lQ8QeGr" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Αναζήτηση</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="en">
<file id="frontend.en">
<unit id="eLrezdb" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Search</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment state="translated">
<source>part.labelp</source>
<target>Parts</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>New (not added to DB yet)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Very weak</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Weak</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Medium</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Strong</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Very strong</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Go!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="es">
<file id="frontend.es">
<unit id="eLrezdb" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Buscar</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment>
<source>part.labelp</source>
<target>Componentes</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>Nuevo (no añadido a la base de datos)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Muy débil</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Débil</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Medio</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Fuerte</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Muy fuerte</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>¡Vamos!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="fr">
<file id="frontend.fr">
<unit id="lQ8QeGr" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Recherche</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment>
<source>part.labelp</source>
<target>Composants</target>
</segment>
</unit>
<unit id="N66qZeD" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Rechercher!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="hu">
<file id="frontend.en">
<unit id="eLrezdb" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Keresés</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment state="translated">
<source>part.labelp</source>
<target>Alkatrészek</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>Új (még nem hozzáadva az adatbázishoz)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Nagyon gyenge</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Gyenge</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Közepes</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Erős</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Nagyon erős</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Indítás!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="it">
<file id="frontend.it">
<unit id="eLrezdb" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Ricerca</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment>
<source>part.labelp</source>
<target>Componenti</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>Nuovo (non ancora aggiunto al DB)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Molto debole</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Debole</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Media</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Forte</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Molto forte</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Cerca!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="ja">
<file id="frontend.ja">
<unit id="lQ8QeGr" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>検索</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment>
<source>part.labelp</source>
<target>部品</target>
</segment>
</unit>
<unit id="N66qZeD" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>検索</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="nl">
<file id="frontend.nl">
<unit id="lQ8QeGr" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Zoeken</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="pl">
<file id="frontend.pl">
<unit id="eLrezdb" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Szukaj</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment>
<source>part.labelp</source>
<target>Komponenty</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>Nowość (jeszcze niedodana do DB)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Bardzo słabe</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Słabe</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Średnie</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Mocne</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Bardzo mocne</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Idź!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="ru">
<file id="frontend.ru">
<unit id="eLrezdb" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>Поиск</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment>
<source>part.labelp</source>
<target>Компоненты</target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>Новый (еще не добавленный в БД)</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Очень слабый</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Слабый</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Средний</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Сильный</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Очень сильный</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Поехали!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="zh">
<file id="frontend.zh">
<unit id="lQ8QeGr" name="search.placeholder">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:67</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">Part-DB1\templates\AdminPages\EntityAdminBase.html.twig:19</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:61</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:27</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:43</note>
<note priority="1">Part-DB1\templates\_sidebar.html.twig:63</note>
<note priority="1">templates\AdminPages\EntityAdminBase.html.twig:9</note>
<note priority="1">templates\base.html.twig:80</note>
<note priority="1">templates\base.html.twig:179</note>
<note priority="1">templates\base.html.twig:206</note>
<note priority="1">templates\base.html.twig:237</note>
</notes>
<segment state="translated">
<source>search.placeholder</source>
<target>搜索</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment>
<source>part.labelp</source>
<target>部件</target>
</segment>
</unit>
<unit id="_cXCaLo" name="entity.select.group.new_not_added_to_DB">
<segment state="translated">
<source>entity.select.group.new_not_added_to_DB</source>
<target>新建(尚未添加到数据库)</target>
</segment>
</unit>
<unit id="RdFvZsb" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>非常弱</target>
</segment>
</unit>
<unit id="IBjmblZ" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>弱</target>
</segment>
</unit>
<unit id="qSm_ID0" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>中</target>
</segment>
</unit>
<unit id="aWAaADS" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>强</target>
</segment>
</unit>
<unit id="Wa9CStW" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>非常强</target>
</segment>
</unit>
<unit id="N66qZeD" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>GO!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -4025,16 +4025,6 @@ Pokud jste to provedli nesprávně nebo pokud počítač již není důvěryhodn
<target>Reg.Ex. shoda</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Jdi!</target>
</segment>
</unit>
<unit id="UXyo9ZT" name="project.labelp">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:37</note>
@@ -11224,36 +11214,6 @@ Element 3</target>
<target>Není uveden žádný textový obsah! Popisky zůstanou prázdné.</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Velmi slabé</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Slabé</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Střední</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Silné</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Velmi silné</target>
</segment>
</unit>
<unit id="m.RBg6w" name="perm.users.impersonate">
<segment state="translated">
<source>perm.users.impersonate</source>

View File

@@ -4032,16 +4032,6 @@ Bemærk også, at uden to-faktor-godkendelse er din konto ikke længere så godt
<target>Reg. Ex. matching</target>
</segment>
</unit>
<unit id="N66qZeD" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Kom nu!</target>
</segment>
</unit>
<unit id="w0jVACo" name="project.labelp">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:37</note>
@@ -11256,36 +11246,6 @@ Oversættelsen
<target>Intet tekstindhold angivet! De oprettede etiketter vil være tomme.</target>
</segment>
</unit>
<unit id="RdFvZsb" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Meget svag</target>
</segment>
</unit>
<unit id="IBjmblZ" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Svag</target>
</segment>
</unit>
<unit id="qSm_ID0" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Middel</target>
</segment>
</unit>
<unit id="aWAaADS" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Stærk</target>
</segment>
</unit>
<unit id="Wa9CStW" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Meget stærk</target>
</segment>
</unit>
<unit id="6OHd5fv" name="perm.users.impersonate">
<segment state="translated">
<source>perm.users.impersonate</source>

File diff suppressed because it is too large Load Diff

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="en">
<file id="messages.en">
<unit id="x_wTSQS" name="attachment_type.caption">
@@ -221,7 +221,7 @@
</notes>
<segment state="final">
<source>part.info.timetravel_hint</source>
<target>This is how the part appeared before %timestamp%. &lt;i&gt;Please note that this feature is experimental, so the info may not be correct.&lt;/i&gt;</target>
<target><![CDATA[This is how the part appeared before %timestamp%. <i>Please note that this feature is experimental, so the info may not be correct.</i>]]></target>
</segment>
</unit>
<unit id="3exvSpl" name="standard.label">
@@ -649,10 +649,10 @@
</notes>
<segment state="translated">
<source>user.edit.tfa.disable_tfa_message</source>
<target>This will disable &lt;b&gt;all active two-factor authentication methods of the user&lt;/b&gt; and delete the &lt;b&gt;backup codes&lt;/b&gt;!
&lt;br&gt;
The user will have to set up all two-factor authentication methods again and print new backup codes! &lt;br&gt;&lt;br&gt;
&lt;b&gt;Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!&lt;/b&gt;</target>
<target><![CDATA[This will disable <b>all active two-factor authentication methods of the user</b> and delete the <b>backup codes</b>!
<br>
The user will have to set up all two-factor authentication methods again and print new backup codes! <br><br>
<b>Only do this if you are absolutely sure about the identity of the user (seeking help), otherwise the account could be compromised by an attacker!</b>]]></target>
</segment>
</unit>
<unit id="APsHYu0" name="user.edit.tfa.disable_tfa.btn">
@@ -803,9 +803,9 @@ The user will have to set up all two-factor authentication methods again and pri
</notes>
<segment state="translated">
<source>entity.delete.message</source>
<target>This can not be undone!
&lt;br&gt;
Sub elements will be moved upwards.</target>
<target><![CDATA[This can not be undone!
<br>
Sub elements will be moved upwards.]]></target>
</segment>
</unit>
<unit id="2tKAqHw" name="entity.delete">
@@ -1359,7 +1359,7 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="final">
<source>homepage.github.text</source>
<target>Source, downloads, bug reports, to-do-list etc. can be found on &lt;a href="%href%" class="link-external" target="_blank"&gt;GitHub project page&lt;/a&gt;</target>
<target><![CDATA[Source, downloads, bug reports, to-do-list etc. can be found on <a href="%href%" class="link-external" target="_blank">GitHub project page</a>]]></target>
</segment>
</unit>
<unit id="D5OKsgU" name="homepage.help.caption">
@@ -1381,7 +1381,7 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="translated">
<source>homepage.help.text</source>
<target>Help and tips can be found in Wiki the &lt;a href="%href%" class="link-external" target="_blank"&gt;GitHub page&lt;/a&gt;</target>
<target><![CDATA[Help and tips can be found in Wiki the <a href="%href%" class="link-external" target="_blank">GitHub page</a>]]></target>
</segment>
</unit>
<unit id="dnirx4v" name="homepage.forum.caption">
@@ -1623,7 +1623,7 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="translated">
<source>email.pw_reset.fallback</source>
<target>If this does not work for you, go to &lt;a href="%url%"&gt;%url%&lt;/a&gt; and enter the following info</target>
<target><![CDATA[If this does not work for you, go to <a href="%url%">%url%</a> and enter the following info]]></target>
</segment>
</unit>
<unit id="DduL9Hu" name="email.pw_reset.username">
@@ -1653,7 +1653,7 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="translated">
<source>email.pw_reset.valid_unit %date%</source>
<target>The reset token will be valid until &lt;i&gt;%date%&lt;/i&gt;.</target>
<target><![CDATA[The reset token will be valid until <i>%date%</i>.]]></target>
</segment>
</unit>
<unit id="8sBnjRy" name="orderdetail.delete">
@@ -3526,8 +3526,8 @@ Sub elements will be moved upwards.</target>
</notes>
<segment state="translated">
<source>tfa_google.disable.confirm_message</source>
<target>If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.&lt;br&gt;
Also note that without two-factor authentication, your account is no longer as well protected against attackers!</target>
<target><![CDATA[If you disable the Authenticator App, all backup codes will be deleted, so you may need to reprint them.<br>
Also note that without two-factor authentication, your account is no longer as well protected against attackers!]]></target>
</segment>
</unit>
<unit id="yu9MSt5" name="tfa_google.disabled_message">
@@ -3547,7 +3547,7 @@ Also note that without two-factor authentication, your account is no longer as w
</notes>
<segment state="translated">
<source>tfa_google.step.download</source>
<target>Download an authenticator app (e.g. &lt;a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2"&gt;Google Authenticator&lt;/a&gt; oder &lt;a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp"&gt;FreeOTP Authenticator&lt;/a&gt;)</target>
<target><![CDATA[Download an authenticator app (e.g. <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2">Google Authenticator</a> oder <a class="link-external" target="_blank" href="https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp">FreeOTP Authenticator</a>)]]></target>
</segment>
</unit>
<unit id="eriwJoR" name="tfa_google.step.scan">
@@ -3789,8 +3789,8 @@ Also note that without two-factor authentication, your account is no longer as w
</notes>
<segment state="translated">
<source>tfa_trustedDevices.explanation</source>
<target>When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed.
If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of &lt;i&gt;all &lt;/i&gt;computers here.</target>
<target><![CDATA[When checking the second factor, the current computer can be marked as trustworthy, so no more two-factor checks on this computer are needed.
If you have done this incorrectly or if a computer is no longer trusted, you can reset the status of <i>all </i>computers here.]]></target>
</segment>
</unit>
<unit id="FZINq8z" name="tfa_trustedDevices.invalidate.confirm_title">
@@ -3959,16 +3959,6 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
<target>Reg.Ex. Matching</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Go!</target>
</segment>
</unit>
<unit id="UXyo9ZT" name="project.labelp">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:37</note>
@@ -5236,7 +5226,7 @@ If you have done this incorrectly or if a computer is no longer trusted, you can
</notes>
<segment state="translated">
<source>label_options.lines_mode.help</source>
<target>If you select Twig here, the content field is interpreted as Twig template. See &lt;a href="https://twig.symfony.com/doc/3.x/templates.html"&gt;Twig documentation&lt;/a&gt; and &lt;a href="https://docs.part-db.de/usage/labels.html#twig-mode"&gt;Wiki&lt;/a&gt; for more information.</target>
<target><![CDATA[If you select Twig here, the content field is interpreted as Twig template. See <a href="https://twig.symfony.com/doc/3.x/templates.html">Twig documentation</a> and <a href="https://docs.part-db.de/usage/labels.html#twig-mode">Wiki</a> for more information.]]></target>
</segment>
</unit>
<unit id="isvxbiX" name="label_options.page_size.label">
@@ -7084,15 +7074,15 @@ Exampletown</target>
</notes>
<segment state="translated">
<source>mass_creation.lines.placeholder</source>
<target>Element 1
<target><![CDATA[Element 1
Element 1.1
Element 1.1.1
Element 1.2
Element 2
Element 3
Element 1 -&gt; Element 1.1
Element 1 -&gt; Element 1.2</target>
Element 1 -> Element 1.1
Element 1 -> Element 1.2]]></target>
</segment>
</unit>
<unit id="TWSqPFi" name="entity.mass_creation.btn">
@@ -9152,25 +9142,25 @@ Element 1 -&gt; Element 1.2</target>
<unit id="r4vDLAt" name="filter.parameter_value_constraint.operator.&lt;">
<segment state="translated">
<source>filter.parameter_value_constraint.operator.&lt;</source>
<target>Typ. Value &lt;</target>
<target><![CDATA[Typ. Value <]]></target>
</segment>
</unit>
<unit id="X9SA3UP" name="filter.parameter_value_constraint.operator.&gt;">
<segment state="translated">
<source>filter.parameter_value_constraint.operator.&gt;</source>
<target>Typ. Value &gt;</target>
<target><![CDATA[Typ. Value >]]></target>
</segment>
</unit>
<unit id="BQGaoQS" name="filter.parameter_value_constraint.operator.&lt;=">
<segment state="translated">
<source>filter.parameter_value_constraint.operator.&lt;=</source>
<target>Typ. Value &lt;=</target>
<target><![CDATA[Typ. Value <=]]></target>
</segment>
</unit>
<unit id="2ha3P6g" name="filter.parameter_value_constraint.operator.&gt;=">
<segment state="translated">
<source>filter.parameter_value_constraint.operator.&gt;=</source>
<target>Typ. Value &gt;=</target>
<target><![CDATA[Typ. Value >=]]></target>
</segment>
</unit>
<unit id="4DaBace" name="filter.parameter_value_constraint.operator.BETWEEN">
@@ -9278,7 +9268,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="4tHhDtU" name="parts_list.search.searching_for">
<segment state="translated">
<source>parts_list.search.searching_for</source>
<target>Searching parts with keyword &lt;b&gt;%keyword%&lt;/b&gt;</target>
<target><![CDATA[Searching parts with keyword <b>%keyword%</b>]]></target>
</segment>
</unit>
<unit id="4vomKLa" name="parts_list.search_options.caption">
@@ -9938,13 +9928,13 @@ Element 1 -&gt; Element 1.2</target>
<unit id="NdZ1t7a" name="project.builds.number_of_builds_possible">
<segment state="translated">
<source>project.builds.number_of_builds_possible</source>
<target><![CDATA[You have enough stocked to build <b>%max_builds%</b> builds of this [project].]]></target>
<target>You have enough stocked to build &lt;b&gt;%max_builds%&lt;/b&gt; builds of this [project].</target>
</segment>
</unit>
<unit id="iuSpPbg" name="project.builds.check_project_status">
<segment state="translated">
<source>project.builds.check_project_status</source>
<target><![CDATA[The current [project] status is <b>"%project_status%"</b>. You should check if you really want to build the [project] with this status!]]></target>
<target>The current [project] status is &lt;b&gt;"%project_status%"&lt;/b&gt;. You should check if you really want to build the [project] with this status!</target>
</segment>
</unit>
<unit id="Y7vSSxi" name="project.builds.following_bom_entries_miss_instock_n">
@@ -10058,7 +10048,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="GzqIwHH" name="entity.select.add_hint">
<segment state="translated">
<source>entity.select.add_hint</source>
<target>Use -&gt; to create nested structures, e.g. "Node 1-&gt;Node 1.1"</target>
<target><![CDATA[Use -> to create nested structures, e.g. "Node 1->Node 1.1"]]></target>
</segment>
</unit>
<unit id="S4CxO.T" name="entity.select.group.new_not_added_to_DB">
@@ -10082,13 +10072,13 @@ Element 1 -&gt; Element 1.2</target>
<unit id="XLnXtsR" name="homepage.first_steps.introduction">
<segment state="translated">
<source>homepage.first_steps.introduction</source>
<target>Your database is still empty. You might want to read the &lt;a href="%url%"&gt;documentation&lt;/a&gt; or start to creating the following data structures:</target>
<target><![CDATA[Your database is still empty. You might want to read the <a href="%url%">documentation</a> or start to creating the following data structures:]]></target>
</segment>
</unit>
<unit id="Q79MOIk" name="homepage.first_steps.create_part">
<segment state="translated">
<source>homepage.first_steps.create_part</source>
<target>Or you can directly &lt;a href="%url%"&gt;create a new part&lt;/a&gt;.</target>
<target><![CDATA[Or you can directly <a href="%url%">create a new part</a>.]]></target>
</segment>
</unit>
<unit id="vplYq4f" name="homepage.first_steps.hide_hint">
@@ -10100,7 +10090,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="MJoZl4f" name="homepage.forum.text">
<segment state="translated">
<source>homepage.forum.text</source>
<target>For questions about Part-DB use the &lt;a href="%href%" class="link-external" target="_blank"&gt;discussion forum&lt;/a&gt;</target>
<target><![CDATA[For questions about Part-DB use the <a href="%href%" class="link-external" target="_blank">discussion forum</a>]]></target>
</segment>
</unit>
<unit id="YsukbnK" name="log.element_edited.changed_fields.category">
@@ -10766,7 +10756,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="p_IxB9K" name="parts.import.help_documentation">
<segment state="translated">
<source>parts.import.help_documentation</source>
<target>See the &lt;a href="%link%"&gt;documentation&lt;/a&gt; for more information on the file format.</target>
<target><![CDATA[See the <a href="%link%">documentation</a> for more information on the file format.]]></target>
</segment>
</unit>
<unit id="awbvhVq" name="parts.import.help">
@@ -10958,7 +10948,7 @@ Element 1 -&gt; Element 1.2</target>
<unit id="o5u.Nnz" name="part.filter.lessThanDesired">
<segment state="translated">
<source>part.filter.lessThanDesired</source>
<target>In stock less than desired (total amount &lt; min. amount)</target>
<target><![CDATA[In stock less than desired (total amount < min. amount)]]></target>
</segment>
</unit>
<unit id="YN9eLcZ" name="part.filter.lotOwner">
@@ -11153,36 +11143,6 @@ Element 1 -&gt; Element 1.2</target>
<target>No text content given! The labels will remain empty.</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Very weak</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Weak</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Medium</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Strong</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Very strong</target>
</segment>
</unit>
<unit id="m.RBg6w" name="perm.users.impersonate">
<segment state="translated">
<source>perm.users.impersonate</source>
@@ -11764,13 +11724,13 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="i68lU5x" name="part.merge.confirm.title">
<segment state="translated">
<source>part.merge.confirm.title</source>
<target>Do you really want to merge &lt;b&gt;%other%&lt;/b&gt; into &lt;b&gt;%target%&lt;/b&gt;?</target>
<target><![CDATA[Do you really want to merge <b>%other%</b> into <b>%target%</b>?]]></target>
</segment>
</unit>
<unit id="k0anzYV" name="part.merge.confirm.message">
<segment state="translated">
<source>part.merge.confirm.message</source>
<target>&lt;b&gt;%other%&lt;/b&gt; will be deleted, and the part will be saved with the shown information.</target>
<target><![CDATA[<b>%other%</b> will be deleted, and the part will be saved with the shown information.]]></target>
</segment>
</unit>
<unit id="mmW5Yl1" name="part.info.merge_modal.title">
@@ -12124,7 +12084,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">
<segment state="translated">
<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>
</unit>
<unit id="ZdUHpZc" name="settings.ips.element14.storeId">
@@ -12136,7 +12096,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">
<segment state="translated">
<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>
</unit>
<unit id="WKWZIm2" name="settings.ips.tme">
@@ -12154,7 +12114,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">
<segment state="translated">
<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>
</unit>
<unit id="yswx4bq" name="settings.ips.tme.secret">
@@ -12202,7 +12162,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">
<segment state="translated">
<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>
</unit>
<unit id="Q66CNjw" name="settings.ips.mouser.searchLimit">
@@ -12280,7 +12240,7 @@ Please note, that you can not impersonate a disabled user. If you try you will g
<unit id="kKv0J3." name="settings.system.attachments">
<segment state="translated">
<source>settings.system.attachments</source>
<target>Attachments &amp; Files</target>
<target><![CDATA[Attachments & Files]]></target>
</segment>
</unit>
<unit id="dsRff8T" name="settings.system.attachments.maxFileSize">
@@ -12304,7 +12264,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">
<segment state="translated">
<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>
</unit>
<unit id=".OyihML" name="settings.system.attachments.downloadByDefault">
@@ -12478,8 +12438,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">
<segment state="translated">
<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.
&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>
<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.
<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>
</unit>
<unit id="cvpTUeY" name="settings.system.privacy">
@@ -12509,7 +12469,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">
<segment state="translated">
<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>
</unit>
<unit id="VwvmcWE" name="settings.behavior.sidebar">
@@ -12527,7 +12487,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">
<segment state="translated">
<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>
</unit>
<unit id="gVSWDkE" name="settings.behavior.sidebar.rootNodeEnabled">
@@ -12575,7 +12535,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">
<segment state="translated">
<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>
</unit>
<unit id="hazr_g5" name="settings.ips.oemsecrets">
@@ -12629,7 +12589,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">
<segment state="translated">
<source>settings.ips.oemsecrets.sortMode.M</source>
<target>Completeness &amp; Manufacturer name</target>
<target><![CDATA[Completeness & Manufacturer name]]></target>
</segment>
</unit>
<unit id="8C9ijHM" name="entity.export.flash.error.no_entities">
@@ -13289,7 +13249,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">
<segment state="translated">
<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>
</unit>
<unit id="CYw3_pS" name="settings.system.customization.showVersionOnHomepage">
@@ -14003,7 +13963,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">
<segment state="translated">
<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>
</unit>
<unit id="xIZ_mEX" name="project.builds.no_bom_entries">
@@ -14032,260 +13992,299 @@ You can do this in the provider info list.</target>
</segment>
</unit>
<unit id="L4nq52R" name="settings.misc.ipn_suggest.useDuplicateDescription.help">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.useDuplicateDescription.help</source>
<target>When enabled, the parts description is used to find existing parts with the same description and to determine the next available IPN by incrementing their numeric suffix for the suggestion list.</target>
</segment>
</unit>
<unit id="NIw6dtz" name="settings.misc.ipn_suggest.regex.help">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.regex.help</source>
<target>A PCRE-compatible regular expression every IPN has to fulfill. Leave empty to allow everything as IPN.</target>
</segment>
</unit>
<unit id="MoHHSNT" name="user.labelp">
<segment>
<segment state="translated">
<source>user.labelp</source>
<target>Users</target>
</segment>
</unit>
<unit id="5.oI1XD" name="currency.labelp">
<segment>
<segment state="translated">
<source>currency.labelp</source>
<target>Currencies</target>
</segment>
</unit>
<unit id="8F2EwVK" name="measurement_unit.labelp">
<segment>
<segment state="translated">
<source>measurement_unit.labelp</source>
<target>Measurement units</target>
</segment>
</unit>
<unit id="hYrcka2" name="attachment_type.labelp">
<segment>
<segment state="translated">
<source>attachment_type.labelp</source>
<target>Attachment types</target>
</segment>
</unit>
<unit id="p.Sjja3" name="label_profile.labelp">
<segment>
<segment state="translated">
<source>label_profile.labelp</source>
<target>Label profiles</target>
</segment>
</unit>
<unit id="Y_ISV0y" name="part_custom_state.labelp">
<segment>
<segment state="translated">
<source>part_custom_state.labelp</source>
<target>Custom part states</target>
</segment>
</unit>
<unit id="aXr7mN." name="group.labelp">
<segment>
<segment state="translated">
<source>group.labelp</source>
<target>Groups</target>
</segment>
</unit>
<unit id="O10voez" name="settings.synonyms.type_synonym.type">
<segment>
<segment state="translated">
<source>settings.synonyms.type_synonym.type</source>
<target>Type</target>
</segment>
</unit>
<unit id="1BDQVEp" name="settings.synonyms.type_synonym.language">
<segment>
<segment state="translated">
<source>settings.synonyms.type_synonym.language</source>
<target>Language</target>
</segment>
</unit>
<unit id="2.g2ewQ" name="settings.synonyms.type_synonym.translation_singular">
<segment>
<segment state="translated">
<source>settings.synonyms.type_synonym.translation_singular</source>
<target>Translation Singular</target>
</segment>
</unit>
<unit id="Up9ZhvR" name="settings.synonyms.type_synonym.translation_plural">
<segment>
<segment state="translated">
<source>settings.synonyms.type_synonym.translation_plural</source>
<target>Translation Plural</target>
</segment>
</unit>
<unit id="BHoS230" name="settings.synonyms.type_synonym.add_entry">
<segment>
<segment state="translated">
<source>settings.synonyms.type_synonym.add_entry</source>
<target>Add entry</target>
</segment>
</unit>
<unit id="wvtOEBn" name="settings.synonyms.type_synonym.remove_entry">
<segment>
<segment state="translated">
<source>settings.synonyms.type_synonym.remove_entry</source>
<target>Remove entry</target>
</segment>
</unit>
<unit id="mLu.2F2" name="settings.synonyms">
<segment>
<segment state="translated">
<source>settings.synonyms</source>
<target>Synonyms</target>
</segment>
</unit>
<unit id="SHgc9i." name="settings.synonyms.help">
<segment>
<segment state="translated">
<source>settings.synonyms.help</source>
<target>The synonyms systems allow overriding how Part-DB call certain things. This can be useful, especially if Part-DB is used in a different context than electronics.
Please note that this system is currently experimental, and the synonyms defined here might not show up at all places.</target>
</segment>
</unit>
<unit id="piB78W5" name="settings.synonyms.type_synonyms">
<segment>
<segment state="translated">
<source>settings.synonyms.type_synonyms</source>
<target>Type synonyms</target>
</segment>
</unit>
<unit id="J8T2HuD" name="settings.synonyms.type_synonyms.help">
<segment>
<segment state="translated">
<source>settings.synonyms.type_synonyms.help</source>
<target>Type synonyms allow you to replace the labels of built-in data types. For example, you can rename "Footprint" to something else.</target>
</segment>
</unit>
<unit id="wjcsjzT" name="log.element_edited.changed_fields.part_ipn_prefix">
<segment>
<segment state="translated">
<source>log.element_edited.changed_fields.part_ipn_prefix</source>
<target>IPN prefix</target>
</segment>
</unit>
<unit id="R4hoCqe" name="part.labelp">
<segment>
<segment state="translated">
<source>part.labelp</source>
<target>Parts</target>
</segment>
</unit>
<unit id=".tjK0ju" name="project_bom_entry.labelp">
<segment>
<segment state="translated">
<source>project_bom_entry.labelp</source>
<target>BOM entries</target>
</segment>
</unit>
<unit id="ftBf11d" name="part_lot.labelp">
<segment>
<segment state="translated">
<source>part_lot.labelp</source>
<target>Part lots</target>
</segment>
</unit>
<unit id="UVDJmYp" name="orderdetail.labelp">
<segment>
<segment state="translated">
<source>orderdetail.labelp</source>
<target>Order details</target>
</segment>
</unit>
<unit id="83AQqv." name="pricedetail.labelp">
<segment>
<segment state="translated">
<source>pricedetail.labelp</source>
<target>Price details</target>
</segment>
</unit>
<unit id="4KRV2mB" name="parameter.labelp">
<segment>
<segment state="translated">
<source>parameter.labelp</source>
<target>Parameters</target>
</segment>
</unit>
<unit id="AAYYeiw" name="part_association.labelp">
<segment>
<segment state="translated">
<source>part_association.labelp</source>
<target>Part associations</target>
</segment>
</unit>
<unit id="2_3Lz7i" name="bulk_info_provider_import_job.labelp">
<segment>
<segment state="translated">
<source>bulk_info_provider_import_job.labelp</source>
<target>Bulk info provider imports</target>
</segment>
</unit>
<unit id="BXTqi16" name="bulk_info_provider_import_job_part.labelp">
<segment>
<segment state="translated">
<source>bulk_info_provider_import_job_part.labelp</source>
<target>Bulk import job part</target>
</segment>
</unit>
<unit id="91Wvg.F" name="password_toggle.hide">
<segment>
<segment state="translated">
<source>password_toggle.hide</source>
<target>Hide</target>
</segment>
</unit>
<unit id="2vciZN7" name="password_toggle.show">
<segment>
<segment state="translated">
<source>password_toggle.show</source>
<target>Show</target>
</segment>
</unit>
<unit id="PVw6Lx4" name="settings.misc.ipn_suggest.regex.help.placeholder">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.regex.help.placeholder</source>
<target>e.g. Format: 34 alphanumeric segments (any number) separated by "-", followed by "-" and 4 digits, e.g., PCOM-RES-0001</target>
</segment>
</unit>
<unit id="M5Q_eZW" name="part.edit.tab.advanced.ipn.prefix.global_prefix">
<segment>
<segment state="translated">
<source>part.edit.tab.advanced.ipn.prefix.global_prefix</source>
<target>The global IPN prefix, common across all parts</target>
</segment>
</unit>
<unit id="WlKvQeB" name="settings.misc.ipn_suggest.fallbackPrefix">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.fallbackPrefix</source>
<target>Fallback prefix</target>
</segment>
</unit>
<unit id="Fr8hXdE" name="settings.misc.ipn_suggest.fallbackPrefix.help">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.fallbackPrefix.help</source>
<target>The IPN prefix that should be used, if a category has no prefix defined.</target>
</segment>
</unit>
<unit id="_p9cMpI" name="settings.misc.ipn_suggest.numberSeparator">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.numberSeparator</source>
<target>Number separator</target>
</segment>
</unit>
<unit id="CqfzuM6" name="settings.misc.ipn_suggest.numberSeparator.help">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.numberSeparator.help</source>
<target>The separator character used to separate the IPN number from the prefix.</target>
</segment>
</unit>
<unit id="PisGfwB" name="settings.misc.ipn_suggest.categorySeparator">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.categorySeparator</source>
<target>Category separator</target>
</segment>
</unit>
<unit id="jlNd0CI" name="settings.misc.ipn_suggest.categorySeparator.help">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.categorySeparator.help</source>
<target>The separator character used to separate different levels of category prefixes.</target>
</segment>
</unit>
<unit id="MkiJuRK" name="settings.misc.ipn_suggest.globalPrefix">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.globalPrefix</source>
<target>Global prefix</target>
</segment>
</unit>
<unit id="Akh9iFg" name="settings.misc.ipn_suggest.globalPrefix.help">
<segment>
<segment state="translated">
<source>settings.misc.ipn_suggest.globalPrefix.help</source>
<target>If enabled, an option for to generate IPN with this global prefix, shared across parts in all categories.</target>
</segment>
</unit>
<unit id="MxKRRx_" name="datatable.datatable.lengthMenu">
<notes>
<note priority="1">Do not remove! Used for datatables rendering.</note>
</notes>
<segment state="translated">
<source>datatable.datatable.lengthMenu</source>
<target>_MENU_</target>
</segment>
<notes>
<note priority="1">Do not remove! Used for datatables rendering.</note>
</notes>
<segment state="translated">
<source>datatable.datatable.lengthMenu</source>
<target>_MENU_</target>
</segment>
</unit>
<unit id="Ae8pGfM" name="settings.ips.buerklin">
<segment state="translated">
<source>settings.ips.buerklin</source>
<target>Buerklin</target>
</segment>
</unit>
<unit id="nlVH1Nb" name="settings.ips.buerklin.username">
<segment state="translated">
<source>settings.ips.buerklin.username</source>
<target>User name</target>
</segment>
</unit>
<unit id="BlR_EQc" name="settings.ips.buerklin.help">
<segment state="translated">
<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">
<segment state="translated">
<source>project.bom.part_id</source>
<target>[Part] ID</target>
</segment>
</unit>
<unit id="WdszgJG" name="info_providers.search.error.general_exception">
<segment state="translated">
<source>info_providers.search.error.general_exception</source>
<target>Unknown error while trying to retrieve parts from info provider: %type%. Check that your providers are configured correctly and access keys are correct. See server logs for more information.</target>
</segment>
</unit>
<unit id="RFhwYWd" name="info_providers.search.error.transport_exception">
<segment state="translated">
<source>info_providers.search.error.transport_exception</source>
<target>Transport error while retrieving information from the providers. Check that your server has internet accesss. See server logs for more info.</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -4024,16 +4024,6 @@ Subelementos serán desplazados hacia arriba.</target>
<target>Reg.Ex. Matching</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>¡Vamos!</target>
</segment>
</unit>
<unit id="UXyo9ZT" name="project.labelp">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:37</note>
@@ -11242,36 +11232,6 @@ Elemento 3</target>
<target>¡No se ha dado contenido de texto! Las etiquetas permanecerán vacías.</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Muy débil</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Débil</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Medio</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Fuerte</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Muy fuerte</target>
</segment>
</unit>
<unit id="m.RBg6w" name="perm.users.impersonate">
<segment state="translated">
<source>perm.users.impersonate</source>

View File

@@ -4014,16 +4014,6 @@ Si vous avez fait cela de manière incorrecte ou si un ordinateur n'est plus fia
<target>Reg.Ex. Correspondance</target>
</segment>
</unit>
<unit id="N66qZeD" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Rechercher!</target>
</segment>
</unit>
<unit id="Kw3N1AA" name="actions">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:2</note>

View File

@@ -3952,16 +3952,6 @@
<target>Reguláris kifejezés egyezés</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Indítás!</target>
</segment>
</unit>
<unit id="UXyo9ZT" name="project.labelp">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:37</note>
@@ -11157,36 +11147,6 @@
<target>Nincs szöveges tartalom megadva! A címkék üresek maradnak.</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Nagyon gyenge</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Gyenge</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Közepes</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Erős</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Nagyon erős</target>
</segment>
</unit>
<unit id="m.RBg6w" name="perm.users.impersonate">
<segment state="translated">
<source>perm.users.impersonate</source>

View File

@@ -4026,16 +4026,6 @@ Se è stato fatto in modo errato o se un computer non è più attendibile, puoi
<target>Corrispondenza Reg.Ex.</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Cerca!</target>
</segment>
</unit>
<unit id="UXyo9ZT" name="project.labelp">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:37</note>
@@ -11244,36 +11234,6 @@ Element 3</target>
<target>Nessun contenuto di testo specificato! Le etichette generate saranno vuote.</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Molto debole</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Debole</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Media</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Forte</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Molto forte</target>
</segment>
</unit>
<unit id="m.RBg6w" name="perm.users.impersonate">
<segment state="translated">
<source>perm.users.impersonate</source>

View File

@@ -4014,16 +4014,6 @@
<target>正規表現で検索</target>
</segment>
</unit>
<unit id="N66qZeD" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>検索</target>
</segment>
</unit>
<unit id="Kw3N1AA" name="actions">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:2</note>

View File

@@ -4029,16 +4029,6 @@ Jeśli zrobiłeś to niepoprawnie lub komputer nie jest już godny zaufania, mo
<target>Dopasowywanie Reg.Ex.</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Idź!</target>
</segment>
</unit>
<unit id="UXyo9ZT" name="project.labelp">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:37</note>
@@ -11247,36 +11237,6 @@ Element 3</target>
<target>Nie podano zawartości tekstowej! Etykiety pozostaną puste.</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Bardzo słabe</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Słabe</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Średnie</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Mocne</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Bardzo mocne</target>
</segment>
</unit>
<unit id="m.RBg6w" name="perm.users.impersonate">
<segment state="translated">
<source>perm.users.impersonate</source>

View File

@@ -4035,16 +4035,6 @@
<target>Соответствие рег.выраж.</target>
</segment>
</unit>
<unit id="U5IhkwB" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>Поехали!</target>
</segment>
</unit>
<unit id="UXyo9ZT" name="project.labelp">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:37</note>
@@ -11251,36 +11241,6 @@
<target>Текстовое содержание не указано! Созданные ярлыки будут пустыми.</target>
</segment>
</unit>
<unit id="9rnHbSK" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>Очень слабый</target>
</segment>
</unit>
<unit id="gKHmHwM" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>Слабый</target>
</segment>
</unit>
<unit id="c44gN8b" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>Средний</target>
</segment>
</unit>
<unit id="NwiBLHc" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>Сильный</target>
</segment>
</unit>
<unit id="Bw.iCUm" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>Очень сильный</target>
</segment>
</unit>
<unit id="m.RBg6w" name="perm.users.impersonate">
<segment state="translated">
<source>perm.users.impersonate</source>

View File

@@ -4033,16 +4033,6 @@
<target>正则匹配</target>
</segment>
</unit>
<unit id="N66qZeD" name="search.submit">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_navbar_search.html.twig:68</note>
<note priority="1">Part-DB1\templates\_navbar_search.html.twig:62</note>
</notes>
<segment state="translated">
<source>search.submit</source>
<target>GO!</target>
</segment>
</unit>
<unit id="w0jVACo" name="project.labelp">
<notes>
<note category="file-source" priority="1">Part-DB1\templates\_sidebar.html.twig:37</note>
@@ -11250,36 +11240,6 @@ Element 3</target>
<target>没有文字内容。标签将保持为空</target>
</segment>
</unit>
<unit id="RdFvZsb" name="user.password_strength.very_weak">
<segment state="translated">
<source>user.password_strength.very_weak</source>
<target>非常弱</target>
</segment>
</unit>
<unit id="IBjmblZ" name="user.password_strength.weak">
<segment state="translated">
<source>user.password_strength.weak</source>
<target>弱</target>
</segment>
</unit>
<unit id="qSm_ID0" name="user.password_strength.medium">
<segment state="translated">
<source>user.password_strength.medium</source>
<target>中</target>
</segment>
</unit>
<unit id="aWAaADS" name="user.password_strength.strong">
<segment state="translated">
<source>user.password_strength.strong</source>
<target>强</target>
</segment>
</unit>
<unit id="Wa9CStW" name="user.password_strength.very_strong">
<segment state="translated">
<source>user.password_strength.very_strong</source>
<target>非常强</target>
</segment>
</unit>
<unit id="6OHd5fv" name="perm.users.impersonate">
<segment state="translated">
<source>perm.users.impersonate</source>

View File

@@ -1,17 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="da">
<file id="security.en">
<unit id="aazoCks" name="user.login_error.user_disabled">
<unit id="GrLNa9P" name="user.login_error.user_disabled">
<segment state="translated">
<source>user.login_error.user_disabled</source>
<target>Din konto er deaktiveret! Kontakt en administrator, hvis du mener, at dette er en fejl.</target>
</segment>
</unit>
<unit id="Dpb9AmY" name="saml.error.cannot_login_local_user_per_saml">
<unit id="IFQ5XrG" name="saml.error.cannot_login_local_user_per_saml">
<segment state="translated">
<source>saml.error.cannot_login_local_user_per_saml</source>
<target>Du kan ikke logge ind som lokal bruger via SSO! Brug dit lokale password i stedet.</target>
</segment>
</unit>
<unit id="wOYPZmb" name="saml.error.cannot_login_saml_user_locally">
<segment state="translated">
<source>saml.error.cannot_login_saml_user_locally</source>
<target>Du kan ikke logge ind som SAML-bruger ved hjælp af lokal godkendelse! Brug SSO-login i stedet.</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff xmlns="urn:oasis:names:tc:xliff:document:2.0" version="2.0" srcLang="en" trgLang="da">
<file id="validators.en">
<unit id="xevSdCK" name="part.master_attachment.must_be_picture">
<unit id="cRbk.cm" name="part.master_attachment.must_be_picture">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentContainingDBElement.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
@@ -42,7 +42,7 @@
<target>Forhåndsvisnings-bilaget skal være et rigtigt billede!</target>
</segment>
</unit>
<unit id="VJHTkxx" name="structural.entity.unique_name">
<unit id="v8HkcJB" name="structural.entity.unique_name">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Attachments\AttachmentType.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Base\AbstractCompany.php:0</note>
@@ -87,7 +87,7 @@
<target>Der eksisterer allerede et element med dette navn på dette niveau!</target>
</segment>
</unit>
<unit id="3ODUtpU" name="parameters.validator.min_lesser_typical">
<unit id="dW7b2B_" name="parameters.validator.min_lesser_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -107,7 +107,7 @@
<target>Værdi skal være mindre end eller lig med den typiske værdi ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="jDBA_WW" name="parameters.validator.min_lesser_max">
<unit id="Yfp2uC5" name="parameters.validator.min_lesser_max">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -127,7 +127,7 @@
<target>Værdi skal være mindre end maksumumværdien ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="ygK_e_X" name="parameters.validator.max_greater_typical">
<unit id="P6b.8Ou" name="parameters.validator.max_greater_typical">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AbstractParameter.php:0</note>
<note category="file-source" priority="1">Part-DB1\src\Entity\Parameters\AttachmentTypeParameter.php:0</note>
@@ -147,7 +147,7 @@
<target>Værdi skal være større eller lig med den typiske værdi ({{ compared_value }}).</target>
</segment>
</unit>
<unit id="isXL.ie" name="validator.user.username_already_used">
<unit id="P41193Y" name="validator.user.username_already_used">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
@@ -157,7 +157,7 @@
<target>Der eksisterer allerede en bruger med dette navn</target>
</segment>
</unit>
<unit id="NcM463r" name="user.invalid_username">
<unit id="EKPQiyf" name="user.invalid_username">
<notes>
<note category="file-source" priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
<note priority="1">Part-DB1\src\Entity\UserSystem\User.php:0</note>
@@ -167,179 +167,209 @@
<target>Brugernavn skal må kun indeholde bogstager, tal, understregningstegn, punktummer, plusser og minusser!</target>
</segment>
</unit>
<unit id="lZvhKYu" name="validator.noneofitschild.self">
<unit id="_v.DMg." name="validator.noneofitschild.self">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment state="translated">
<source>validator.noneofitschild.self</source>
<target>Et element kan ikke være dets eget overordnede element!</target>
<target>Et element kan ikke være overordnet for sig selv!</target>
</segment>
</unit>
<unit id="pr07aV4" name="validator.noneofitschild.children">
<unit id="W90LyFQ" name="validator.noneofitschild.children">
<notes>
<note category="state" priority="1">obsolete</note>
</notes>
<segment state="translated">
<source>validator.noneofitschild.children</source>
<target>Et underelement kan ikke være dets overordnede element (Dette ville resultere i løkker)!</target>
<target>Et underordnet element kan ikke være det overordnede element!</target>
</segment>
</unit>
<unit id="ayNr6QK" name="validator.select_valid_category">
<unit id="GAUS.LK" name="validator.select_valid_category">
<segment state="translated">
<source>validator.select_valid_category</source>
<target>Vælg venligst en gyldig kategori!</target>
</segment>
</unit>
<unit id="6vIlN5q" name="validator.part_lot.only_existing">
<unit id="h6qELde" name="validator.part_lot.only_existing">
<segment state="translated">
<source>validator.part_lot.only_existing</source>
<target>Lagerlokationen er markeret som "Kun eksisterende dele", så derfor kan nye dele ikke tilføjes.</target>
<target>Opbevaringsstedet er markeret som "kun eksisterende dele", så nye dele kan ikke tilføjes.</target>
</segment>
</unit>
<unit id="3xoKOIS" name="validator.part_lot.location_full.no_increase">
<unit id="Prriyy0" name="validator.part_lot.location_full.no_increase">
<segment state="translated">
<source>validator.part_lot.location_full.no_increase</source>
<target>Lokationen er fuld. Antal kan ikke forøges (Ny værdi skal være mindre end {{ old_amount }}).</target>
</segment>
</unit>
<unit id="R6Ov4Yt" name="validator.part_lot.location_full">
<unit id="eeEjB4s" name="validator.part_lot.location_full">
<segment state="translated">
<source>validator.part_lot.location_full</source>
<target>Lokation er fuld. Kan ikke tilføje nye dele til denne.</target>
<target>Lagerpladsen er fuld, så nye dele kan ikke tilføjes.</target>
</segment>
</unit>
<unit id="BNQk2e7" name="validator.part_lot.single_part">
<unit id="2yWi8eP" name="validator.part_lot.single_part">
<segment state="translated">
<source>validator.part_lot.single_part</source>
<target>Lagerlokationen er markeret som "Kun én komponent", så der kan ikke tilføjes yderligere.</target>
<target>Lagerlokationen er markeret som "Kun én komponent", så en ny komponent kan ikke tilføjes.</target>
</segment>
</unit>
<unit id="4gPskOG" name="validator.attachment.must_not_be_null">
<unit id="A.TFhbb" name="validator.attachment.must_not_be_null">
<segment state="translated">
<source>validator.attachment.must_not_be_null</source>
<target>Du skal vælge en bilagstype!</target>
</segment>
</unit>
<unit id="cDDVrWT" name="validator.orderdetail.supplier_must_not_be_null">
<unit id=".lqKoij" name="validator.orderdetail.supplier_must_not_be_null">
<segment state="translated">
<source>validator.orderdetail.supplier_must_not_be_null</source>
<target>Du skal vælge en leverandør!</target>
</segment>
</unit>
<unit id="k5DDdB4" name="validator.measurement_unit.use_si_prefix_needs_unit">
<unit id="bcNZzK." name="validator.measurement_unit.use_si_prefix_needs_unit">
<segment state="translated">
<source>validator.measurement_unit.use_si_prefix_needs_unit</source>
<target>For at kunne aktivere SI-prefixes, så skal du have indtastet et enhedsymbol!</target>
</segment>
</unit>
<unit id="DuzIOCr" name="part.ipn.must_be_unique">
<unit id="gZ5FFL1" name="part.ipn.must_be_unique">
<segment state="translated">
<source>part.ipn.must_be_unique</source>
<target>Det interne partnummer skal være unikt. {{ value }} værdien er allerede i brug!</target>
</segment>
</unit>
<unit id="Z4Kuuo2" name="validator.project.bom_entry.name_or_part_needed">
<unit id="P31Yg.d" name="validator.project.bom_entry.name_or_part_needed">
<segment state="translated">
<source>validator.project.bom_entry.name_or_part_needed</source>
<target>Du skal vælge en komponent eller angive et navn til en ikke-komponent styklistepost!</target>
</segment>
</unit>
<unit id="WF_v4ih" name="project.bom_entry.name_already_in_bom">
<unit id="5CEup_N" name="project.bom_entry.name_already_in_bom">
<segment state="translated">
<source>project.bom_entry.name_already_in_bom</source>
<target>Der findes allerede en BOM linie med dette navn!</target>
</segment>
</unit>
<unit id="5v4p85H" name="project.bom_entry.part_already_in_bom">
<unit id="jB3B50E" name="project.bom_entry.part_already_in_bom">
<segment state="translated">
<source>project.bom_entry.part_already_in_bom</source>
<target>Delen eksisterer allerede i denne BOM!</target>
</segment>
</unit>
<unit id="3lM32Tw" name="project.bom_entry.mountnames_quantity_mismatch">
<unit id="NdkzP1n" name="project.bom_entry.mountnames_quantity_mismatch">
<segment state="translated">
<source>project.bom_entry.mountnames_quantity_mismatch</source>
<target>Antallet af bestykningsnavne skal svare til BOM antallet af komponenter!</target>
</segment>
</unit>
<unit id="x47D5WT" name="project.bom_entry.can_not_add_own_builds_part">
<unit id="8teRCgR" name="project.bom_entry.can_not_add_own_builds_part">
<segment state="translated">
<source>project.bom_entry.can_not_add_own_builds_part</source>
<target>En projekt BOM-liste kan ikke tilføjet til en BOM.</target>
</segment>
</unit>
<unit id="2x2XDI_" name="project.bom_has_to_include_all_subelement_parts">
<unit id="asBxPxe" name="project.bom_has_to_include_all_subelement_parts">
<segment state="translated">
<source>project.bom_has_to_include_all_subelement_parts</source>
<target>Projekt BOM skal indeholde alle underprojekters styklister. Komponent %part_name% fra projekt %project_name% mangler!</target>
</segment>
</unit>
<unit id="U9b1EzD" name="project.bom_entry.price_not_allowed_on_parts">
<unit id="uxaE9Ct" name="project.bom_entry.price_not_allowed_on_parts">
<segment state="translated">
<source>project.bom_entry.price_not_allowed_on_parts</source>
<target>Du kan ikke sætte pris for komponent-BOM indtastninger. Indtast prisen på selve komponenten selv.</target>
</segment>
</unit>
<unit id="ID056SR" name="validator.project_build.lot_bigger_than_needed">
<unit id="xZ68Nzl" name="validator.project_build.lot_bigger_than_needed">
<segment state="translated">
<source>validator.project_build.lot_bigger_than_needed</source>
<target>Du har valgt en større mængde til plukning end nødvendigt. Fjern det overskydende antal</target>
</segment>
</unit>
<unit id="6hV5UqD" name="validator.project_build.lot_smaller_than_needed">
<unit id="68_.V_X" name="validator.project_build.lot_smaller_than_needed">
<segment state="translated">
<source>validator.project_build.lot_smaller_than_needed</source>
<target>Du har valgt et for lille antal til plukning end der er nødvendigt for dette byg. Tilføj yderligere mængde.</target>
</segment>
</unit>
<unit id="G9ZKt.4" name="part.name.must_match_category_regex">
<unit id="yZGS8uZ" name="part.name.must_match_category_regex">
<segment state="translated">
<source>part.name.must_match_category_regex</source>
<target>Komponentnavnet matcher ikke med det regulære udtryk (regular expression) som der er specificeret for kategorien: %regex%</target>
</segment>
</unit>
<unit id="m8kMFhf" name="validator.attachment.name_not_blank">
<unit id="Q8wP5Jd" name="validator.attachment.name_not_blank">
<segment state="translated">
<source>validator.attachment.name_not_blank</source>
<target>Vælg en værdi, eller upload en fil for automatisk at bruge dens filnavn som navn for denne vedhæftede fil.</target>
</segment>
</unit>
<unit id="nwGaNBW" name="validator.part_lot.owner_must_match_storage_location_owner">
<unit id="DH0IkNR" name="validator.part_lot.owner_must_match_storage_location_owner">
<segment state="translated">
<source>validator.part_lot.owner_must_match_storage_location_owner</source>
<target>Ejeren af denne komponentbeholdning og den valgte lagerplacering skal matche (%owner_name%)!</target>
</segment>
</unit>
<unit id="HXSz3nQ" name="validator.part_lot.owner_must_not_be_anonymous">
<unit id="TzySicw" name="validator.part_lot.owner_must_not_be_anonymous">
<segment state="translated">
<source>validator.part_lot.owner_must_not_be_anonymous</source>
<target>Ejeren kan ikke være den anonyme bruger!</target>
</segment>
</unit>
<unit id="N8aA0Uh" name="validator.part_association.must_set_an_value_if_type_is_other">
<unit id="GthNWUb" name="validator.part_association.must_set_an_value_if_type_is_other">
<segment state="translated">
<source>validator.part_association.must_set_an_value_if_type_is_other</source>
<target>Hvis linktypen er sat til "Andet", skal du angive en beskrivende værdi!</target>
</segment>
</unit>
<unit id="9VYNZ4v" name="validator.part_association.part_cannot_be_associated_with_itself">
<unit id="Be4Im81" name="validator.part_association.part_cannot_be_associated_with_itself">
<segment state="translated">
<source>validator.part_association.part_cannot_be_associated_with_itself</source>
<target>En komponent kan ikke knyttes til sig selv!</target>
</segment>
</unit>
<unit id="csc1PNn" name="validator.part_association.already_exists">
<unit id="q5Ej6Xm" name="validator.part_association.already_exists">
<segment state="translated">
<source>validator.part_association.already_exists</source>
<target>Et link til denne komponent findes allerede!</target>
</segment>
</unit>
<unit id="sfW4NYE" name="validator.part_lot.vendor_barcode_must_be_unique">
<unit id="HbI5bga" name="validator.part_lot.vendor_barcode_must_be_unique">
<segment state="translated">
<source>validator.part_lot.vendor_barcode_must_be_unique</source>
<target>Denne leverandørstregkodeværdi er allerede brugt til en anden beholdning. Stregkoden skal være unik!</target>
</segment>
</unit>
<unit id="ufQJh7E" name="validator.year_2038_bug_on_32bit">
<segment state="translated">
<source>validator.year_2038_bug_on_32bit</source>
<target>På grund af tekniske begrænsninger er det ikke muligt at vælge en dato efter 19. januar 2038 på 32-bit systemer!</target>
</segment>
</unit>
<unit id="iM9yb_p" name="validator.fileSize.invalidFormat">
<segment state="translated">
<source>validator.fileSize.invalidFormat</source>
<target>Ugyldig filstørrelse. Brug et heltal med K, M eller G som suffiks for kilo, mega eller gigabyte.</target>
</segment>
</unit>
<unit id="ZFxQ0BZ" name="validator.invalid_range">
<segment state="translated">
<source>validator.invalid_range</source>
<target>Det angivne interval er ikke gyldigt!</target>
</segment>
</unit>
<unit id="m4gp2P_" name="validator.google_code.wrong_code">
<segment state="translated">
<source>validator.google_code.wrong_code</source>
<target>Ugyldig kode. Kontroller, at godkendelsesappen er konfigureret korrekt, og at både serveren og enheden har indstillet det korrekte klokkeslæt.</target>
</segment>
</unit>
<unit id="I330cr5" name="settings.synonyms.type_synonyms.collection_type.duplicate">
<segment state="translated">
<source>settings.synonyms.type_synonyms.collection_type.duplicate</source>
<target>Der er allerede defineret en oversættelse for denne type og sprog!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -347,13 +347,13 @@
<target>Aufgrund technischer Beschränkungen ist es nicht möglich, ein Datum nach dem 19.01.2038 auf 32-Bit Systemen auszuwählen!</target>
</segment>
</unit>
<unit id="89nojXY" name="validator.fileSize.invalidFormat">
<unit id="iM9yb_p" name="validator.fileSize.invalidFormat">
<segment state="translated">
<source>validator.fileSize.invalidFormat</source>
<target>Ungültige Angabe für die Dateigröße. Verwenden Sie eine ganze Zahl mit K, M, G als Suffix für Kilo, Mega oder Gigabytes.</target>
</segment>
</unit>
<unit id="iXcU7ce" name="validator.invalid_range">
<unit id="ZFxQ0BZ" name="validator.invalid_range">
<segment state="translated">
<source>validator.invalid_range</source>
<target>Der gegebene Bereich ist nicht gültig!</target>
@@ -365,5 +365,11 @@
<target>Ungültiger Code. Überprüfen Sie, ob die Authenticator App korrekt eingerichtet ist und ob der Server und das Gerät beide die korrekte Uhrzeit eingestellt haben.</target>
</segment>
</unit>
<unit id="I330cr5" name="settings.synonyms.type_synonyms.collection_type.duplicate">
<segment state="translated">
<source>settings.synonyms.type_synonyms.collection_type.duplicate</source>
<target>Es existiert bereits eine Übersetzung für diesen Typ und Sprache!</target>
</segment>
</unit>
</file>
</xliff>

View File

@@ -366,7 +366,7 @@
</segment>
</unit>
<unit id="I330cr5" name="settings.synonyms.type_synonyms.collection_type.duplicate">
<segment>
<segment state="translated">
<source>settings.synonyms.type_synonyms.collection_type.duplicate</source>
<target>There is already a translation defined for this type and language!</target>
</segment>

View File

@@ -169,22 +169,25 @@ for (const theme of AVAILABLE_THEMES) {
if (Encore.isProduction()) {
Encore.addPlugin(new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
// zlibs `level` option matches Brotlis `BROTLI_PARAM_QUALITY` option.
level: 11,
},
//threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false,
}))
Encore
.addPlugin(new CompressionPlugin({
filename: '[path][base].br',
algorithm: 'brotliCompress',
test: /\.(js|css|html|svg)$/,
compressionOptions: {
// zlibs `level` option matches Brotlis `BROTLI_PARAM_QUALITY` option.
level: 11,
},
threshold: 10240,
minRatio: 0.8,
deleteOriginalAssets: false,
}))
.addPlugin(new CompressionPlugin({
filename: '[path][base].gz',
algorithm: 'gzip',
threshold: 10240,
minRatio: 0.8,
test: /\.(js|css|html|svg)$/,
deleteOriginalAssets: false,
}))

2285
yarn.lock

File diff suppressed because it is too large Load Diff